#! /usr/bin/perl -w -I .
#
# autorip is designed to be a simple mp3 rip-and-encode front end.
# No UI, no headaches, no user interaction: ripping begins
# as soon as a CD is dropped into the CD-ROM, and the CD is ejected
# as soon as encoding is complete.  Useful for encoding a large stack
# of CDs with minimum bother.
#
# Requirements:
#	Perl5
#	the MPEG::MP3Info perl module
#	cdparanoia
#	an mp3 encoder

########################################################################
# autorip configuration section
########################################################################

# specify path to cdparanoia.
my $cdparanoia = `which cdparanoia 2>/dev/null` || "/usr/bin/cdparanoia";
my @cdparanoia_opts = ();

# specify bitrate:
$bitrate = 192;

# if LAME, uncomment these lines:
my $encoder = `which lame 2>/dev/null` || '/usr/local/bin/lame';
my @encoder_opts = (
	'--vbr-new',
	'--abr', $bitrate,
	'-S',
);
my $encoder_ext = "mp3";

# if XING, uncomment these lines:
# my $encoder = "/usr/gojira/bin/xingmp3enc";
# my @encoder_opts = ('-B', $bitrate, '-Q');
# my $encoder_ext = "mp3";

# if Ogg Vorbis, uncomment these lines:
# my $encoder = "/usr/bin/oggenc";
# my @encoder_opts = qw/-m 4 -q/;
# my $encoder_ext = "ogg";

# specify path to "eject".
my $eject = "/usr/bin/eject";

# specify path to place extracted mp3s:
my $mp3dir = "."; 

# directory where autorip will store its temporary work files:
my $tmpdir = "/tmp";

# the following contains a list of FreeDB and CDDB servers.  The servers
# will be scanned in order until a match is found.
my @cddb_server_list = (
   # freedb server is the morally correct choice:
   "freedb.freedb.org:888",
   # but cddb.com has a more complete album list
   "us.cddb.com:8880",
   "sj.ca.us.cddb.com:8880",
   "sc.ca.us.cddb.com:8880",
   "cddb.sonic.net:888",
   "sunsite.unc.edu:8880",
   "www.cddb.com:8880",
   "cddb.netads.com:8880",
   "cddb.kagomi.com:8880",
   "cddb.celestial.com:888" );

########################################################################
# end of configurable options.
########################################################################

chomp($cdparanoia);
chomp($encoder);

use MPEG::MP3Info;
use Getopt::Long;
$| = 1;

print <<HEADER ;
autorip 1.0 - the no-frills mp3 encoding front-end.
License: GPL
Author: jmayer\@earthling.net

Simple five-step usage guide:
	1. configure by editing $0
	2. run $0
	3. Drop audio CD into the CD-ROM tray.
	4. Do something else until autorip ejects your CD.
	5. Repeat steps 3-4 until CD supply is exhausted.

HEADER

my $justid = 0;
my $ret = GetOptions ('info', \$justid);

sub escape
{
	my $a = shift;
	# massage the data:
	$a =~ s/[^a-zA-Z0-9\-]+/_/g;
	# whee!
	$a;
}

my $tmpwav = $tmpdir . "/tmp.wav";

