#!/usr/bin/perl -w
my $RCS_Id = '$Id: mp3getcddb.pl,v 1.4 2003/08/01 13:42:04 jv Exp $ ';

# Author          : Johan Vromans
# Created On      : Apr  6 09:57:26 2003
# Last Modified By: Johan Vromans
# Last Modified On: Fri Aug  1 15:35:25 2003
# Update Count    : 90
# Status          : Unknown, Use with caution!

################ Common stuff ################

$VERSION = sprintf("%d.%02d", '$Revision: 1.4 $ ' =~ /: (\d+)\.(\d+)/);

use strict;

# Package name.
my $my_package = 'Sciurix';
# Program name and version.
my ($my_name, $my_version) = $RCS_Id =~ /: (.+).pl,v ([\d.]+)/;
# Tack '*' if it is not checked in into RCS.
$my_version .= '*' if length('$Locker:  $ ') > 12;

################ Command line parameters ################

my $gracenote = 2;		# use Gracenote (CDDB.com)
my $freecddb = 1;		# use FreeDB (freedb.org)
my $verbose = 0;		# more verbosity

# Development options (not shown with --help).
my $debug = 0;			# debugging
my $trace = 0;			# trace (show process)
my $test = 0;			# test mode.

# Process command line options.
app_options();

# Post-processing.
$trace |= ($debug || $test);

################ Presets ################

my $TMPDIR = $ENV{TMPDIR} || $ENV{TEMP} || '/usr/tmp';

################ The Process ################

use MP3::Info;
use CDDB;
use File::Basename;

