#!/usr/bin/perl -w

use Socket;
use Getopt::Std;
use IPC::Open2;
use IPC::Open3;

#my ($remote,$group,$iaddr,$paddr,$proto,$cmd,$resp);

sub dialog {

  my @load;
  my @dia=('dialog',@_);

  pipe RH,WH or die;
  my $pid=fork;
  defined $pid or die "fork failed";
  unless($pid) {
    close RH;
    open \*STDERR, ">&WH";
    close WH;
    exec @dia or die "Can't start dialog";
  }
  close WH;
  @load=<RH>;
  waitpid($pid,0)==$pid or die "no dialog process";
  if($?) {
    @load and print @load;
    print "\nexiting ...\n";
    exit;
  };
  close RH;
  print "\n";
  return @load;
}

sub open_server {
  my $server=shift;
  my $adr=shift;
  my $group=shift;
  
  socket($server,PF_INET,SOCK_STREAM,getprotobyname('tcp')) or die "socket: $!";
  connect($server,$adr) or die "connect: $!";
  my $resp=<$server>;
  $resp=~m/^20[01] / or die "no newsserver at $opt_l";
  if($group) {
    send $server,"group $group\r\n",0;
    $resp=<$server>;
    $resp=~m/^211 / or die "unknown group $group";
  }
}

sub close_server {
  my $server=shift;
  send $server,"quit\r\n",0;
  $resp=<$server>;
  close($server) or die "close: $!";
}