while (1) {
	# wait for a cd:
	print "Waiting for next disk...\n";
	{
	  	my $gotcd = 0;
		while (!$gotcd) {
			system("$cdparanoia -qQ &> /dev/null");
			if (($? >> 8) == 0) { $gotcd = 1; }
			else { sleep(2); }
		}
	}

	my $info = CDDB::get();

	if (!defined($info->{DTITLE})) {
		print "ERROR: Could not identify disk!\n";
    		system($eject);
		sleep(2);
		next;
	} 
  
	my ($artist,$title) = split(/\s+\/\s+/, $info->{DTITLE});
	my @track = ();
	while (my($key,$val) = each %$info) {
		if ($key =~ m/^TTITLE(\d+)/) {
			$track[$1] = $val;
		}
	}

	print "-" x 75,"\n";
	print "Title: $title\n";
	print "Artist: $artist\n";
	print "Tracks:\n\to ",join("\n\to ",@track),"\n\n";

	if ($justid) {
		system($eject);
		next;
	}

	# make directory:
	unlink($tmpwav);
	my $subdir = escape($artist) . "-" . escape($title);
	my $dir = $tmpdir."/".$subdir;
	system ("mkdir -p $dir");

# encode
my @tracks_pending = ( 1 .. ($#track+1) );
my @encodes_pending = ();
my $rip_pid = undef;
my $enc_pid = undef;
my $ripping = undef;

my $started_rip = time();

while(1) {
	if (@tracks_pending && ($#encodes_pending < 1) && !defined($rip_pid))
	{
		my $track = shift(@tracks_pending);
		$rip_pid = rip($track);
		$ripping = $track;
	}
	if (@encodes_pending && !defined($enc_pid))
	{
		my $track = shift(@encodes_pending);
		my $tmpwav = $tmpdir . "/tmp.$$.$track.wav";
		my $mp3file = sprintf ("%s/%02d-%s.%s", $dir, $track, 
    			escape($track[$track-1]), $encoder_ext);
		$enc_pid = enc($tmpwav, $mp3file, 
    			$track, $track[$track-1], $artist, $title);
	}
	last if ((!defined($enc_pid)) && (!defined($rip_pid)));
	my $pid = wait();
	if (defined($enc_pid) && $pid == $enc_pid) {
		$enc_pid = undef;
	}
	if (defined($rip_pid) && $pid == $rip_pid) {
		push (@encodes_pending, $ripping);
		$ripping = undef;
		$rip_pid = undef;
	}
}

my $dur_rip = time() - $started_rip;
my $min_rip = int($dur_rip / 60);
my $sec_rip = $dur_rip % 60;

  print "Files moved to: ${mp3dir}/${subdir}\n";
  print "Finished disk (time elapsed: $min_rip:$sec_rip)\n";
  system ("mv $dir ${mp3dir}/${subdir}");	
  system ($eject);
  print "\n";
}

sub rip
{
	my $track = shift;
	my $tmpwav = $tmpdir . "/tmp.$$.$track.wav";
	my $pid = fork();
	return $pid if ($pid);
	
	# I'm a child:
	my $cmd = "$cdparanoia -qw $track $tmpwav";
	print "Started ripping track #$track\n";
	my $start =time();
	system ("$cmd");
	my $dur = time() - $start;
	print "Finished ripping track #$track ($dur secs)\n";
	exit(0);
}

sub enc
{
	my $inputwav = shift || die;
  my $outputmp3 = shift || die;
  my ($tracknum, $track, $artist, $title) = (shift,shift,shift,shift);
	#my $track = shift;
	#my $tmpwav = $tmpdir . "/tmp.$$.$track.wav";
	#my $i = $track - 1;
	#my $mp3file = sprintf ("%s/%02d-%s.mp3", $dir, $track, escape($track[$i]));
	my $pid = fork();
	return $pid if ($pid);
	
	# I'm a child:
	print "Started encoding $track\n";
	my $start = time();

	if ($encoder_ext eq 'mp3') {
	  # mp3
	  system ("$encoder @encoder_opts $inputwav \"${outputmp3}\" 2>&1 > /dev/null");
	  set_mp3tag ($outputmp3, 
		$track,
		$artist,
		$title,
		"",
		"",
		"",
    		int($tracknum));
	my $dur = time()-$start;
	} else {
	  # ogg vorbis
	  system ($encoder, @encoder_opts,
                "-o", $outputmp3,
                "-t", $track,
                "-l", $title,
                "-a", $artist,
                $inputwav);
   	}
	unlink($inputwav);
	my $dur = time()-$start;
	print "Finished encoding ${outputmp3} ($dur secs)\n";
	exit(0);
}

exit(0);

package CDDB;

use FileHandle;
use Data::Dumper;
use IO::Socket;

sub cddb_sum
{
	my $n = shift;
	my $ret = 0;
	while ($n > 0) {
		$ret += ($n % 10);
		$n = int($n / 10);
	}
	return $ret;
}

sub cddb_discid
{
	my $offset = shift;
	my $length = shift;
	
	# print Data::Dumper->Dump([ $offset, $length ]);
	
	my $t = 0;
	my $n = 0;
	my $ref;
	my $track = 1;
	my @o = ();
	foreach $ref (@$offset)
	{
		my $this_off = $ref->[0] * 60 + $ref->[1] + 2;
		# $this_off *= 75;
		# print STDERR "$track: $this_off\n"; $track++;
		$n = $n + cddb_sum( $this_off );
		push (@o, $this_off * 75);
	}
	
	my $lastoff =$offset->[$#$offset]->[3];
	my $lastlen =$length->[$#$length]->[3];
	$t = $lastoff + $lastlen;
	# print STDERR "$lastoff + $lastlen: t = $t / 75...\n";
	$t = int($t / 75);
	# $t = $t & 0xFFFFFF;

	# print STDERR sprintf("n=%x(%d) t=%x(%d)\n",$n,$n,$t,$t);
	
	my $id = (($n & 0xFF) << 24 | $t << 8 | ($#$length+1));
	return sprintf("%08x",$id), ($#$length+1), @o, $t;
}

sub get_toc_cdparanoia
{
	my $ripper = shift;
	
	# print STDERR "Running $ripper:\n";
	my $fh = new FileHandle("$ripper -Q 2>&1 |");

	my @length = ();
	my @offset = ();
	while (defined($_ = <$fh>)) {
		if (  /(\d+)\s+\[(\d+):(\d+)\.(\d+)\].+?(\d+)\s+\[(\d+):(\d+)\.(\d+)\]/  )  {
			# print STDERR $_;
			push (@length, [ $2, $3, $4, $1 ]);  # min, sec, frame, $tracks
			push (@offset, [ $6, $7, $8, $5 ]);  # min, sec, frame, $tracks
		}
	}
	$fh->close();
	# print STDERR "done with $ripper\n";
	
	return \@length, \@offset;
}

sub get_cddb
{
	my @id = @_;

	my $status;
	my %entry;
	my $server;
	foreach $server (@cddb_server_list)
	{
		($status, %entry) = get_cddb_entry ($server, @id);
		last if ($status != 0);
	}
	
	return \%entry;
}

sub myprint
{
	my $fh = shift;
	# print STDERR @_;
	print $fh @_;
}

sub get_cddb_entry
{
	my $server = shift;
	my @id = @_;

	my $sock = new IO::Socket::INET (
		PeerAddr => $server,
		Proto    => 'tcp') || return 0;
	
	$_ = <$sock>; # print STDERR $_;
	return 0 unless (m/^2/);

	my $user = $ENV{USER};
	my $hostname = `hostname`; chomp($hostname);
	myprint ($sock, "cddb hello $user $hostname CDDB.pm 0.1\n");
	$_ = <$sock>; # print STDERR $_;
	return 0 unless (m/^2/);

	myprint ($sock, "cddb query @id\n");
	$_ = <$sock>; print STDERR $_;
	return 0 if (m/^202/); # no match for disc
	return 0 unless (m/^2/);

	my $cat; my $real_id;
	if (m/^200\s+(\S+)\s+(\S+)/) {
		$cat = $1; $real_id = $2;
	} else {
		$_ = <$sock>; # print STDERR $_;
		return 0 unless (m/^(\S+)\s+(\S+)/);
		$cat = $1; $real_id = $2;
		while (defined($_ = <$sock>))
		{ # print STDERR $_;
			last if (m/^\./);
		}
	}
	print STDERR "found real id: $cat $real_id\n";

	myprint ($sock,"cddb read $cat $real_id\n");
	$_ = <$sock>; # print STDERR $_;
	return 0 unless (m/^2/);

	my $info = {};

	while (defined ($_ = <$sock>)) { # print STDERR $_;
		last if (m/^\./);
		next if (m/^#/);
		if (m/^(\S+?)=(.+)/) {
			$info->{$1} = $2;
			$info->{$1} =~ s/\s*$//;
		}
	}

	myprint ($sock,"quit\n");
	$sock->close();

	return (1, %$info);
}

sub get
{
	my ($len, $off) = get_toc_cdparanoia($cdparanoia);
	my @id = cddb_discid($off, $len);
	my $info = get_cddb(@id);
	return $info;
}

sub test
{
	my ($len, $off) = get_toc_cdparanoia($cdparanoia);
	my @id = cddb_discid($off, $len);
	print "Disc ID: @id\n";
	my $info = get_cddb(@id);
	print Data::Dumper->Dump([ $info ]),"\n";
}