foreach my $dir ( @ARGV ) {
    $dir =~ s;/+$;;;
    unless ( -d $dir ) {
	warn("$dir: not a directory -- skipped\n");
	next;
    }

    my (@toc, @tracks);
    my $album_status = 1;

    # Note we can escape meta characters, but they seem to cause trouble
    # with MP3 lib anyway. Let it fail.

    if ( $dir =~ /([\[\{\?\*\\])/ ) {
	warn("$dir: potential problematic directory name\n");
    }
    foreach my $file ( sort(glob("$dir/*.mp3")) ) {
	my $info = get_mp3info($file);
	if ( $info ) {
	    push(@toc, $info);
	    push(@tracks, $file);
	}
	else {
	    warn("$file does not seem to be a valid mp3\n");
	}
    }

    if ( $verbose ) {
	warn("$dir\n  Found " . scalar(@tracks) . " tracks\n");
	foreach ( @tracks ) {
	    warn("    " . basename($_) . "\n")
	      if $verbose > 1;
	}
    }
    if ( !@tracks ) {
	warn("$dir: no tracks -- skipped\n");
	next;
    }

    my $query = query_builder( toc => \@toc );
    if ( $debug ) {
	use Data::Dumper;
	print STDERR Dumper(\@toc);
	print STDERR Dumper($query);
    }
    my $cddb;
    my @cddb_discs;

    if ( $freecddb ) {
	$cddb = new CDDB (Debug => $debug)
	  or die("Unable to connect to FreeCDDB: $!");

	@cddb_discs = $cddb->get_discs( $query->{discid},
					$query->{frames},
					$query->{disctime},
				      );
	if ( @cddb_discs ) {
	    # By default, don't poll gracenote if we've got something.
	    $gracenote = 0 if $gracenote == 2;
	}
	else {
	    warn("  FreeDB query failed\n") if $verbose;
	}
    }

    if ( $gracenote ) {
	$cddb = new CDDB (Host => "cddb.cddb.com", Port => 8880,
			  Client_Name => "xmcd", Client_Version => "2.6PL0",
			  Debug => $debug)
	  or die("Unable to connect to CDDB: $!\n");

	my @a = $cddb->get_discs( $query->{discid},
				  $query->{frames},
				  $query->{disctime},
				);
	# CDDB sometimes returns an undef entry.
	if ( @a && $a[0] ) {
	    push(@cddb_discs, @a);
	}
	else {
	    warn("  CDDB query failed\n") if $verbose;
	}
    }

    my $i = "00";
    if ( @cddb_discs ) {
	warn("  Found " . @cddb_discs . " matches\n");
	foreach (@cddb_discs) {
	    unless ( $_ ) {
		warn("  Oops -- undefined result\n");
		next;
	    }
	    if ( $debug ) {
		use Data::Dumper;
		print STDERR Dumper($_);
	    }
	    my $disc_info = $cddb->get_disc_details($_->[0], $_->[1]);
	    if ( open(my $fh,">$dir/.cddb$i") ) {
		print $fh $disc_info->{xmcd_record};
		close($fh);
		warn("  Created .cddb$i: ".$_->[0]." ".$_->[1]." ".$_->[2]."\n");
		$i++;
	    }
	    else {
		warn("$dir/.cddb$i: $!\n");
		warn("  Could not create .cddb$i: ".$_->[0]." ".$_->[1]." ".$_->[2]."\n");
	    }
	    if ( $verbose ) {
		my $i = 1;
		map { printf STDERR ("    %2d. %s\n", $i, $_); $i++ }
		  @{$disc_info->{ttitles}};
	    }
	}
    }
    else {
	warn("  Could not find any matches\n");
    }
}

################ Subroutines ################


sub query_builder {
   my %a = @_;
   die("Need a toc to build a query") unless $a{toc};

   my $discid       = disc_id(toc => $a{toc});
   my $disctime     = disc_time(toc => $a{toc});
   my $total_tracks = @{$a{toc}};
   my $frames	    = get_frames(toc=> $a{toc});

   return { discid => $discid,
	    disctime => $disctime,
	    disctracks => $total_tracks,
	    frames => $frames };
}

sub get_frames {
   my %a = @_;

   my @frames; 
   my $t = 0;
   foreach my $track ( @{$a{toc}} ) {
      push @frames, $t * 75;
      $t += ( $track->{MM} * 60 + $track->{SS} );
   }
   return \@frames;
}

sub disc_time {
   my %a = @_;
   my $total_time = 0;

   foreach my $track (@{$a{toc}}) {
        my $track_time = $track->{MM} * 60 + $track->{SS};
        $total_time +=           $track_time;
   }

   return $total_time;
}

sub disc_id {
   my %a = @_;
   my $n          = 0;
   my $total_time = 0;

   foreach my $track ( @{$a{toc}} ) {
        my $track_time = $track->{MM} * 60 + $track->{SS};

        $n          += cddb_sum($total_time);
        $total_time += $track_time;
    }

    return sprintf("%08x", ($n % 0xFF) << 24 | $total_time << 8 | @{$a{toc}});
}

sub cddb_sum {
    my ($n, $ret) = (shift, 0);
    for (split //, $n) { $ret += $_ }
    return $ret;
}

################ Command Line Options ################

use Getopt::Long 2.33;		# will enable help/version

sub app_options {

    GetOptions(ident	   => \&app_ident,
	       'verbose|v' => \$verbose,
	       # application specific options go here

	       "gracenote!" => \$gracenote,
	       "freecddb!"  => \$freecddb,

	       # development options
	       test	   => \$test,
	       trace	   => \$trace,
	       debug	   => \$debug)
      or Getopt::Long::HelpMessage(2);

    unless (scalar @ARGV) {
	Getopt::Long::HelpMessage(2);
    }
}

sub app_ident {
    print STDOUT ("This is $my_package [$my_name $my_version]\n");
}

__END__

=head1 NAME

mp3getcddb - fetch CDDB entry data for a directory of MP3 files

=head1 SYNOPSIS

mp3getcddb [options] directory [...]

Options:

   --[no]freecddb       [Do not] Use FreeCDDB
   --[no]gracenote      [Do not] Use CDDB
   --help, -h		This message
   --verbose, -v	Sets verbosity on
   --ident		show identification
   --help		brief help message
   --verbose		verbose information

=head1 OPTIONS

=over 8

=item B<-->[B<no>]B<freecddb>

Use, or do not use, the Free CDDB servers.
Default is to use them.

=item B<-->[B<no>]B<gracenote>

Use, or do not use, the commercial CDDB servers. Default is to only
use them if no CDDB data could be found on the free servers.

=item B<--verbose>

More verbose information.

=item B<--version>

Print a version identification to standard output and exits.

=item B<--help>

Print a brief help message to standard output and exits.

=item B<--ident>

Prints a program identification.

=item I<file>

Input file(s).

=back

=head1 DESCRIPTION

B<This program> will process one or more directories of MP3 files, and
tries to fetch CDDB entry data assuming each directory contains the
MP3 files for one album.

The files must be in the right album order when sorted.

The resultant CDDB entry data is written in the directory, under the
name(s) B<.cddb00>, B<.cddb01> and so on.

=head1 AUTHOR

Johan Vromans <jvromans@squirrel.nl>

=head1 COPYRIGHT

This programs is Copyright 2003, Squirrel Consultancy.

This program is free software; you can redistribute it and/or modify
it under the terms of the Perl Artistic License or the GNU General
Public License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.

=cut
