#!/usr/bin/perl

#    cddb2md - Fetch track title infomation from freedb.org, and
#    write it to a NetMD-capable MiniDisc recorder.
#
#    Copyright Peter Oliver <cddb2md@mavit.org.uk>, 2003-09-28.
#
#    This program is free software; you can redistribute it and/or
#    modify it under the terms of the GNU General Public License,
#    version 2, as published by the Free Software Foundation.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#    http://www.opensource.org/licenses/gpl-license.html

use strict;
use warnings;
use POSIX qw( floor );
use CDDB;
use Term::ReadLine;

# These two subroutines courtesey of Jamie Zawinski, via
# "http://www.freedb.org/modules.php?name=Sections&sop=viewarticle&artid=6".
# Equivalent code is included in CDDB.pm, but I found this first.

# Usage:
#
#      my $id = compute_discid ($leader, @frames);
#
# "$leader" is the number of frames before track 1.
# "@frames" is the length in frames of each track on the disc.
# (A frame is 1/75th of a second.)
# Returns the disc ID as a string.

sub cddb_sum {
  # a number like 2344 becomes 2+3+4+4 (13).
  my ($n) = @_;
  my $ret = 0;
  while ($n > 0) {
    $ret += ($n % 10);
    $n /= 10;
  }
  return $ret;
}

sub compute_discid {
  my @frames = @_;

  my $tracks = $#frames + 1;
  my $n = 0;

  my @start_secs;
  my $i;

  for ($i = 0; $i < $tracks; $i++) {
    $start_secs[$i] = POSIX::floor ($frames[$i] / 75);
  }

  for ($i = 0; $i < $tracks-1; $i++) {
    $n = $n + cddb_sum ($start_secs[$i]);
  }

  my $t = $start_secs[$tracks-1] - $start_secs[0];

  my $id = ((($n % 0xFF) << 24) | ($t << 8) | $tracks-1);
  return sprintf ("%08x", $id);
}


# Get an overview of the disc:
my $totaltracks;
try: foreach my $try ( 1..3 ) {
    open OMDinfo, 'omd info |' 
	or die "Couldn't run omd.  Make sure it is installed an on your path.  Error was: $!";
    while ( defined( my $line = <OMDinfo> ) ) {
	if ( $line =~ m/^No NetMD devices found/ ) {
	    print "No NetMD devices found.\n";
	    exit 2;
	}
	if ( $line =~ m/^Number of tracks: (\d+)/ ) {
	    $totaltracks = $1;
	    last try;
	}
	print "Warning: $1\n"
	    if $line =~ m/^(Disc is write protected\.)$/
	    or $line =~ m/^(Disc media is pre-mastered \(read-only\)\.)$/;
    }
    sleep 2; #Newly attatched devices take a few seconds to initialise.
}
close OMDinfo;
unless ( $totaltracks ) {
    print "No NetMD devices found.\n";
    exit 2;
}

# Find out how many groups there are on the disc.  Could be got from
# "omd list" if I was less lazy.
my @grouplist;
my $manygroups;
{
    open OMDgl, 'omd grouplist |' or die "Couldn't run omd: $!";
    while ( defined( my $line = <OMDgl> ) ) {
	next unless $line =~ m/^\d+\) Start=(\d+)\s+Finish=(\d+): /;
	push @grouplist, $1, $2;
    }
    close OMDgl;

    if ( @grouplist ) {
	$manygroups = 1;
    }
    else {
	@grouplist = ( 1, $totaltracks );
    }
}

open OMD, 'omd list |' or die "Couldn't run omd: $!";
my @OMD = <OMD>;
close OMD;

my $term = Term::ReadLine -> new();
my $cddbp = CDDB -> new or die "$!";
my $thisgroup;
my @timestrings;

# Now iterate over the groups:
  nextgroup: 
while ( my $groupstart = shift @grouplist ) {
    my $groupfinish = shift @grouplist;
    $thisgroup++;

# Figure out the data we need to submit a CDDB query:
    my $offset = 150;
    my @frames = ( $offset );
    while ( defined( my $line = shift @OMD ) ) {
#     if ( $line =~ m/^Total time: (?:(\d+):)?(\d+):(\d\d)\./ ) {
# 	$total = 3600*( defined $1 ? $1 : 0 ) + 60*$2 + $3;
# 	last;
#     }
	next unless $line =~ m/^\s*(\d+)\)\s+(?:(\d+):)?(\d+):(\d\d\.\d\d) -/;
	next if $1 < $groupstart;
	$offset = $offset + 75 * ( 3600*( defined $2 ? $2 : 0 ) + 60*$3 + $4 );
	push @frames, POSIX::floor 0.5 + $offset;
	push @timestrings, $1 if $line =~ m/^\s*\d+\)\s+(.+?)\s/; 
	last if $line =~ m/^\s*$groupfinish\)/;
    }

    unless ( @frames ) {
	print "Sorry, no MiniDisc table of contents found.\n";
	exit 2;
    }

    my $total = POSIX::floor( ( pop @frames ) / 75 );

#print 'cddb query ', compute_discid( @frames ), ' ', 1 + $#frames, " @frames $total\n";
    print "\n";

