#!/usr/local/bin/perl
#
#   Copyright: GNU Public License 2 applies
#
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2, or (at your option)
#   any later version.
#
#   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.
#
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
#
# ---
#
# rebot3 - Copyright (c) 2000,2001 Thomas Wei <panos@bigfoot.de>
#
# Version 0.9.1: - initial release
# Version 1.0:   - added remote cddb support
# Version 1.0.1: - bugfix release
# Version 1.0.2: - applied a patch by Marcel Hild that introduces the -g
#                  option
#                - added -G option, that prints the list of genres known
#                  by the cddb server
#                - made the default cddb paths ~/.cddb and ~/cddb work.
#                - rebot3 now uses MP3::ID3v1Tag instead of MPEG::...
#                  since the author of this module has dropped this
#                  compatibility wrapper in the current 1.11 version
#
# Use at your own risk!
#
# Thanks go to Sander van Zoest for MP3::ID3v1Tag, Rocco Caputo for the
# CDDB module, the authors of cdda2wav and of course to Larry Wall and all
# the other authors of Perl!
#
# Special thanks go to Marcel Hild, for providing a patch that introduces
# the -g option and Arcady Stepanov for providing a small bugfix concerning
# the default values of @CDDB_PATH

use MP3::ID3v1Tag;
use CDDB;

$VERSION = '1.0.2'; 

$CDROM_MSF         = 0x02;
$CDROM_LEADOUT     = 0xAA;
$CDROMREADTOCHDR   = 0x5305;
$CDROMREADTOCENTRY = 0x5306;

$HOME           = ($ENV{HOME} || $ENV{LOGDIR} || '/etc');
$KDE_DIR        = ($ENV{KDEDIR} || '/opt/kde');
$DEBUG          = 0;
$UNDO           = 0;
$UNDO_ONLY      = 0;
$UNDO_NAME      = 'undo';
@CDDB_PATHS     = ("$HOME/.cddb", "$HOME/cddb",
                   "$KDE_DIR/share/apps/kscd/cddb",
                   "/usr/X11R6/lib/X11/xmcd/cddb");
$SILENT         = 0;
$SORT_FILE_LIST = 1;
$ID_FILE        = 'audio.cddb';
$USE_CDDA2WAV   = 0;
$USE_CD         = 0;
$CDROM_DEVICE   = '/dev/cdrom';
$PATTERN        = '%b - %n - %t';
# %%:            %
# %a, %{artist}: Artist
# %b, %{album}:  Album
# %n, %{number}: Track number
# %t, %{title}:  Track title
$APPEND_ID3     = 0;
$ID3_ONLY       = 0;
$REMOVE_ID3     = 0;
$XMMS_ONLY      = 0;
$XMMS_CDINFO    = $HOME . '/.xmms/cdinfo';
$REMOTE_CDDB    = 0;
$HAVE_GENRE     = 0;
$PRINT_GENRES   = 0;
$GENRE_FILE     = $HOME . '/.cddb-genres';
$CDDB_SERVER    = 'freedb.freedb.org';
$CDDB_PORT      = 8880;
$NUM_COUNT      = 0;

if ((defined $ENV{REBOT_CDDB_PATHS}) && ($ENV{REBOT_CDDB_PATHS} ne '')) {
    undef @CDDB_PATHS;
    @CDDB_PATHS = split /:/, $ENV{REBOT_CDDB_PATHS};
}

if ((defined $ENV{REBOT_PATTERN}) && ($ENV{REBOT_PATTERN} ne '')) {
    $PATTERN = $ENV{REBOT_PATTERN};
}

if ((defined $ENV{REBOT_CDDB_SERVER}) && ($ENV{REBOT_CDDB_SERVER} ne '')) {
    if ($ENV{REBOT_CDDB_SERVER} =~ /^(.+):(\d+)$/) {
        $TMP_CDDB_SERVER = $1;
        $TMP_CDDB_PORT   = $2;
    }
    else {
        $TMP_CDDB_SERVER = $ENV{REBOT_CDDB_SERVER};
    }

    if ($TMP_CDDB_SERVER =~ /:/) {
        $TMP_CDDB_SERVER = $CDDB_SERVER;
        $TMP_CDDB_PORT   = $CDDB_PORT;
    }

    $CDDB_SERVER = $TMP_CDDB_SERVER;
    $CDDB_PORT   = $TMP_CDDB_PORT;
}

