#!/usr/pkg/bin/perl
#
# choad
# 
# a command-line front end to cdparanoia and lame
#
# copyright 2001  pete gamache  gamache@ftso.org
#                       ,
# all rights reversed  (K)
#
# This program is free software; you can redistribute it
# and/or modify it under the same terms as Perl itself.
#
# 0.72:  format string parsing improvements and code cleanup
#        by Darrell Bishop
#
# 0.721: fixed typo in clean_up_string()
#
# 0.8:   now follows strict syntax; tweaked around with query_CD and 
#        get_CDDB_info(); fixed capitalization bug in parse_format_string()
#        added -submit option
# 0.81:  avoided lame --tg problems
# 0.82:  added "--" to cdparanoia command line in rip_track_with_name()
#        fixed exactly-one-CDDB-match ID3 bug 
# 0.822: added track numbering support for the kids

require 5;
use strict;

### constants #######################################################
my $true = 22;
my $false = 0;
my $choad = $0; $choad =~ s/.*\/([^\/]+)$/$1/;
my $choad_version = "0.822";
my $HOME = $ENV{HOME};
my $USER = $ENV{USER};
my $HOST = `hostname`; $HOST =~ s/\n//;
my $cdpflags_default = "";
my $lameflags_default = "-b 128";
my $trackname_default = '%a[30]/%l[30]/%NN - %t[22].mp3';


### defaults ########################################################
my $cdparanoia = `which cdparanoia`; $cdparanoia =~ s/\n//;
my $cdpflags = $cdpflags_default;
my $lame = `which lame`; $lame =~ s/\n//;
my $lameflags = $lameflags_default;

my $cddb_host = "freedb.freedb.org";
my $cddb_port = 888;

my $auto = $false;
my $nocddb = $false;
my $we_have_names = $false;  # if we have artist/title/track list 

my $maketracks = $true;
my $encode = $true;
my $submit = $false;         # new to 0.8


my $trackname = "";

my $strip_the = $true;

my $from = 1;
my $artist = "artist-$$";
my $album = "album-$$";
my $genre = "";
my @tracks = ();
my @onlyv = ();
my $how_many_tracks = 0;


### main routine ####################################################

parse_config_file ("/etc/choadrc");
parse_config_file ("$HOME/.choadrc");
parse_command_line (@ARGV);

my $MSF_info = query_CD();
get_CDDB_info() unless $nocddb == $true;

if ($we_have_names == $true) {
    print make_tracks_list();
    submit_CD() unless ($submit == $false);
} else {
    print "$how_many_tracks tracks on disc.\n";
}

$trackname = $trackname_default unless ($trackname ne "");

# now do the encoding
if ($encode == $true) {
    if (scalar @onlyv > 0) {
        while (<@onlyv>) {
            my $i = shift @onlyv;
            my $n = shift @onlyv;
            
            if ($n eq "auto") {$n = $trackname;}
            make_appropriate_dirs ($n);
            rip_track_with_name ($i, parse_format_string($n, $i));
        }
    } else {  #batch mode
        my $curdir = make_appropriate_dirs ($trackname);
        if ($maketracks == $true) {
            my $tracksfile = ">" . $curdir . "tracks";
            open TRACKS, $tracksfile or die $!;
            print TRACKS "# autogenerated by $choad $choad_version\n";
            print TRACKS make_tracks_list();
            close TRACKS;
        }

        for (my $i=$from; $i <= $how_many_tracks; $i++) {
            rip_track_with_name ($i, parse_format_string($trackname, $i));
        }
    }
} # and that's all

#######################################################################
#
# do-shit routines
#
#######################################################################