# Submit the query:
    my @choices = $cddbp->get_discs_by_query( 'cddb query '. compute_discid( @frames ). ' '.( 1 + $#frames ). " @frames $total\n" );

    my %data;
    my $input;
    do {

# Get the user to pick the right catalogue entry:
	my $i;
	print "Group ${thisgroup}:\n" if $manygroups;
	foreach my $disc ( @choices ) {
#	    print $i++, ") ", join( "\t", @$disc ), "\n";
	    printf "%2s) %-7s %8s  %s\n", $i++, @$disc;
	}
	if ( defined( $choices[0] ) ) {
	    print "Pick a disc from the list, or enter a CDDB category and disc-ID as found at \nhttp://www.freedb.org/freedb_search.php\n";
	}
	else {
	    print "Sorry, no automatic matches could be made.  Search for a disc ID at \nhttp://www.freedb.org/freedb_search.php and enter it here.\n";
	}
	if ( @grouplist ) {
	    print "Alternatively, press ^D to move onto the next group.\n";
	}
	else {
	    print "Alternatively, press ^D to exit.\n";
	}

	do {
	    $input = $term -> readline( '> ');
	    unless ( defined $input ) {
		print "\n";
		next nextgroup;
	    }
	} until ( ( $input =~ m/^\d+$/ and 0 <= $input and $input <= $#choices )
		  or ( $input =~ m/^\s*(blues|classical|country|data|folk|jazz|misc|new\s*age|reggae|rock|sound\s*track)[\s\/]+([[:xdigit:]]{8})\s*$/ ) );

# Look up the details of the selected disc:
	if ( $1 ) {
	    %data = %{ $cddbp->get_disc_details( $1, $2 ) };
	}
	else {
	    %data = %{ $cddbp->get_disc_details( @{$choices[$input]}[0,1] ) };
	}    

# Show the tracks to the user and get them to confirm:
	my $j;
	print $data{'dtitle'}, "\n";
	print " Num   MiniDisc     CDDB Title\n";
	foreach my $tracktitle ( @{$data{'ttitles'}} ) {
#	    print "\t", ++$j, ') ', $tracktitle, "\n";
	    printf "%3s) % 10s % 2u:%02u:%02u %s\n", ++$j, $timestrings[$groupstart+$j-2] || "", ( gmtime @{$data{'seconds'}}[$j-1] )[2,1,0], $tracktitle;
	}
	if ( @{$data{'ttitles'}} != $groupfinish - $groupstart + 1 ) {
	    print 'Warning: There are ', $groupfinish - $groupstart + 1,
	    ' tracks on the disc, but ', scalar @{$data{'ttitles'}},
	    " in the catalogue.\n";
	}
	$input = $term -> readline( 'Is this correct (y/n)? ');
    } until ( $input =~ m/^\s*y(es)?/i );
    
# Now write to the disc:
    if ( $manygroups ) {
	system 'omd', 'grname', $thisgroup, $data{'dtitle'} and die "Couldn't run omd: $!";
    }
    else {
	system 'omd', 'title', $data{'dtitle'} and die "Couldn't run omd: $!";
    }
    my $j = $groupstart;
    foreach my $tracktitle ( @{$data{'ttitles'}} ) {
	system 'omd', 'rename', $j++, $tracktitle and die "Couldn't run omd: $!";
	last if $j > $groupfinish;
    }
}

__END__

=head1 NAME

cddb2md.pl - Fetch CDDB track title infomation from freedb.org, and
write it to a NetMD-capable MiniDisc recorder.

=head1 SYNOPSIS

cddb2md.pl

=head1 DESCRIPTION

cddb2md is fully interactive. Give it a whirl.

CDDB disc IDs are computed from the exact lengths of tracks on
CDs. Unfortunately, the track lengths will be slightly different on
MiniDisc to CD, especially if the disc is an analogue recording with
manually added track marks. However, CDDB has a fuzzy match feature,
and, if possible, cddb2md will list likely matches to choose
from. Failing that, you can search by artist and album title at
freedb.org's website, and enter the disc category and ID directly into
cddb2md. The artist, album title and track titles can then be
transfered to the disc.

Of course, for any of this to work, or even be useful, you must first
listen through the disc, making sure that there are the correct number
of tracks, and that the track marks are in roughly the right
places. When making an analogue recording, track marks get automatically
placed when there's a few seconds of silence. If there's no silence
between tracks, or silence in the middle of tracks, you'll need to
manually add or remove the track marks. You also need to remove any
extra silence that's been recorded after the last track.

If you have recorded several albums on one MiniDisc, you also need to
form them into groups using either your recorder's controls or omd's
addgroup option.

=head1 SEE ALSO

L<http://www.mavit.org.uk/cddb2md/>,
L<http://www.sorted.org/~pete/Xmd/>, L<http://www.freedb.org/>

=head1 BUGS

=over 4

=item *

omd fails if the disc and group titles become very long.  If this
happens, use the editing features on your recorder to reduce the
lengths.

=item *

Due to lazyness on my part, the two track time columns in album listing
are in different formats.

=back

=head1 AUTHOR

Peter Oliver <cddb2md@mavit.org.uk>

=head1 VERSION

0.2.1

=cut

ChangeLog:

2004-03-26: Version 0.2.2

    * Check more carefuly for the case where no unit is attached.

2003-09-28: Version 0.2.1

    * display track lengths of both the tracks in the CDDB catalogue and
      on the disc.

2003-09-13:

    * warn if the disc has a different no of tracks to the catalogue
      entry, and don't try to write too many tracks.

2003-09-07: Version 0.2

    * added group support.

2003-08-29: Version 0.1

    * initial release.