if (@ARGV) {
    for (my $i = 0; $i <= $#ARGV; $i++) {
        $_ = $ARGV[$i];
        
        if (/^(-r|--remote-cddb)$/) {
            $REMOTE_CDDB = 1;
        }
        elsif (/^(-cs|--cddb-server)$/) {
            $i++;
            &help if ($i > $#ARGV);
            if ($ARGV[$i] =~ /^(.+):(\d+)$/) {
                $CDDB_SERVER = $1;
                $CDDB_PORT   = $2;
            }
            else {
                $CDDB_SERVER = $ARGV[$i];
            }
            die "Error: Invalid port for cddb server specified\n"
              if ($CDDB_SERVER =~ /:/);
        }
        elsif (/^(-cd|--cdrom-device)$/) {
            $i++;
            &help if ($i > $#ARGV);
            $CDROM_DEVICE = $ARGV[$i];
        }
        elsif (/^(-id|--disc-id)$/) {
            $i++;
            if ($i > $#ARGV) { &help; }
            $disc_id = $ARGV[$i];
        }
        elsif (/^(-if|--id-file)$/) {
            $i++;
            &help if ($i > $#ARGV);
            $ID_FILE = $ARGV[$i]; 
        }
        elsif (/^(-c|--use-cdda2wav)$/) {
            $USE_CDDA2WAV = 1;
        }
        elsif (/^(-C|--use-cd)$/) {
            $USE_CD = 1;
        }
        elsif (/^(-d|--debug)$/) {
            $DEBUG = 1;
        }
        elsif (/^(-db|--cddb-path)$/) {
            $i++;
            if ($i > $#ARGV) { &help; }
            undef @CDDB_PATHS;
            push @CDDB_PATHS, $ARGV[$i];
        }
        elsif (/^(-u|--undo)$/) {
            $UNDO = 1;
            if ((defined $ARGV[$i + 1]) && ($ARGV[$i + 1] =~ /^[^-]/)) {
                $i++;
                $UNDO_NAME = $ARGV[$i];
            } 
        }
        elsif (/^(-uo|--undo-only)$/) {
            $UNDO = 1;
            $UNDO_ONLY = 1;
            if ((defined $ARGV[$i + 1]) && ($ARGV[$i + 1] =~ /^[^-]/)) {
                $i++;
                $UNDO_NAME = $ARGV[$i];
            }
        }
        elsif (/^(-s|--silent|-q|--quiet)$/) {
            $SILENT = 1;
        }
        elsif (/^(-f|--files?)$/) {
            if ($i == $#ARGV) { &help; }
            push @mp3files, @ARGV[++$i..$#ARGV];
            $i = $#ARGV;
        }
        elsif (/^(-S|--dont-sort)$/) {
            $SORT_FILE_LIST = 0;
        }
        elsif (/^(-l|--track-list)$/) {
            $i++;
            if ($i > $#ARGV) { &help; }
            @tracks = split /[,:]/, $ARGV[$i];
            foreach my $track (@tracks) {
                die "Error: Invalid track specified with -l\n"
                  if (!($track =~ /^\d{1,2}$/));
            }
            $SORT_FILE_LIST = 0; 
        }
        elsif (/^(-n|--use-number)$/) {
            $i++;
            if ($i > $#ARGV) { &help; }
            $NUM_COUNT = $ARGV[$i];
            die "Error: Invalid number specified with -n\n"
              if ($NUM_COUNT =~ /\D/) || ($NUM_COUNT < 1);
            $NUM_COUNT--;
        }
        elsif (/^(-p|--pattern)$/) {
            $i++;
            &help if ($i > $#ARGV);
            $PATTERN = $ARGV[$i];
            die "Error: Invalid pattern specified\n"
              if (!($PATTERN =~ /%[nt]/)); 
        }
        elsif (/^(-t|--append-id3-tags?)$/) {
            $APPEND_ID3 = 1;
        } 
        elsif (/^(-to|--id3-tags?-only)$/) {
            $APPEND_ID3 = 1; 
            $ID3_ONLY = 1;
        }
        elsif (/^(-T|--remove-tags?)$/) {
            $REMOVE_ID3 = 1;
        }
        elsif (/^(-x|--xmms-cdinfo-file)$/) {
            $i++;
            &help if ($i > $#ARGV);
            $XMMS_CDINFO = $ARGV[$i];
            $XMMS_CDINFO =~
              s{^~([^/]*)}{$1 ? (getpwnam($1))[7] : ( $ENV{HOME} || $ENV{LOGDIR} )}ex;
        }
        elsif (/^(-xo|--xmms-cdinfo-only)$/) {
            $XMMS_ONLY = 1;
        }
        elsif (/^(-h|--help)$/) {
            &help;
        }
        elsif (/^(-g|--genre)$/) {
            $HAVE_GENRE = 1;
            $i++;
            &help if ($i > $#ARGV);
            $disc_genre = $ARGV[$i];
        }
        elsif (/^(-G|--print-genres)$/) {
            $PRINT_GENRES = 1;
        }
        else {
            &help;
        }
    }
}

die <<EOT
Error: You have specified the disc id to use in remote cddb mode but didn't
 give the album's genre. In remote cddb mode the disc id is useful only if
 the album's genre is specified. So please use local cddb mode (omit the
 -r command line switch) or specify the album's genre (with the -g option)
 or omit the disc id and let me calculate the disc id from the audio cd in
 your cdrom drive, if there is one.
EOT
  if $REMOTE_CDDB && defined $disc_id && !$HAVE_GENRE;

if ($PRINT_GENRES) {
    $| = 1;
    
    if ($REMOTE_CDDB) {
        my ($cddb, @genres);
        die "Error: Couldn't connect cddb server: $!\n"
          unless ($cddb = new CDDB(Host => $CDDB_SERVER,
                                   Port => $CDDB_PORT,
                                   Login => 'rebot3'));

        die "Error: Couldn't fetch genre list from cddb server.\n"
          unless @genres = $cddb->get_genres();

        @genres = sort { lc($a) cmp lc($b) } @genres;

        foreach (@genres) {
            print "$_\n";
        }

        $mode = -e $GENRE_FILE ? 'update' : 'create';
        open OUT, "> $GENRE_FILE"
          or die "Error: Couldn't $mode genre file: $!\n";
        
        foreach (@genres) {
            print OUT "$_\n";
        }

        close IN;
    }
    else {
        open IN, "< $GENRE_FILE" or
          die "Error: Couldn't open genre file: $!\n" .
            "Perhaps you should first use remote cddb mode to fetch the " .
              "genre list\nfrom a cddb server.\n";

        my @genres = ();
        while (<IN>) {
            chomp;
            next if /^\s*$/;
            push @genres, $_;
        }

        close IN;

        @genres = sort { lc($a) cmp lc($b) } @genres;

        die "Error: Genre file is empty. You can fill it using " .
          "remote cddb mode.\n" if @genres == 0;

        foreach (@genres) {
            print "$_\n";
        }
    }

    exit 0;
}

if ((!$REMOVE_ID3) && (!$REMOTE_CDDB)) {
    if ($USE_CDDA2WAV) {
        my $output = `cdda2wav -D $CDROM_DEVICE -v 1 -J 2>&1`;
        ($disc_id) = $output =~ /CDDB discid: 0x(.*)$/;
        die "Error: Couldn't fetch disc id from cdda2wav\n"
          if (!defined $disc_id);
    }
    elsif ($USE_CD) {
        my $cdrom_tochdr = '';
        my $cdrom_tocentry;
        my @cdrom_toc;
    
        my $start_track;
        my $end_track;
    
        open CDROM, $CDROM_DEVICE
          or die "Error: Couldn't open device: $CDROM_DEVICE: $!\n";
    
        ioctl CDROM, $CDROMREADTOCHDR, $cdrom_tochdr
          or die "Error: Couldn't read TOC header: $!\n";
        ($start_track, $end_track) = unpack 'CC', $cdrom_tochdr;
    
        for (my $track = $start_track; $track <= $end_track; $track++) {
            $cdrom_tocentry = pack 'CCC', $track, 0, $CDROM_MSF;
        
            ioctl CDROM, $CDROMREADTOCENTRY, $cdrom_tocentry or
              die "Error: Couldn't read TOC entry #$track: $!\n";
        
            my @cdte = unpack 'CCCCCCC', $cdrom_tocentry;
        
            push @cdrom_toc, "$track $cdte[4] $cdte[5] $cdte[6]";
        }
    
        $cdrom_tocentry = pack 'CCC', $CDROM_LEADOUT, 0, $CDROM_MSF;
        
        ioctl CDROM, $CDROMREADTOCENTRY, $cdrom_tocentry or
          die "Error: Couldn't read Leadout TOC entry: $!\n";
        
        my @cdte = unpack 'CCCCCCC', $cdrom_tocentry;
        
        push @cdrom_toc, "999 $cdte[4] $cdte[5] $cdte[6]";
        close CDROM;

        my $cddb = new CDDB(Host  => $CDDB_SERVER,
                            Port  => $CDDB_PORT,
                            Login => 'rebot3')
          or die "Error: Couldn't calculate disc id: $!\n";

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

        $disc_id = $cddb_id;
    }
    elsif (!defined $disc_id) {
        die "Error: File $ID_FILE does not exist\n" if (!-e $ID_FILE);
        
        open IN, $ID_FILE or die "Error: Couldn't open file $ID_FILE: $!\n";
        while (<IN>) {
            s/\r//g;
            if (/^DISCID=/) {
                ($disc_id) = /^DISCID=(.+)$/;
                last;
            }
        }
        close IN;
        die "Error: No disc id found in file $ID_FILE\n"
          if (!defined $disc_id);
    }
}

unless (@mp3files) {
    opendir DIR, '.' or die "Error: Couldn't open current dir: $!\n";
    @dir = readdir DIR;
    closedir DIR;
    
    foreach (@dir) {
        next if (/^\./);
        push @mp3files, $_ if (/\d{1,2}.*\.mp3$/);
    }
    die "Error: No mp3's found in current dir" unless @mp3files;
}

if ($SORT_FILE_LIST) {
    foreach (@mp3files) {
        my $num;

        if ($NUM_COUNT == 0) {
            ($num) = /^\D*(\d{1,2}).*\..*?/;
        }
        else {
            /^\D*(\d+\D+){$NUM_COUNT}(\d{1,2}).*\..*?/;
            $num = $2;
        }

        die "Error: Couldn't determine the track number of file $_.\n".
            "       Use -l to specify it manually\n" if !defined $num;
    }

    if ($NUM_COUNT == 0) {
        @mp3files =
          sort {($a =~ /(\d{1,2})/)[0] <=> ($b =~ /(\d{1,2})/)[0]} @mp3files;
    }
    else {
        @mp3files =
          sort {($a =~ /^\D*(\d+\D+){$NUM_COUNT}(\d{1,2})/)[1] <=>
                  ($b =~ /^\D*(\d+\D+){$NUM_COUNT}(\d{1,2})/)[1]} @mp3files;
    }
}

if ($REMOVE_ID3) {
    foreach (@mp3files) {
        my $mp3 = new MP3::ID3v1Tag($_);
        if ($mp3->got_tag()) {
            $mp3->remove_tag();
        }
        else {
            warn "Warning: $_ doesn't contain id3 tag\n";
        } 
    }
    exit 0; 
}

die "Error: Number of mp3 files and tracks (specified with -t) does " .
  "not match\n" if (@tracks && ($#tracks != $#mp3files));

if (!$REMOTE_CDDB) {
    if ((-e $disc_id) && (-r $disc_id)) {
        $cddb_file = $disc_id;
    }
    elsif (!$XMMS_ONLY) {
        foreach (@CDDB_PATHS){
            &list_cddb_files($_) if (-e && -d && -r && -x);
        }

        if (@cddb_file_list) {
            foreach (@cddb_file_list) {
                $cddb_file = $_ if (/$disc_id$/);
            }
        }
    }

    if (!defined $cddb_file) {
        open IN, $XMMS_CDINFO
          or die "Error: Couldn't open file $XMMS_CDINFO: $!\n";
        my $found = 0;
        while (<IN>) {
            if (!$found) {
                if (/^\[/) {
                    my ($id) = /^\[(\S{8})\]$/;
                    if ((defined $id) && ($id eq $disc_id)) {
                        $found = 1;
                        next;
                    }
                }
            }
            else {
                last if (/^$/);
                push @disc_info, $_;
            }
        }
        close IN;
    }

    if (defined $cddb_file) {
        $album = '';
        $first = 1;
        $last_track = -1;
        open IN, $cddb_file or die "Error: Couldn't open CDDB file: $!\n";
        while (<IN>) {
            if (/^TTITLE/) {
                chomp;
                s/\r//g;
                ($new_track, $name) = /^TTITLE(\d{1,2})=(.+)$/;
                if ((defined $new_track) && (defined $name)) {
                    #$name =~ s/[\*\?\/\\\:]/-/g;
                    if ($new_track == $last_track) {
                        $track_titles[$#track_titles] .= $name;
                    }
                    else {
                        push @track_titles, $name;
                    }
                    $last_track = $new_track;
                    undef $new_track;
                    undef $name;
                }
            }
            elsif (/^DTITLE/) {
                chomp;
                s/\r//g;
                if ($first) {
                    ($artist, $dt) = /^DTITLE=(.+?) [\/|-] (.+)$/;
                    $first = 0;
                }
                else {
                    ($dt) = /^DTITLE=(.+)$/;
                }
                if (defined $dt) {
                    #$dt =~ s/[\*\?\/\\\:]/-/g;
                    $album .= $dt;
                    undef $dt;
                }
            }
            elsif (/^DGENRE/) {
                chomp;
                s/\r//g;
                ($genre) = /^DGENRE=(.+)$/;
            }
        }
        close IN;
        die "Error: CDDB file format error\n"
          if ((!@track_titles) || ($album eq ""));
    }
    elsif (@disc_info) {
        die "Error: Xmms cdinfo has wrong format\n"
          if (($#disc_info < 2) || (!($disc_info[0] =~ /^Albumname=.+/))
              || (!($disc_info[1] =~ /^Artistname=.+/)));
        ($album)  = shift(@disc_info) =~ /^Albumname=(.+)$/;
        ($artist) = shift(@disc_info) =~ /^Artistname=(.+)$/;
        foreach (@disc_info) {
            my ($title) = /=(.+)$/;
            push @track_titles, $title;
        }
        undef @disc_info;
    }
    else {
        die "Error: No matching CDDB file/entry found\n";
    }
}
else {
    my @cdrom_toc;

    unless ($HAVE_GENRE) {
        my $cdrom_tochdr = '';
        my $cdrom_tocentry;
        
        my $start_track;
        my $end_track;
        
        open CDROM, $CDROM_DEVICE
          or die "Error: Couldn't open device: $CDROM_DEVICE: $!\n";
    
        ioctl CDROM, $CDROMREADTOCHDR, $cdrom_tochdr
          or die "Error: Couldn't read TOC header: $!\n";
        ($start_track, $end_track) = unpack 'CC', $cdrom_tochdr;
    
        for (my $track = $start_track; $track <= $end_track; $track++) {
            $cdrom_tocentry = pack 'CCC', $track, 0, $CDROM_MSF;
        
            ioctl CDROM, $CDROMREADTOCENTRY, $cdrom_tocentry or
              die "Error: Couldn't read TOC entry #$track: $!\n";
        
            my @cdte = unpack 'CCCCCCC', $cdrom_tocentry;
            
            push @cdrom_toc, "$track $cdte[4] $cdte[5] $cdte[6]";
        }
    
        $cdrom_tocentry = pack 'CCC', $CDROM_LEADOUT, 0, $CDROM_MSF;
        
        ioctl CDROM, $CDROMREADTOCENTRY, $cdrom_tocentry or
          die "Error: Couldn't read Leadout TOC entry: $!\n";
        
        my @cdte = unpack 'CCCCCCC', $cdrom_tocentry;
        
        push @cdrom_toc, "999 $cdte[4] $cdte[5] $cdte[6]";
        close CDROM;
    }

    print "Connecting to $CDDB_SERVER:$CDDB_PORT...\n" unless $SILENT;
    
    my $cddb = new CDDB(Host => $CDDB_SERVER,
                        Port => $CDDB_PORT,
                        Login => 'rebot3')
      or die "Error: Couldn't connect to cddb server: $!\n";

    my ($cddb_id, $track_numbers, $track_lengths,
        $track_offsets, $total_seconds);

    if ($HAVE_GENRE) {
        $cddb_id = $disc_id;
        $genre   = $disc_genre;
    }
    else {
        ($cddb_id,
         $track_numbers,
         $track_lengths,
         $track_offsets,
         $total_seconds) = $cddb->calculate_id(@cdrom_toc);

        print "Retrieving disc information...\n" if (!$SILENT);

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

        if (@discs) {
            print "Discs found: " . ($#discs + 1) . "\n" if (!$SILENT);
        }
        else {
            die "No matching discs found.\n";
        }

        my $disc_number = 1;

        if ($#discs > 0) {
            my $counter = 0;
            foreach my $disc (@discs) {
                $counter++;
                my (undef, undef, $title) = @$disc;
                print "$counter: $title\n";
            }

            do {
                print "Enter the number of the correct disc: ";
                $disc_number = <STDIN>;
                $disc_number = -1 if ($disc_number !~ /^\d+$/);
            } until ((($disc_number - 1) >= 0)
                     && (($disc_number - 1) <= $#discs));
        }

        my $title;
        ($genre, $cddb_id, $title) = @{$discs[$disc_number - 1]};
    }

    my $disc_info;

    die "Error: Couldn't fetch disc info\n"
      unless ($disc_info = $cddb->get_disc_details($genre, $cddb_id));

    my $disc_title = $disc_info->{dtitle};
    @track_titles  = @{$disc_info->{ttitles}};

    ($artist, $album) = $disc_title =~ /^(.+?) \/ (.+)$/;
    die "No matching disc found.\n" unless ($disc_title ne '');
}

warn "Warning: Number of audio files and track titles do not match\n"
  if ($#mp3files != $#track_titles);

foreach (@tracks) {
    die "Error: Invalid track number specified with -t\n"
      if ($_ > $#track_titles + 1);
}

my $count_renamed = 0;
for (my $i = 0; $i < @mp3files; $i++) {
    my $num;
    unless (@tracks) {
        if ($NUM_COUNT == 0) {
            ($num) = $mp3files[$i] =~ /^\D*(\d{1,2}).*\..*?/;
        }
        else {
            $mp3files[$i] =~ /^\D*(\d+\D+){$NUM_COUNT}(\d{1,2}).*\..*?/;
            $num = $2;
        }
    }
    else {
        $num = $tracks[$i];
    }

    die("Error: Could not determine the track number of $mp3files[$i]. " .
        "Set it manually with the -l option\n")
      unless defined $num;

    unless (defined $track_titles[$num - 1]) {
        warn("Warning: No title defined for track $num. " .
             "Skipping $mp3files[$i]...\n");
        next;
    }
    
    my ($ext) = $mp3files[$i] =~ /(\..*?)$/;
    $ext = '' if (!defined $ext);

    my $filename = $PATTERN . $ext;

    my %patterns = ("a"         => $artist,
                    "{artist}"  => $artist,
                    "b"         => $album,
                    "{album}"   => $album,
                    "n"         => sprintf('%02d', $num),
                    "{number}"  => sprintf('%02d', $num),
                    "t"         => $track_titles[$num - 1],
                    "{title}"   => $track_titles[$num - 1]);

    $filename = &substitute($filename, %patterns);
    
    $filename =~ s/[\*\?\/\\\:]/-/g;
    
    if ((!$ID3_ONLY) && ((!$SILENT) || ($DEBUG))) {
        print $mp3files[$i] . " -> " . $filename . "\n";
        next if ($DEBUG);
    }

    if ($APPEND_ID3) {        
        my $mp3 = new MP3::ID3v1Tag($mp3files[$i]);
        $mp3->set_title($track_titles[$num - 1]);
        $mp3->set_artist($artist);
        $mp3->set_album($album);
        if (defined $year)
          { $mp3->set_year($year); }
        else
          { $mp3->set_year(0); }
        $mp3->set_genre('Other');
        $mp3->set_comment(sprintf('%02d', $num)); 
        $mp3->save()
          or warn("Warning: Couldn't append ID3 tag to file " .
                  $mp3files[$i] . "\n");
    }
    
    next if $ID3_ONLY; 

    if ($UNDO) {
        my $oldname = $mp3files[$i];
        my $newname = $filename;

        $oldname =~ s/ /\\ /g;
        $newname =~ s/ /\\ /g;

        if ($i == 0) {
            if (-e $UNDO_NAME) {
                rename $UNDO_NAME, $UNDO_NAME . '~'
                  or die("Error: Couldn't create backup of existing undo " .
                         "file: $!\n");
            }

            open OUT, ">$UNDO_NAME"
              or die "Error: Couldn't create undo file: $!\n";
        }
        else {
            open OUT, ">>$UNDO_NAME"
              or die "Error: Couldn't open undo file: $!\n";
        }

        print OUT "$newname $oldname\n";
        close OUT;
    }

    next if $UNDO_ONLY;

    if (rename $mp3files[$i], $filename) {
        $count_renamed++;
    }
    else {
      warn "Warning: Could not rename file " . $mp3files[$i] . "\n";
    }
}

print "Just type 'cat $UNDO_NAME | mmv' to restore the old filenames.\n"
  if $UNDO && !$SILENT && $count_renamed > 0;

sub pretty_print {
    my $list = shift; # array ref
    
    my $max_length = 0;
    foreach (@$list) {
        $max_length = length if length > $max_length;
    }

    my $cols = ++$max_length >= 79 ? 1 : int(79 / $max_length);
    my $rows = @$list / $cols;
    $rows-- if $rows - int($rows) == 0;

    foreach my $i (0..$rows) {
        foreach my $j (0..$cols - 1) {
            my $offset = $cols * $i + $j;
            last if $offset > $#$list;
            printf '%-' . $max_length . 's', $$list[$offset];
        }

        print "\n";
        $i += $cols - 1;
    }
}

sub substitute {
    my $string1  = shift @_;
    my $string2  = '';
    my %patterns = @_;

    $patterns{'%'} = '%';

    return $string1 if ($string1 !~ /\%/);

    my @keys   = keys %patterns;
    my $offset = 0;

    @keys = sort { length $b <=> length $a } @keys;
    
    while (1) {
        my $pos = index $string1, '%', $offset;

        if ($pos == -1) {
            $string2 .= substr $string1, $offset;
            last;
        }
        
        $string2 .= substr $string1, $offset, $pos - $offset;

        if ($pos + 1 == length $string1) {
            $string2 .= '%';
            last;
        }

        my $no_match = 1;
        
        foreach my $key (@keys) {
            if (substr($string1, $pos + 1, length($key)) eq $key) {
                $string2 .= $patterns{$key};
                $offset = ++$pos + length $key;
                $no_match = 0;
                last;
            }
        }

        if ($no_match) {
            $string2 .= '%';
            $offset = ++$pos;
        }
    }

    return $string2;
}

sub list_cddb_files {    
    my $dir = $_[0];
    
    opendir DIR, $dir or die "Error: Couldn't open dir $dir: $!\n";
    my @files = readdir DIR;
    closedir DIR;
    
    foreach (@files) {
        next if (/^\./);
        if (-d "$dir/$_") {
            &list_cddb_files("$dir/$_");
        } else {
            push @cddb_file_list, "$dir/$_";
        }
    }
}

sub help {
print <<HELP; 

rebot3 v$VERSION

usage: rebot3 [OPTIONS]...
  -r,  --remote-cddb
  -cs, --cddb-server SERVER[:PORT]
  -cd, --cdrom-device DEVICE
  -id, --disc-id ID
  -g,  --genre GENRE
  -G,  --print-genres
  -if, --id-file FILE
  -c,  --use-cdda2wav
  -C,  --use-cd
  -db, --cddb-path PATH
  -x,  --xmms-cdinfo-file FILE
  -xo, --xmms-cdinfo-only
  -f,  --file[s] FILES...
  -l,  --track-list LIST
  -S,  --dont-sort
  -n,  --use-number NUMBER
  -p,  --pattern PATTERN
  -t,  --add-id3-tag[s]
  -to, --id3-tag[s]-only
  -T,  --remove-tag[s]
  -u,  --undo
  -uo, --undo-only
  -s,  --silent
  -q,  --quiet
  -d,  --debug
  -h,  --help

Have a look at the manpage for a detailed description of all options.

Please send any comments, suggestions, bug reports to panos\@bigfoot.de.

HELP
exit 1;
}