sub get_CDDB_info {
    use CDDB;

    $submit = $false;  # don't resubmit data

    my $cddb = new CDDB( Host  => $cddb_host,
                         Port  => $cddb_port,
                         Login => "$USER\@$HOST",
                         ) or die $!;

#    print $MSF_info;
    my @toc = split /\//, $MSF_info;

    my ($cddb_id, $track_numbers, $track_lengths, $track_offsets, 
        $total_seconds) = $cddb->calculate_id(@toc);

    my @discs = $cddb->get_discs($cddb_id, $track_offsets, $total_seconds);

    my $discchoice = 1; 
    my $i=0;
    if (scalar @discs > 1) {
        if ($auto == $false) {
            foreach my $disc (@discs) {
                $i++;
                ($genre, my $cddb_id, my $title) = @{$disc};
                print "$i  $title\n";
            }
          ASK: print "\nWhich disc? ";
            
            while ((my $c .= getc STDIN) ne "\n") {
                $discchoice = $c;
            }
            
            if ($discchoice > scalar @discs) {
                print "Too high; try again.";
                goto ASK;
            }
        }
        $we_have_names = $true;
    } elsif (scalar @discs == 0) {
        print STDERR "No CDDB entry for this disc (id=0x$cddb_id)\n";
	$we_have_names = $false;
    } else {
        $we_have_names = $true;
    }

  #  my ($genre_, $title);
    if (scalar @discs > 0) {
	($genre, my $cddb_id, my $title) = @{$discs[$discchoice-1]};
	$title=~/([^\/]*) \/ (.*)/; 
	$artist = $1; $album = $2;
    
    my $disc_info     = $cddb->get_disc_details($genre, $cddb_id);
    @tracks  = @{$disc_info->{ttitles}};
    }
}

sub submit_CD {
    # submits CD info to CDDB
    # use in conjunction with -tracks

    use CDDB;
#    use MailTools;
    
    my @toc = split /\t/, $MSF_info;

    my $cddb = new CDDB ( Host  => $cddb_host,
			  Port  => $cddb_port,
			  Login => "$USER\@$HOST",
			  );

    if (scalar @tracks != $how_many_tracks) {
	print STDERR
	    "Track list has too many or too few songs.  Not submitting.\n";
	return;
    }

    my ($cddb_id,       # used for further CDDB queries
	$track_numbers, # padded with 0's provided as a convenience
	$track_lengths, # length of each track, in MM:SS format
	$track_offsets, # absolute offsets (used for further CDDB queries)
	$total_seconds  # total play time, in MM:SS format
	) = $cddb->calculate_id(@toc);
    
    my $YN = "n";
ASK:
    print "Are you sure the above info is correct? (y/N) ";
    while ((my $c .= getc STDIN) ne "\n") {
	$YN = $c;
    };
    if (uc $YN eq "N") {
	print "\nOkay, not submitting.\n";
    } elsif (uc $YN ne "Y") {
	print "\n\"$YN\" not understood.  Try again.\n";
	goto ASK;
    }

    $cddb->submit_disc
	( Genre       => $genre,
	  Id          => $cddb_id,
	  Artist      => $artist,
	  DiscTitle   => $album,
	  Offsets     => $track_offsets,
	  TrackTitles => \@tracks,
	  From        => "$USER\@$HOST"
	  );
}