sub checkmp3 {
  my $in=shift;
  my $out=shift;
  my @resp=`mp3check -en -GZ $in`;
  my $first=0;
  my $junk=0;
  my $trunc=0;
  
  shift(@resp);
  if(@resp && $resp[0]=~m/^(\d+) bytes of junk before first frame header/) {
    $first=$1;
    shift(@resp);
    print "$first bytes of leading junk, ";
  }
  if(@resp && $resp[$#resp]=~m/^frame\s+\d+: (\d+) bytes of junk after last frame/) {
    $junk=$1;
    pop(@resp);
    print "$junk bytes of trailing junk, ";
  } elsif(@resp && $resp[$#resp]=~m/^frame\s+\d+: file truncated, (\d+) bytes missing/) {
    if($1 <= $opt_t) {
      pop(@resp);
    } else {
      print "last $1 bytes missing, ";
      open FRAMES,"mp3check -d $in|" or die "Can't start mp3check -d\n";
      while(<FRAMES>) { $trunc=$_; }
      close(FRAMES);
      $? and  die "Can't get framelist\n"; 
      if($trunc=~m/^\s*(\d+) \(.*\)/) {
	$trunc=($1 > $opt_t ? $1 : 0);
	pop(@resp);
      }
    }
  }
  if(@resp) {
    print "format violations\n";
    rename $in,"$out.broken";
    return @resp;
  }
  print "repairing ... \n" if($first || $trunc || $junk);
  truncate($in,$trunc) if($trunc);
  truncate($in,(-s $in)-$junk) if($junk);
  
  if($first) {
    $first++;
    my $pid=fork;
    defined $pid or die "fork failed";
    unless($pid) {
      open MP3,"> $out";
      open \*STDOUT, ">&MP3";
      exec 'tail','-c',"+$first",$in;
    }
    (waitpid($pid,0)!=$pid || $?) and die "tail failed.\n";
    unlink $in;
  } else {
    rename $in,$out;
  }
  return @resp;
}


sub match_pattern {
  @pattern or return 1;
  for $p (@pattern) {
    ($opt_i ? index(lc $_,lc $p) : index($_,$p)) >=0 and return 1;
  }
  return 0;
}

# Singal handler

$got_int=0;

$cachedir=(($ENV{"MP3CACHEDIR"}."/") || ($ENV{"HOME"}."/.mp3suck/"));
-d $cachedir or $cachedir="";


sub catch_int {
  $got_int=1;
}

$opt_n=$opt_l=$opt_g=$opt_i=$opt_d=$opt_x=$opt_a=$opt_e=
$opt_c=$opt_f=$opt_t=$opt_b=$opt_r=$opt_D=$opt_h="";

$opt_w=-1;

$res=getopts('n:l:g:idx:ae:cft:w:brDh');

$opt_n or $opt_n='news';
$opt_l or $opt_l=$opt_n;
$opt_t or $opt_t=7;
$opt_x or $opt_x=500;
$opt_D and $opt_c=$opt_f=1;

$opt_w==-1 and $opt_w=100;

$ext=($opt_e ? ".$opt_e" : ".mp3");

@pattern=@ARGV;

if(! $res || $opt_h) {
  print <<"EOF";
USAGE: mp3suck [options] newsgroup [pattern ...]
       mp3suck -r [file] ...
Suck mp3-files from  newsserver and optionally check for integrity.
If mp3suck does caching if a $HOME/.mp3cache dir is provided.

-n <newsserver> Newsserver to contact (default: news)
-l <listserver> Newsserver to get article list from (default: as with -n)
-g <group>      Set Newsgroup and skip Newsgroup dialog
-i              ignore case when matching pattern
-d              reopen selection dialog after download
-x <max>        set max. number of songs to display
-a              suppress selection dialog and get all matching files
-e <ext>        name alternate extention (eg. mp2) - turns off all checking
-c              switch off on-the-fly integrity checking
-f              skip final check, don't repair files
-t <bytes>      set max. number of tolerated missing bytes (default: 7)
-w <msecs>      Set number of milliseconds to wait after each 100 lines read
-b              fork to background after selection, logfile is created
-r              repair mode: strip leading and trailing junk from listed files 
-D              dump mode: dump selected articles, don't decode
-h              Display this message and exit
EOF
  exit;
}

if($opt_r) {
  $tmp="mp3suck$$.tmp";
  while($f=shift) {
    if(! -e $f || -e $tmp ) {
      print "can't open $f or tempfile exists. skipping ...\n";
      next;
    } 
    print "checking file $f:\n";
    local $SIG{INT}=\&catch_int;
    rename $f,$tmp or die "can't create tempfile\n";
    if(@resp=checkmp3($tmp,$f)) {
      print @resp,"file is broken, skipping ...\n";
      rename $tmp,$f;
    };
    if($got_int) {
      print "caught SIGINT. exiting ...\n";
      exit;
    };
    $SIG{INT}='DEFAULT'; 
  }
  exit;
}



$adr_n=sockaddr_in(119,inet_aton($opt_n)) or die "server $opt_n not found";
$adr_l=sockaddr_in(119,inet_aton($opt_l)) or die "server $opt_l not found";

select STDOUT; $|=1;

if($opt_g) {
  $group=$opt_g;
} else {
  my (@groups,@dia);
  my ($i,$x,$y,$l);
  
  unless(($y,$x)=split(' ',`stty size`)) {
    warn "Can't determine screen size, using defaults.\n";
    ($y,$x)=(24,80);
  };

  print "getting groups from $opt_l ";

  @dia=('--title','MP3 SUCK','--menu',"MP3 groups on $opt_l",$y,$x,$y-8);
  $l=$x-20;
  
  open_server(LIST,$adr_l);
  send LIST,"list active *binaries*mp3*\r\n",0;
  $resp=<LIST>;
  $resp=~m/^215/ or die "Can't get newsgroup list.\n";
  while(<LIST>) {
    m/^\./ and last;
    print ".";
    m/^(\S+) (\d+) (\d+) / or next;
    push @groups,$1;
    push @dia,($#groups,sprintf("%-${l}.${l}s%7d",$1,$2-$3));
  }
  close_server(LIST);
  print "\n";
  @groups or die "No mp3 groups found.\n";
  @resp=dialog(@dia) or exit;
  $resp[0] or exit;
  $group=$groups[$resp[0]] or exit;
}

print "opening group $group at $opt_l ...\n";

REOPEN:

open_server(LIST,$adr_l);
send LIST,"group $group\r\n",0;
$resp=<LIST>;
if($resp=~m/^211 (\d+) (\d+) (\d+) /) {
  $min=$2; $max=$3;
} else {
  print $resp;
  die "nntp error\n";
}

$cache=$cachedir . $group . '@' . $opt_l;
$newcache=$cache . '.new';
$num=0;

$cachedir and -e "$newcache" and die "$cache is locked, exiting ...\n";

if($cachedir) {
  open NEWCACHE,"> $newcache" or $newcache="";
} else {
  $newcache="";
}

if($cachedir && -e $cache) {
  print "reading cache file $cache ";
  open CACHE,$cache or die;
  while(<CACHE>) {
    $. % 1000 == 1 and print ".";
    (m/^(\d+) (.*)\.${opt_e}.*[\(\[](\d+)%(\d+)[\)\]]/ or next) if($opt_e);
    (m/^(\d+) (.*)\.mp3.*[\(\[](\d+)%(\d+)[\)\]]/ or next) unless($opt_e);
##    if($opt_e) {
##      m/^(\d+) (.*)\.${opt_e}.*[\(\[](\d+)%(\d+)[\)\]]/ or next;
##    } else {
##      m/^(\d+) (.*)\.mp3.*[\(\[](\d+)%(\d+)[\)\]]/ or next;
##    }
    $num=$1;
    $1 < $min and next;
    $newcache and print NEWCACHE;
    match_pattern($_) or next;

    if($song{$2}) {
      if($song{$2}[0]!=$4 || $song{$2}[$3]) { $song{$2}[0]=0; next; }
    } else {
      $song{$2}[0]=$4;
    }
    $song{$2}[$3]=$1;
  }
  close CACHE;  
  print ".\n";
}

$num++;

if($num<=$max) {
  print "looking for new articles at $opt_n "; 
  send LIST,"xhdr Subject $num-$max\r\n",0;
  $resp=<LIST>;
  $resp=~m/^22\d/ or die "server $opt_l doesn't support the XHDR command";

  while(<LIST>) {
    $. % 1000 == 1 and print ".";
    m/^\./ and last;
    s/\.MP3/\.mp3/;
    tr{"/\\}{'%%};
    (m/^(\d+) (.*)\.${opt_e}.*[\(\[](\d+)%(\d+)[\)\]]/ or next) if($opt_e);
    (m/^(\d+) (.*)\.mp3.*[\(\[](\d+)%(\d+)[\)\]]/ or next) unless($opt_e);
##    if($opt_e) { s/$ext/.mp3/g; }
##    m/^(\d+) (.*)\.mp3.*[\(\[](\d+)%(\d+)[\)\]]/ or next;
    ($3 < 1 || $4 < 1 || $3 > $4 || $4 > 999) and next;
    $newcache and print NEWCACHE;
    match_pattern($_) or next;

    if($song{$2}) {
      if($song{$2}[0]!=$4 || $song{$2}[$3]) { $song{$2}[0]=0; next; }
    } else {
      $song{$2}[0]=$4;
    }
    $song{$2}[$3]=$1;
  }
  print ".\n";
}

close_server(LIST);

if($newcache) {
  close NEWCACHE or die;
  $cache and unlink $cache;
  rename $newcache,$cache;
}


# Mark incomplete songs as broken

$total=0;

SONG:

for $s (keys %song) {
  $song{$s}[0] or next;
  for $i (1..$song{$s}[0]) {
    unless($song{$s}[$i]) { $song{$s}[0]=0; next SONG; }
  }
  $total++;
}

unless(@pattern or 1) {
  for $s (keys %song) {
     $s=~s/(\D \d\d) of \d\d(\D)/$1$2/;
     $s=~m/^(.*)\W\W+(\d\d)\W\w+/ or next;
     $cds{$1}++;
  }
  @list=grep $cds{$_}>1,sort keys %cds;
  my ($x,$y);
  unless(($y,$x)=split(' ',`stty size`)) {
    warn "Can't determine screen size, using defaults.\n";
    ($y,$x)=(24,80);
  };
  my @dia=( '--separate-output','--title','MP3 SUCK','--checklist',
            "Possible CDs found in $group",$y,$x,$y-8 );
  for $i (0..$#list) {
    my $l=$x-17-length($#list);
    my $tag=sprintf("%-${l}.${l}s%3d",$list[$i],$cds{$list[$i]});
    push @dia,($i,$tag,'off');
  }
  @load=dialog(@dia);
  if(@load) {
    for $i (@load) {
      push @pattern,$list[$load[$i]];
    }
    goto REOPEN;
  }
}
  

DIALOG:

for(;;) {

# Delete broken or already downloaded songs from songlist

  for $s (keys %song) {
    delete $song{$s} if(! $song{$s}[0] || -e "$s$ext");
  }

  if($opt_x) {
    my @l=sort { $song{$a}[1] < $song{$b}[1] } keys %song;
    $total=$#l;
    while($#l>=$opt_x) { pop @l; }
    @list=sort @l;
  } else {
    @list=sort keys %song;
    $total=$#list;
  }

  @list or die "No songs found.\n";

# popup the selection dialog unless disabled

  if($opt_a) {
    @load=(0..$#list);
  } else {
    my ($x,$y);
    unless(($y,$x)=split(' ',`stty size`)) {
      warn "Can't determine screen size, using defaults.\n";
      ($y,$x)=(24,80);
    };
    my @dia=( '--separate-output','--title','MP3 SUCK','--checklist',
              "$group - $#list of $total songs listed",$y,$x,$y-8 );
    for $i (0..$#list) {
      my $l=$x-17-length($#list);
      $tag=sprintf("%-${l}.${l}s%3d",$list[$i],$song{$list[$i]}[0]);
      push @dia,($i,$tag,'off');
    };
    @load=dialog(@dia);
    @load or @load=(0..$#list);
  }

  print "\nThe following songs are scheduled for loading:\n\n";
  for $i (@load) {
    $s=$list[$i];
    print "$s ($song{$s}[0] parts)\n";
  }
  print "\n";

# Download selected songs

  if($opt_b) {
    $pid=fork;
    defined $pid or die "fork failed";
    if($pid) {
      print "background process (pid $pid) forked, exiting now ...\n";
      exit;
    }
    $SIG{HUP}='IGNORE';
    setpriority 0,0,10;
    open STDOUT,">mp3suck$$.run" or die "Can't create logfile";
    open STDERR,">&STDOUT" or die "Can't dup stdout";
    open STDIN,"/dev/null" or die  "Can't open dummy input file";
    select STDERR; $|=1;
    select STDOUT; $|=1;
    $j=0;
    for $i (@load) {
      $j++;
      $s=$list[$i];
      print "$j: $s [$song{$s}[0]]\n";
    }
    print "\n";
  }
  $SIG{USR1}=sub { $opt_w= ($opt_w>100) ? $opt_w-100 : 0 };
  $SIG{USR2}=sub { $opt_w+=100; };

  $tmp="mp3suck$$.tmp";
  $t0=time;
  $loaded=0;

  open_server(NEWS,$adr_n,$group);

  LOAD:

  for $i (@load) {
    local $SIG{INT}='DEFAULT';
    if($got_int) {
      print "caught SIGINT. exiting ...\n";
      exit;
    };

    $s=$list[$i];
    $n=$song{$s}[0];
    $song{$s}[0]=0;
    $t1=time;
    unlink $tmp;
    if($opt_D) {
      open UU,"> $tmp" or die "Can't open temp-file.\n";
    } else {
      open UU,"|uudecode -o $tmp 2>/dev/null" or die "Can't fork uudecode\n";
    }
    print "Loading \"$s\" [$n]\n";
    $0="mp3suck: [wait $opt_w ms] [0/$n] $s";
    $|=1;
    send NEWS,"body $song{$s}[1]\r\n",0;
    $resp=<NEWS>;
    unless($resp=~m/^222 /) {
      print "Can't get first article.\n",$resp;
      next LOAD;
    }
    if($opt_D) {
      open UU,"> $tmp" or die "Can't open temp-file.\n";
      print "RAW: ";
    } else {

      HEADER:

      while(<NEWS>) {
	if(m/^\.[^\.]/) {
	  print "No UU or MIME header found.\n";
	  next LOAD;
	}
	if(m/^begin/) {
	  open UU,"|uudecode -o $tmp" or die "Can't fork uudecode.\n";
          print "UU: ";
	  print UU;
	  last;
	}
	if(m/^Content-Transfer-Encoding: base64/) {
	  open UU,"|mimencode -u -o $tmp" or die "Can't fork mimencode.\n";
	  while(<NEWS>) {
	    m/^Content/ and next;
	    m/^\t/ and next;
            print "MIME: ";
	    print UU;
	    last HEADER;
	  }
	}
      }
    }
    print "[1] ";
    $0="mp3suck: [wait $opt_w ms] [1/$n] $s";
    while(<NEWS>) {
      m/^\.[^\.]/ and last;
      s/^\.\./\./;
      m/^[ \.\n\r]*$/ and next;
      $opt_w and ($.%100==0) and select undef,undef,undef,$opt_w/1000;
      print UU;
    }
    unless(-s $tmp or $opt_D) {
      print "No tempfile created. Decoding failed.\n";
      close UU;
      next LOAD;
    }
    for $j (2..$n) {
      if(!$opt_c && !$opt_e && $j<5) {
	@resp=`mp3check -en -GZEST $tmp`;
	shift(@resp);
	if($? || @resp) {
	  splice(@resp,3,$#resp-3,("[${$#resp-3} messages skiped]")) if($#resp>4);
	  print "mp3 error.\n",@resp;
	  close UU;
          if(-e $tmp) {
            rename $tmp,"$s$ext.trunc";
          } else {
            system "touch","$s$ext.empty";
          }
	  next LOAD;
	}
      }
      print "[$j] ";
      $0="mp3suck: [wait $opt_w ms] [$j/$n] $s";
      send NEWS,"body $song{$s}[$j]\r\n",0;
      $resp=<NEWS>;
      unless($resp=~m/^222 /) {
	print "nntp error.\n",$resp;
	close UU;
	next LOAD;
      }
      while(<NEWS>) {
	m/^\.[^\.]/ and last;
	s/^\.\./\./;
	m/^[ \.\n\r]*$/ and next;
        $opt_w and ($.%100==0) and select undef,undef,undef,$opt_w/1000;
	print UU;
      }
    }
    $|=0;

    $SIG{INT}=\&catch_int;

    if($opt_D) {
      close(UU);
      print "done\n";
      $loaded++;
      rename($tmp,"$s.uu");
      next LOAD;
    }

    close(UU);
    $resp=$?;
    if(! -e $tmp) {
      print "decode error.\nno tempfile created.\n";
      next LOAD;
    };
#    if($?) {
#      print "code error.\nuudecode failed.\n";
#      next LOAD;
#    }
    $size=-s $tmp;
    $rate=int($size/(time-$t1));
    print "$size bytes loaded ($rate bps).\n";

    if($opt_f || $opt_e) {
      $loaded++;
      rename($tmp,"$s$ext");
      next LOAD;
    }

    if(@resp=checkmp3($tmp,"$s$ext")) {
      print @resp;
      next LOAD;
    }
    $loaded++;

  }
  unlink $tmp if($tmp and -e $tmp);
  $secs=time-$t0;
  $mins=int($secs / 60);
  $secs%=60;
  print "$loaded files loaded in $mins min $secs sec.\n";
  $|=0;

  last unless($opt_d && !opt_a && !opt_b);
}

-e "mp3suck$$.run" and rename("mp3suck$$.run","mp3suck$$.log");

exit;