sub make_appropriate_dirs {
# takes a format string as input
# returns directory name

    my $fmtstring = $_[0];
    my $dir = parse_format_string ($fmtstring, 1); 
    if (! ($dir=~/\//)) {return "";}  #no dirs to create
    $dir =~s/(.*)\/[^\/]+$/$1/; #strip mp3 name from end
    my @dirs = split /\//, $dir;
    my $curdir = "";
    foreach $dir (@dirs) {
        if (! $dir=~/^$/) {
            `mkdir "$curdir$dir"`;   # if $dir's already there, no harm done
        }
        $curdir .= $dir . "/";
    }
    return $curdir;
}

sub rip_track_with_name {
    my $tracknumber = $_[0];
    my $mp3name = $_[1];
    if ($mp3name eq "") {return};
    
    my $lameoptions = $lameflags;
    if ($we_have_names == $true) {
        $lameoptions .= 
            " --tt \"$tracks[$tracknumber-1]\" --ta \"$artist\" --tl \"$album\" --tn $tracknumber";
# why i left that "--tn $tracknumber" out in the first damn place, i dunno.

    }

    my $command = 
     "$cdparanoia $cdpflags -- $tracknumber - | $lame $lameoptions - \"$mp3name\"";

    print STDERR "$command\n";
    `$command`;
}

#######################################################################
#
# file parsing routines
#
#######################################################################

sub parse_config_file {
    my $CHOADRC = $_[0];
    my $i=0;
    open RC, $CHOADRC or return;
    while (<RC>) {
        $i++;
        if (! (/^[\#;]/ || /^\s*$/)) {    # ignore comments and blank lines
            /^\s*(\S*)\s*=?\s*(.*)\s*$/;  # grab useful bits from line
            my @line = ("-$1", $2);
            parse_command_line (@line);
        }
    }
}

sub parse_tracks_file {
    my $tracksfile = $_[0];
    
    open TRACKS, $tracksfile or die "Could not open $tracksfile: $!\n";
    while (<TRACKS>) {
        my $j; $j++;
        if (! (/^[\#;]/ || /^\s*$/)) {     # ignore comments and blank lines
            if (/^artist\s*(.*)\s*$/) {
                $artist = clean_up_string($1);
            }
            elsif (/^title\s*(.*)\s*$/) {
                $album = clean_up_string($1);
            }
	    elsif (/^genre\s*(.*)\s*$/) {
                $genre = clean_up_string($1);
            }
            elsif (/^track\s*([0-9]+)\s(.*)\s*$/) {
                $tracks[$1-1] = $2;
            }
            else {
                print STDERR "\"$tracksfile\", line $j ignored:\n$_\n";
            }
        }
    }
    close TRACKS;
    $how_many_tracks = scalar @tracks;
    $we_have_names = $true;
}

#######################################################################
#
# command-line parsing routine
#
#######################################################################

sub parse_command_line {
    while (@_) {
        if ($_[0] eq "-cdp") {
            shift; $cdparanoia = shift;
        }
        elsif ($_[0] eq "-cdpflags") {
            shift; $cdpflags = shift;
        }
        elsif ($_[0] eq "-lame") {
            shift; $lame = shift;
        }
        elsif ($_[0] eq "-lameflags") {
            shift; $lameflags = shift;
        }
        elsif ($_[0] eq "-choadrc") {
            shift; parse_config_file (shift);
        }
        elsif ($_[0] eq "-server") {
            shift; $cddb_host = shift;
        }
        elsif ($_[0] eq "-port") {
            shift; $cddb_port = shift;
        }
        elsif ($_[0] eq "-auto") {
            shift; $auto = $true;
        }
        elsif ($_[0] eq "-nocddb") {
            shift; $nocddb = $true;
        }
        elsif ($_[0] eq "-tracks") {
            shift; $nocddb = $true; parse_tracks_file (shift);
        }
        elsif ($_[0] eq "-trackname") {
            shift; $trackname = shift;
        }
        elsif ($_[0] eq "-nomaketracks") {
            shift; $maketracks = $false;
        }
        elsif ($_[0] eq "-noencode") {
            shift; $encode = $false;
        }
        elsif ($_[0] eq "-submit") {
            shift; $submit = $true;
        }
        elsif ($_[0] eq "-the") {
            shift; $strip_the = $false;
        }
        elsif ($_[0] eq "-from") {
            shift; $from = shift;
        }
        elsif ($_[0] eq "-h") {
            print usage(); exit 0;
        }
        elsif ($_[0] eq "-V") {
            print version(); exit 0;
        }
        elsif ($_[0] eq "-longhelp") {
            print longhelp(); exit 0;
        } 
        elsif ($_[0] eq "-only") {
            shift;  while (@_) {@onlyv = (@onlyv, shift);}
        }
        else {
            print STDERR "Unrecognized option: $_[0]\n";
            print STDERR usage(); exit 1;
        }
    }
}

#######################################################################
# 
# string-twiddling routines
#
#######################################################################

sub parse_format_string {
    my $format_string = $_[0];
    my $tracknumber = $_[1];  # only used for %t

    ###Darrell Bishop cleaned this part up a LOT -- thanks!
    # Expand format variables in-place.
    my $i = 0;
    while ($i < length($format_string)) {
        my $char = substr($format_string, $i, 1);
        my $width = 2;  # at least '%' and a letter
        if ($char eq "%") {
            # We hit a variable; expand it
            #  (forward look-ahead sanity check)
            my $type = substr($format_string, $i+1, 1) || "";
            my $replacement_text;
            if (uc $type eq "A") {
                $replacement_text = ($type eq "a") ? lc $artist : $artist;
            } elsif (uc $type eq "L") {
                $replacement_text = ($type eq "l") ? lc $album : $album;
            } elsif (uc $type eq "T") {
                $replacement_text = scalar @tracks ? ($type eq "t")? 
		    lc $tracks[$tracknumber-1] : $tracks[$tracknumber-1]
                    : sprintf("track %02d", $tracknumber);
            } elsif (uc $type eq "N") {
                my $tn_format_string = "%d";
                if (uc substr($format_string, $i+2, 1) eq "N") {
                    $tn_format_string = "%02d";
                    $width++;
                }
                $replacement_text = sprintf($tn_format_string, $tracknumber);
            } elsif (uc $type eq "P") {
                $replacement_text = $$;
            } else {
                # Wasn't a format variable after all... *sigh*
                $i++;
                next;
            }
            if ($i+$width < length $format_string and
                    substr($format_string, $i+$width, 1) eq "_") {
                $replacement_text =~ s/\s/_/g;
                $width++;
            }
            if ($i+$width < length $format_string and
                    substr($format_string, $i+$width, 1) eq "[") {
                my ($j, $trunc) = ($i+$width+1, "");
                while ($j < length $format_string and
                        substr($format_string, $j, 1) =~ /\d/) {
                    $trunc .= substr($format_string, $j, 1);
                    $j++;
                }
                if ($trunc =~ /^\d+$/o and $j < length $format_string and
                        substr($format_string, $j, 1) eq "]") {
                    $replacement_text = substr($replacement_text, 0, $trunc);
                    $width += length($trunc) + 2;
                }
            }
            # error fixed here
	    $replacement_text = clean_up_string($replacement_text);
            substr($format_string, $i, $width) = $replacement_text;
            $i += length $replacement_text;
        } else {
            $i++;
        }
    }
    return $format_string;
}

sub clean_up_string {
    # removes: trailing whitespace, unfinished parenthetical phrases
    # replaces colons, slashes with "-"
    my $string = $_[0];

    $string=~s/\([^\)]*$//;
    $string=~s/\s*$//;
    $string=~s/[:\/\\]/-/g;

    return $string;
}

#######################################################################
#
# string-building routines
#
#######################################################################

sub make_tracks_list {
    my $list = "artist $artist\ntitle $album\n";
    $list .= ($genre eq "") ? "\n" : "genre $genre\n\n";
    my $i=1;
    foreach my $track (@tracks) {
        $list .= sprintf "track %d %s\n", $i, $tracks[$i-1];
        $i++;
    }
    return $list;
}

sub query_CD {
    # returns tab-delimited string list of:
    # <track offset-minutes offset-seconds offset-frames>
    # also sets $how_many_tracks

    my $toc = "";
    my $command = "$cdparanoia $cdpflags -Q 2>/tmp/choad-$$"; 

    `$command`;
    open QUERY, "/tmp/choad-$$" or die $!;
    while (<QUERY>) {
	print $_;
        if (/\s*([0-9]+)\.\s+[0-9]+\s+\[[0-9]+:[0-9]+\.[0-9]+\]\s+[0-9]+\s+\[([0-9]+):([0-9]+)\.([0-9]+)\]/) {  # that's a mouthful
            $toc .= "$1 $2 $3 $4/";
            $how_many_tracks = $1;
        }
        elsif (/^TOTAL\s+[0-9]+\s+\[([0-9]+):([0-9]+)\.([0-9]+)\]/) {
            $toc .= "999 $1 $2 $3";  # lead-out track
        }
    }
    close QUERY; #print "made it past query\n";
    `/bin/rm /tmp/choad-$$`;

    return $toc;
}

sub version {
return sprintf <<END
$choad version $choad_version
copyright 2001  pete gamache  gamache\@ftso.org
http://www.ftso.org/choad/
END
    ;
}

sub usage {
    return sprintf  <<END
usage: $choad [options] [-only n1 file1 n2 file2 ...]

options:
  -choadrc <filename>        read <filename> as choadrc configuration file
  -cdp /path/to/cdparanoia   specify path to cdparanoia
                             [default=$cdparanoia]
  -cdpflags "flags"          pass "flags" to cdparanoia
                             [default="$cdpflags_default"]
  -lame /path/to/lame        specify path to lame
                             [default=$lame]
  -lameflags "flags"         pass "flags" to lame
                             [default="$lameflags_default"]
  -server <host>             use <host> as CDDB server
                             [default=$cddb_host]
  -port <port>               use <port> as CDDB server port
                             [default=$cddb_port]
  -nocddb                    do not attempt CDDB lookup
  -tracks <filename>         use <filename> for artist/title/track list
                             instead of CDDB lookup (implies -nocddb)
  -nomaketracks              do not make "tracks" file when encoding CD
  -submit                    submit track list to CDDB (use with -tracks)
  -noencode                  do not encode CD
  -auto                      ask user no questions (Do The Right Thing)
  -trackname "string"        use "string" as track name format string
                             [default="$trackname_default"]  
  -the                       do not strip leading "the" from artist name
  -from <n>                  start encoding at track <n>
  -h                         print help message
  -V                         print version
  -longhelp                  print long help message

If "-only <n> <file> ..." is present, $choad will encode each track <n>,
using <file> as the format string for the filename.  If <file> == "auto",
then the default format string is used.

If "-only ..." is not present, $choad will encode the entire CD.

END
    ;
}

sub longhelp {
    my $usage = usage();
    my $version_string = version();

    return sprintf <<END
$version_string
$usage
submit:
  In order to submit CDDB data, you must have the MailTools and libnet 
  collections of Perl modules installed.  Additionally, you should set 
  the SMTPHOSTS enviromnent variable to your outgoing mail host, like:

   bash: \$ SMTPHOSTS=smtp.mydomain.com choad
   csh:  \% setenv SMTPHOSTS smtp.mydomain.com; choad

choadrc configuration files:
  $choad processes the files /etc/choadrc and ~/.choadrc at startup.  You
  may use the "-choadrc <file>" command-line option to specify other file(s)
  to process.  Commands in this file correspond directly to command line
  options; lines in choadrc files of the form "option" or "option = value"
  are equivalent to command line options "-option" and "-option value",
  respectively.  These commands are processed as if they were inserted onto
  the command line, either before the other options (in the case of 
  /etc/choadrc and ~/.choadrc) or in the place where "-choadrc <file>"
  occurs.  Later options override earlier options on the command line.

  Blank lines are ok, and comment lines must start with ";" or "#".

Format strings:
  You can customize the destination of the mp3 files (directory and filenames)
  if you like.  $choad provides the following variables and options:

  \%A    artist name with leading "the" removed (use "-the" command-line
         option to prevent this)
  \%L    album name
  \%T    track name
  \%N    track number
  \%NN   two-digit track number
  \%P    $choad process ID (use for uniqueness)

  In \%A, \%L, and \%T, the characters "/", "\\", and ":" are changed to "-"
  to prevent confusion with directory paths on Unix, Windows, and Mac
  systems.

  You may place "_" after a variable name ("\%A_") to replace all spaces
  with "_" characters.

  You may place "[n]" after a variable name (and optional "_") to truncate
  at n characters.

  You may use the lowercase form of the variable name to force the output
  to lowercase.

  For instance, given a song named "Unsatisfied" by The Replacements
  (track 7 on album "Let It Be"): 

  Format string "%A[30]/%l_[30]/%NN - %t_[10].mp3" yields
  "Replacements/let_it_be/07 - unsatisfie.mp3".  

  If the format string refers to directories that don't exist, $choad will 
  try to create them.

"tracks" file:
  Unless the "-nomaketracks" or "-only" option is given, $choad will create
  a file called "tracks" in the same directory as the mp3s, of the form:

artist Some Cool Band
title Album Title

track 1 First Song Title
track 2 Second Song Title

  ...and so on.

  If the "-tracks <file>" option is given, $choad will read the specified
  file in the above format to specify the artist, title, and track names
  instead of performing a CDDB lookup.

END
    ;
}
