#!/usr/local/bin/perl -w
#
#-----------------------------------------------------------------------------
#
# This is a perl script to check a collection of MP3 files to make sure
#  they are suitable for burning to an ISO9660 CD-ROM. I need this for
#  my Aiwa CDC-MP3 disc player. YMMV.  --ryan.
#
# This script uses ID3tool, a command line program that can be found by
#  pointing your browser at http://www.freshmeat.net/projects/id3tool/
#
# This script also uses mp3_check (note the difference), but can manage
#  without it. http://www.freshmeat.net/projects/mp3_check/
#
# This script also uses LAME to reencode MP3s to new bitrates, but can 
#  manage without it. http://www.freshmeat.net/projects/lame/
#
# If everything is copacetic, this script will return (exit code 0), and
#  not say a thing. The script will only produce output if there's a problem
#  (or you used --verbose), and will exit with a non-zero error code.
#
#  This is my first Perl program, and I spent as much time hunched over my
#   copy of "Programming Perl" as I spent hunched over my keyboard. I make
#   no promises that any of this is good, correct, or even sane programming
#   practice. Then again, not much in Perl seems to be good, correct, or sane
#   programming practice. Oh well. Enjoy.
#
# Thanks to Andi L6hmus for his suggestions, which made it into version 1.1.
# Thanks to Mark Pulford, who maintains the FreeBSD ports package of mp3check.
# Thanks to Joshua Kleiner, for suggesting reencoding via LAME and other stuff.
# Thanks to Aurel Bodenmann, for urging me to add --force-default
#-----------------------------------------------------------------------------
#
#  Copyright (C) 2000 Ryan C. Gordon (icculus@clutteredmind.org)
#
#  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 of the License, 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#-----------------------------------------------------------------------------
# Changelog:
#  1.0 : First release.
#  1.1 : Added ~/.mp3check config file parsing.
#        Now uses mp3_check (no relation) for verifying mp3 file structure.
#        Now handles directories and can change their names if needed.
#        Added --ignore-dir-names option.
#        Added --ignore-consistency option.
#        Added --ignore-uppercase option.
#        Added --ignore-spaces option.
#        Added --ignore-id3tags option.
#        Added --ignore-all-files option.
#        Added --ignore-track-nums option.
#        Added --no versions of all the options.
#        Changed my email address.
#        Corrected spelling of "extension" all over the place.
#        Improved usage output.
#        Other tweaks, enhancements, and improvements.
#  1.2 : When shrinking filenames, user input is no longer ignored. Whoops.
#        No longer complains that interactive mode can't fix directory names,
#          because it's no longer true.
#        No longer tries to append .mp3 to directories.
#  1.3 : Now runs with "use strict" enabled.
#  1.4 : Can now replaces all "%nnn" characters with underscores.
#        Added --delete-by-default option.
#        Added --ignore-risky-chars option.
#  1.5 : Can now use lame (http://www.mp3dev.org/) for reencoding mp3s.
#        No longer rechecks the contents of a directory tree if a given
#        directory name is changed. 
#        Correctly handles systems that don't have mp3_check/lame/id3tool.
#  1.6 : Added --force-default.
#        Cleaned up some stuff, removed tabs from source.
#        Fixed --ignore-all-files.
#        "./mp3check --recurse ." doesn't tag the "." as a bad filename.
#-----------------------------------------------------------------------------

use strict;

# !!! FIXME TODO : Read in playlist, if it exists, and attempt to do
# !!! FIXME TODO :  auto track numbering in a given directory.


# globals.
my $MP3CHECK_VERSION = "1.6";

my $examined_files = 0;       # Have we actually loooked at a file?
my $last_album = '';          # Last interactively entered album name.
my $last_artist = '';         # Last interactively entered artist name.

# these are flipped via command lines and the config file.
my $verbose = 0;              # verbose output.
my $recurse = 0;              # descent into directories.
my $interactive = 0;          # Try to clean up stuff?
my $ignore_playlists = 0;     # Don't bitch about playlist existance?
my $ignore_all_files = 0;     # Don't bitch about any non-MP3 file's existance?
my $ignore_fnsize = 0;        # Don't bitch about files/dir > 31 characters.
my $ignore_id3tags = 0;       # Don't examine ID3 tags.
my $ignore_consistency = 0;   # "mp3_check" is installed.
my $ignore_uppercase = 1;     # Don't bitch if filenames have capitals.
my $ignore_spaces = 0;        # Don't bitch if filenames have spaces.
my $ignore_track_nums = 0;    # Don't bitch if track numbers are poorly formed.
my $ignore_dir_names = 0;     # Don't bitch if dir names violate rules.
my $ignore_risky_chars = 0;   # Don't bitch if strange characters are used.
my $delete_by_default = 0;    # default to "y" for delete questions.
my $do_reencode = 0;          # Reencode MP3s.
my $reencode_bitrate = -1;    # change MP3s to a single bitrate.
my $reencode_freq = -1;       # change MP3s to a single sample frequency.
my $no_id3tool = 0;           # id3tool is missing.
my $force_defaults = 0;       # Immediately choose default in interactive mode.
my %trackhash;

# Don't capitalize these words in track titles.
my @no_cap = qw(and the of in for on a an to at am are so is as);


# subroutines.

# usage. woohoo.
sub usage {
    print <<__EOF__;

mp3check $MP3CHECK_VERSION Copyright 2001 Ryan C. Gordon

This program is free software, covered by the GNU General Public License,
and you are welcome to change it and/or distribute copies of it under
certain conditions. There is absolutely no warranty for mp3check.

  Program updates: http://www.icculus.org/mp3check/

USAGE: $0 [options] file1 file2 ... fileN

  options:
      --verbose             Chatter a lot during processing.
      --recurse             Descend into subdirectories.
      --interactive         Ask questions, try to fix problems.
      --ignore-spaces       Don't complain if filenames have spaces.
      --no-ignore-uppercase Don't complain if filenames have capital chars.
      --ignore-dir-names    Don't complain if directores have naming problems.
      --ignore-fnsize       Don't complain if filenames are too long.
      --ignore-playlists    Don't complain if playlists are present.
      --ignore-all-files    Don't complain about ANY non-MP3 files.
      --ignore-risky-chars  Don't complain if odd chars are used in filenames.
      --ignore-id3tags      Don't look at ID3 tag state at all.
      --ignore-consistency  Don't look at MP3 data structure.
      --ignore-track-nums   Don't look at format of track numbers.
      --delete-by-default   Default to "yes" when asking to delete a file.
      --reencode-bitrate=N  Reencode all MP3s at N kbits/second.
      --no-reencode-bitrate Don't reencode MP3s' bitrates.
      --reencode-freq=N     Reencode all MP3s at N Hz sample frequency.
      --no-reencode-freq    Don't reencode MP3s' sample frequencies.
      --force-defaults      Automatically pick defaults in interactive mode
      --help                This information.

  Command line options you use all the time can be added to the file
    ~/.mp3check, one per line. Also, all these command lines have a "NO"
    version, so, for example, you can specify --no-ignore-id3tags. This is
    good for overriding the config file, which overrides the defaults.

  YOU USE --force-defaults at your own risk! Files will be altered!

__EOF__

    exit(255);

}

# check command lines...returns number of NON options. Aborts if there's a
#  unknown --option.
sub check_cmdline {
    my @toks = @_;
    my $files_to_check = 0;

    foreach (@toks) {
        if (!(/^--/)) {   # not an option.
            $files_to_check++;
            next;
        }

        if ($_ eq "--verbose") {
            $verbose = 1;
            print(" * Verbose output requested.\n");
            next;
        }

        if ($_ eq "--no-verbose") {
            if ($verbose) {
                print(" * Verbose output was requested, then disabled.\n");
            }
            $verbose = 0;
            next;
        }

        if ($_ eq "--recurse") {
            $recurse = 1;
            next;
        }

        if ($_ eq "--no-recurse") {
            $recurse = 0;
            next;
        }

        if ($_ eq "--interactive") {
            $interactive = 1;
            next;
        }

        if ($_ eq "--no-interactive") {
            $interactive = 0;
            next;
        }

        if ($_ eq "--ignore-fnsize") {
            $ignore_fnsize = 1;
            next;
        }

        if ($_ eq "--no-ignore-fnsize") {
            $ignore_fnsize = 0;
            next;
        }

        if ($_ eq "--ignore-playlists") {
            $ignore_playlists = 1;
            next;
        }

        if ($_ eq "--no-ignore-playlists") {
            $ignore_playlists = 0;
            next;
        }

        if ($_ eq "--ignore-id3tags") {
            $ignore_id3tags = 1;
            next;
        }

        if ($_ eq "--no-ignore-id3tags") {
            $ignore_id3tags = 0;
            next;
        }

        if ($_ eq "--ignore-consistency") {
            $ignore_consistency = 1;
            next;
        }

        if ($_ eq "--no-ignore-consistency") {
            $ignore_consistency = 0;
            next;
        }

        if ($_ eq "--ignore-uppercase") {
            $ignore_uppercase = 1;
            next;
        }

        if ($_ eq "--no-ignore-uppercase") {
            $ignore_uppercase = 0;
            next;
        }

        if ($_ eq "--ignore-spaces") {
            $ignore_spaces = 1;
            next;
        }

        if ($_ eq "--no-ignore-spaces") {
            $ignore_spaces = 0;
            next;
        }

        if ($_ eq "--ignore-all-files") {
            $ignore_all_files = 1;
            next;
        }

        if ($_ eq "--no-ignore-all-files") {
            $ignore_all_files = 0;
            next;
        }

        if ($_ eq "--ignore-track-nums") {
            $ignore_track_nums = 1;
            next;
        }

        if ($_ eq "--no-ignore-track-nums") {
            $ignore_track_nums = 0;
            next;
        }

        if ($_ eq "--ignore-dir-names") {
            $ignore_dir_names = 1;
            next;
        }

        if ($_ eq "--no-ignore-dir-names") {
            $ignore_dir_names = 0;
            next;
        }

        if ($_ eq "--ignore-risky-chars") {
            $ignore_risky_chars = 1;
            next;
        }

        if ($_ eq "--no-ignore-risky-chars") {
            $ignore_risky_chars = 0;
            next;
        }

        if ($_ eq "--delete-by-default") {
            $delete_by_default = 1;
            next;
        }

        if ($_ eq "--no-delete-by-default") {
            $delete_by_default = 0;
            next;
        }

        if ($_ eq "--help") {
            usage();
        }

        if ($_ eq "--no-help") {
            print(" - Fine, I WON'T help you.\n");   # Yes, this is a joke.
            next;
        }

        if (s/\A--reencode-bitrate=(.*)/$1/) {
            if (/[^\d]/) {
                print(" - Invalid bitrate specified. Won't reencode MP3s.\n");
            } else {
                $reencode_bitrate = $_;
            }
            next;
        }

        if ($_ eq "--no-reencode-bitrate") {
            $reencode_bitrate = -1;
            next;
        }

        if (s/\A--reencode-freq=(.*)/$1/) {
            if (/[^\d]/) {
                print(" - Invalid sample rate specified. Won't reencode MP3s.\n");
            } else {
                $reencode_freq = $_;
            }
            next;
        }

        if ($_ eq "--no-reencode-freq") {
            $reencode_freq = -1;
            next;
        }

        if ($_ eq '--force-defaults') {
            $force_defaults = 1;
            next;
        }

        if ($_ eq '--no-force-defaults') {
            $force_defaults = 0;
            next;
        }

        # other command line checks go here...

        # if you hit this, you have a bogus command line option.
        print("Unknown command line option: $_\n");
        usage();
    }

    return($files_to_check);
}


# This tries to find the best way to shrink the filename to 31 or less
#  chars. First it removes extra dashes, underscores, and whitespace. Then it
#  tries to remove unneeded chars like apostrophes and parentheses. Then it
#  tries a hail mary smooshing of all separators: 01-an_mp3 file-with seps.mp3
#  becomes 01AnMp3FileWithSeps.mp3. If that STILL doesn't work, we give up,
#  and truncate the smooshed version to the first 27 chars of the filename
#  plus the .mp3 extension.
sub shrink_file_name {
    my $mp3file = shift;
    my $is_directory = shift;

    while ($mp3file =~ s/--/-/) {}   # remove double dashes.
    while ($mp3file =~ s/__/_/) {}   # remove double underscores.
    while ($mp3file =~ s/  / /) {}  # remove double spaces.

    if (length($mp3file) > 31) {  # not good enough?
        while ($mp3file =~ s/\'//) {}       # remove apostrophes.
        while ($mp3file =~ s/\(//) {}       # remove parentheses.
        while ($mp3file =~ s/\)//) {}       # remove parentheses.
        while ($mp3file =~ s/.mp3\Z//i) {}  # remove extension briefly.
        while ($mp3file =~ s/\.//) {}       # remove extra periods.

        unless ($is_directory) {
            $mp3file = $mp3file . ".mp3";  # put extension back on.
        }

        if (length($mp3file) > 31) {  # still not good enough?
            while ($mp3file =~ s/-/ /) {}             # convert dashes to spaces.
            while ($mp3file =~ s/_/ /) {}             # convert underscores to spaces.
            while ($mp3file =~ s/^ //) {}             # trim spaces just in case.
            while ($mp3file =~ s/ \Z//) {}            # trim spaces just in case.
            while ($mp3file =~ s/ .mp3\Z/.mp3/i) {}   # just in case.

            if (length($mp3file) > 31) {  # still not good enough?
                my $pos = 0;
                while (($pos = index($mp3file, " ")) > 0) {
                    # convert "my music file name.mp3" to "MyMusicFileName.mp3".
                    $mp3file = substr($mp3file, 0, $pos) .
                               uc(substr($mp3file, $pos + 1, 1)) .
                               substr($mp3file, $pos + 2);
                }
            }

            unless ($ignore_uppercase) {
                $mp3file =~ tr/[A-Z]/[a-z]/;
            }

            # put a dash between track number and title.
            # Risk truncation, but oh well. If it's that close...
            if ($mp3file =~ /^\d\d/) {
                $mp3file = substr($mp3file, 0, 2) . "-" . substr($mp3file, 2);
            }

            if (length($mp3file) > 31) {  # STILL not good enough?
                # just truncate. (*shrug*)
                if ($is_directory) {
                    $mp3file = substr($mp3file, 0, 31);
                } else {
                    $mp3file = substr($mp3file, 0, 27) . ".mp3";
                }
            }
        }
    }

    print("Enter new file name. [$mp3file] : ");
    return(getstr($mp3file));
}


sub change_album_name {
    return if ($ignore_id3tags);

    my $mp3file = shift;
    my $filenameidx = rindex($mp3file, '/') + 1;
    my $go_ahead = 1;
    my $new_album = '';

    do
    {
        $go_ahead = 1;

        my $x = length($last_album);
        print("Enter new album name. [$last_album] ($x/30 chars) : ");
        $new_album = getstr($last_album);

        if (length($new_album) > 30) {
            my $trunc = substr($new_album, 0, 30);
            print(" - [$new_album] is more than 30 characters!\n");
            print(" - It will have to be truncated to [$trunc].\n");
            unless (getyn("Proceed, with truncation?")) {
                $go_ahead = 0;
            }
        }
    } until ($go_ahead);

    if ($new_album ne '') {
        if (getny("Use [$new_album] for whole directory?")) {
            $mp3file = "\"" . substr($mp3file, 0, $filenameidx) .
                       "\"*.[mM][pP]3";
        } else {
            $mp3file = "\"$mp3file\"";
        }

        $last_album = $new_album;

        $new_album =~ s/\\\"/\"/g;
        $new_album =~ s/\"/\\\"/g;

        `id3tool --set-album=\"$new_album\" $mp3file`;
    }
}

sub change_artist_name {
    return if ($ignore_id3tags);

    my $mp3file = shift;
    my $filenameidx = rindex($mp3file, '/') + 1;
    my $go_ahead = 1;
    my $new_artist = '';

    do
    {
        $go_ahead = 1;

        my $x = length($last_artist);
        print("Enter new artist name. [$last_artist] ($x/30 chars) : ");
        $new_artist = getstr($last_artist);

        if (length($new_artist) > 30) {
            my $trunc = substr($new_artist, 0, 30);
            print(" - [$new_artist] is more than 30 characters!\n");
            print(" - It will have to be truncated to [$trunc].\n");
            unless (getyn("Proceed, with truncation?")) {
                $go_ahead = 0;
            }
        }
    } until ($go_ahead);

    if ($new_artist ne '') {
        if (getny("Use [$new_artist] for whole directory?")) {
            $mp3file = "\"" . substr($mp3file, 0, $filenameidx) .
                       "\"*.[mM][pP]3";
        } else {
            $mp3file = "\"$mp3file\"";
        }

        $last_artist = $new_artist;

        $new_artist =~ s/\\\"/\"/g;
        $new_artist =~ s/\"/\\\"/g;

        `id3tool --set-artist=\"$new_artist\" $mp3file`;
    }
}

sub change_track_number {
    if ($force_defaults) {
        # !!! FIXME: fix this somehow...need an intelligent way to pick a
        # !!! FIXME:  sane default...
        print("\nPROBLEM: Can't change track number when forcing defaults!\n");
        return;
    }

    my $mp3file = shift;
    my $getout = 0;
    my $new_track = "";

    my $filenameidx = rindex($mp3file, '/') + 1;
    my $filename = substr($mp3file, $filenameidx);

    while ($filename =~ s/^\d//) {}  # trim off a previous track number.
    while ($filename =~ s/^_//) {}   # trim off a previous separator.
    while ($filename =~ s/^-//) {}   # trim off a previous separator.
    while ($filename =~ s/^ //) {}   # trim off a previous separator.

    do {
        print("Enter new track number. [00] : ");
        $new_track = getstr('tooeasytoskipbyandassigntrack00.');
        while (length($new_track) < 2) {
            $new_track = "0" . $new_track;
        }

        $getout = 1;
        for (my $i = 0; (($getout) && ($i < length($new_track))); $i++) {
            my $ch = substr($new_track, $i, 1);  # FIXME: !!! better way to do this?
            if (($ch lt '0') || ($ch gt '9')) {
                $getout = 0;
            }
        }
    } while (!$getout);

    my $newfile = substr($mp3file, 0, $filenameidx) .
                  $new_track . '-'. $filename;

    if (!rename($mp3file, $newfile)) {
        print(" - RENAMING FAILED!\n");
        $newfile = $mp3file;
    }

    return($newfile);
}

sub getstr {
    my $defstr = shift;

    my $in = $defstr;
    if ($force_defaults) {
        print("$defstr\n");
    } else {
        $in = <STDIN>;
        chomp($in);
        $in = $defstr if ($in eq '');
    }

    return($in);
}

sub getyn {
    my $promptstr = shift;
    my $retval = -1;

    while ($retval == -1) {
        print("$promptstr [Y/n] : ");

        my $answer = lc(getstr('y'));
        if ($answer eq 'y') {
            $retval = 1;
        }

        if ($answer eq 'n') {
            $retval = 0;
        }
    }
    return($retval);
}

sub getny {
    my $promptstr = shift;
    my $retval = -1;

    while ($retval == -1) {
        print("$promptstr [y/N] : ");

        my $answer = lc(getstr('n'));
        if ($answer eq 'y') {
            $retval = 1;
        }

        if ($answer eq 'n') {
            $retval = 0;
        }
    }
    return($retval);
}

sub add_mp3_extension {
    my $mp3file = shift;
    my $newfile = $mp3file;

    if (getyn("Append \".mp3\" to file name?")) {
        $newfile = $newfile . ".mp3";

        if (!rename($mp3file, $newfile)) {
            print(" - RENAMING FAILED!\n");
            $newfile = $mp3file;
        }
    }

    return($newfile);
}


sub askdelete {
    my $prompt = shift;
    return( ($delete_by_default) ? getyn($prompt) : getny($prompt) );
}


sub change_track_title {
    return if ($ignore_id3tags);

    my $mp3file = shift;
    my $track_guess = $mp3file;
    my $filenameidx = rindex($mp3file, '/') + 1;

    $track_guess = lc(substr($track_guess, $filenameidx));

    # trim whitespace.
    while ($track_guess =~ s/^ //) {}
    while ($track_guess =~ s/ \Z//) {}

    # lose ".MP3" at end.
    $track_guess =~ s/.mp3\Z//i;

    # For tracks such as "01. trackname.mp3"...
    $track_guess =~ s/^\d\d\.\s//;

    # lose track numbers, if there.
    while ($track_guess =~ s/^\d//) {}

    # turn '_' to spaces.
    $track_guess =~ s/_/ /g;

    # turn '-' to spaces.
    $track_guess =~ s/-/ /g;

    # Take a gamble on junk like "won_t" and "i_m" and "you_re" and "it_s" ...
    while ($track_guess =~ s/\sm\b/\'m/i) {}
    while ($track_guess =~ s/\st\b/\'t/i) {}
    while ($track_guess =~ s/\sre\b/\'re/i) {}
    while ($track_guess =~ s/\ss\b/\'s/i) {}

    # A few others.
    while ($track_guess =~ s/\shasnt/ hasn't/i) {}
    while ($track_guess =~ s/\sdont/ don't/i) {}
    while ($track_guess =~ s/\syoud/ you'd/i) {}

    # Take a gamble on very simple roman numerals...
    while ($track_guess =~ s/[iI]i/II/) {}

    # Try to make acronyms captialize (U.S.A., etc.)
    while ($track_guess =~ s/[\s\.][a-z]\./uc($&)/e) {}

    # trim whitespace.
    while ($track_guess =~ s/^ //) {}
    while ($track_guess =~ s/ \Z//) {}
    while ($track_guess =~ s/  / /) {}

    # FIXME: !!! check for words split by capital letters (smooshing)...

    # capitalize what we've got.
    # FIXME: !!! There's got to be a cleaner way to do this.
    my $pos = index($track_guess, " ") + 1;
    while ($pos > 0) {
        my $skip_capitalizing = 0;
        my $pos2 = index($track_guess, " ", $pos);
        my $tok = "";
        if ($pos2 == -1) {
            $tok = substr($track_guess, $pos);
        } else {
            $tok = substr($track_guess, $pos, $pos2 - $pos);
        }

        foreach(@no_cap) {
            if ($tok eq $_) {
                $skip_capitalizing = 1;
                last;
            }
        }

        if (!$skip_capitalizing) {
            my $fc = substr($track_guess, $pos, 1);
            if (($fc eq "(") || ($fc eq "[")) {
                $pos++;
            }

            $track_guess = substr($track_guess, 0, $pos) .
                                  uc(substr($track_guess, $pos, 1)) .
                                  substr($track_guess, $pos + 1);
        }

        $pos = index($track_guess, " ", $pos) + 1;
    }
    $track_guess = ucfirst($track_guess);  # get first char, too.


    # !!! FIXME : so much code duplication...

    my $go_ahead = 1;
    my $new_title = "";

    do
    {
        $go_ahead = 1;

        my $x = length($track_guess);
        print("Enter new track title. [$track_guess] ($x/30 chars) : ");
        $new_title = getstr($track_guess);
        if (length($new_title) > 30) {
            my $trunc = substr($new_title, 0, 30);
            print(" - [$new_title] is more than 30 characters!\n");
            print(" - It will have to be truncated to [$trunc].\n");
            unless (getyn("Proceed, with truncation?")) {
                $go_ahead = 0;
            }
        }
    } until ($go_ahead);

    `id3tool --set-title=\"$new_title\" \"$mp3file\"`;
}

# recurse into a subdir.
sub recurse_dir {
    my $arg1 = shift;

    if (!opendir(DIRH, $arg1)) {
        print(" - Couldn't open directory [$arg1]!\n");
        return;
    }

    if ($verbose) {
        print(" * Entering directory [$arg1] ...\n");
    }

    my @dirfiles = readdir(DIRH);
    closedir(DIRH);

    foreach(@dirfiles) {
        if (($_ eq ".") || ($_ eq "..")) {
            next;
        }
        check_file("$arg1/$_");
    }

    if ($verbose) {
        print(" * Leaving directory [$arg1] ...\n");
    }
}

sub get_id3tag_field {
    my $id3output = shift;
    my $fieldname = shift;
    my $retval = "";

    if ($id3output =~ s/.*\n$fieldname:\s*(.*?)\s*?\n.*/$1/s) {
        $retval = $id3output;
    }

    if ($verbose) {
        print(" * id3tag field [$fieldname] is [$retval].\n");
    }

    return($retval);
}

sub examine_directory {
    my $dname = shift;
    if ($recurse) {
        recurse_dir($dname);
    }
}


sub examine_playlist {
    my $playlistfile = shift;

    if (!$ignore_playlists) {
        print(" - [$playlistfile] is probably an unnecessary playlist.\n");

        if ( ($interactive) && (askdelete("Delete file [$playlistfile]?")) ) {
            if (!unlink($playlistfile)) {
                print(" - FAILED TO DELETE [$playlistfile]!\n");
            }
        }
    }
}

sub is_an_mp3_file {
    my $mp3file = shift;
    my $hasext = ($mp3file =~ /\.mp3\Z/i) ? 1 : 0;
    return $hasext;
}


# determine if a file should be reencoded.
sub should_do_reencode {
    my $mp3file = shift;

    return 0 if $no_id3tool;
    return 0 if not $do_reencode;
    return 0 if -d $mp3file;
    return 1 if not $interactive;

    my $question = "Reencode [$mp3file] at";
    my $comma = "";
    if ($reencode_bitrate != -1) {
        $question = "$question$comma $reencode_bitrate kbits/second";
        $comma = ',';
    }

    if ($reencode_freq != -1) {
        $question = "$question$comma $reencode_freq HZ";
        $comma = ',';
    }

    return(getyn("$question?"));
}


# determine what we should tell LAME to do when reencoding...
sub calc_lame_commandline {
    my $retval = "--mp3input";

    if ($reencode_bitrate != -1) {
        $retval = "$retval -b $reencode_bitrate";
    }

    if ($reencode_freq != -1) {
        $retval = "$retval --resample $reencode_freq"
    }

    return($retval);
}


sub handle_reencoding {
    my $mp3file = shift;

    return if not should_do_reencode($mp3file);
    if ($verbose) {
        print(" * Reencoding [$mp3file] ...\n");
    }

    my $id3output = `id3tool "$mp3file"`;
    my $album = get_id3tag_field($id3output, "Album");
    my $artist = get_id3tag_field($id3output, "Artist");
    my $title = get_id3tag_field($id3output, "Song Title");
    my $note = get_id3tag_field($id3output, "Note");
    my $year = get_id3tag_field($id3output, "Year");
    my $genre = get_id3tag_field($id3output, "Genre");

    # strip genre down to numeric information.
    #  This info is given in hex, but id3tool wants it in decimal
    #  when setting the value, later...
    $genre =~ s/.*? \((.*?)\)/$1/;
    $genre = hex($genre);

    my $lameargs = calc_lame_commandline();
    if ($verbose) {
        print(" * Calling `lame $lameargs \"$mp3file\" \"$mp3file.tmp\"` ...\n");
    }
    my $rc = `lame $lameargs "$mp3file" "$mp3file.tmp"`;
    if ($rc) {
        print(" - Failed to reencode!\n");
        unlink("$mp3file.tmp");
    } else {

        my $id3toolcmdline = "id3tool";
        $id3toolcmdline = "$id3toolcmdline --set-artist=\"$artist\"";
        $id3toolcmdline = "$id3toolcmdline --set-album=\"$album\"";
        $id3toolcmdline = "$id3toolcmdline --set-title=\"$title\"";
        $id3toolcmdline = "$id3toolcmdline --set-note=\"$note\"";
        $id3toolcmdline = "$id3toolcmdline --set-year=\"$year\"";
        $id3toolcmdline = "$id3toolcmdline --set-genre=\"$genre\"";
        $id3toolcmdline = "$id3toolcmdline \"$mp3file.tmp\"";

        if ($verbose) {
            print(" * Resetting id3tag after reencode.\n");
            print(" * Command line is `$id3toolcmdline` ...\n");
        }

        `$id3toolcmdline`;

        if (!rename("$mp3file.tmp", "$mp3file")) {
            print(" - Failed to replace file with reencoded copy!\n");
        }
    }
}


# the actual examination of MP3 files is done here...
sub check_file {
    my $origfile = shift;
    my $mp3file = $origfile;
    my $tracknum = "";
    my $filenameidx = rindex($mp3file, '/') + 1;
    my $dir = substr($mp3file, 0, $filenameidx);
    if ($dir eq "") {
        check_file("./$mp3file");
        return;
    }

    my $pos = 0;

    if ( ($verbose) && (!(-d $mp3file)) ) {
        print(" * checking [$mp3file] ...\n");
    }

    if (! -e $mp3file) {     # doesn't exist? Skip it.
        print(" - [$mp3file] doesn't exist!\n");
        return;
    }

    if (-d $mp3file) {       # a directory? Check/recurse it.
        examine_directory($mp3file);
        my $f = substr($mp3file, $filenameidx);
        return if (($f eq '.') or ($f eq '..')); # skip metadirs.
    } else {
        # !!! FIXME : This should go through the filename validation
        # !!! FIXME :  routines if not deleted.
        if (($mp3file =~ /playlist\Z/i) || ($mp3file =~ /.m3u\Z/i) ||
            ($mp3file =~ /.sfv\Z/i) || ($mp3file =~ /.nfo\Z/i))  {
                examine_playlist($mp3file);
                return;
        }

        # !!! FIXME : This should go through the filename validation
        # !!! FIXME :  routines if not deleted.
        if (not is_an_mp3_file($mp3file)) {
            if (not $ignore_all_files) {
                print(" - [$mp3file] does not appear to be an mp3 file.\n");
                if ($interactive) {
                    if (askdelete("Delete [$mp3file]?")) {
                        if (!unlink($mp3file)) {
                            print(" - FAILED TO DELETE [$mp3file]!\n");
                        }
                    }
                }
            }
            return;
        }

        $examined_files = 1;

        unless ($ignore_track_nums) {
            if (!(substr($mp3file, $filenameidx) =~ /^\d\d/)) {
                print(" - [$mp3file] does not start with a two digit number.\n");
                if ($interactive) {
                    $mp3file = change_track_number($mp3file);
                }
            }

            # check again, and add to list...

            # FIXME: !!! Break this duplicate track number checking off into
            # FIXME: !!!  it's own subroutine.
            # FIXME: !!! This can force you to change an correct track, and leave a
            # FIXME: !!!  misnumbered track with the wrong name.
            $tracknum = substr($mp3file, $filenameidx, 2);
            if ($tracknum =~ /^\d\d/) {
                while (defined $trackhash{$dir}{$tracknum}) {
                    if ($trackhash{$dir}{$tracknum} eq $mp3file) {
                        last;  # it's us; it's cool.
                    }

                    print(" - [$mp3file] has the same track number as [$trackhash{$dir}{$tracknum}].\n");
                    if (!$interactive) {
                        last;  # just get out.
                    } else {
                        $mp3file = change_track_number($mp3file);
                    }
                    $tracknum = substr($mp3file, $filenameidx, 2);
                }
            }

            if ($tracknum =~ /^\d\d/) {
                $trackhash{$dir}{$tracknum} = $mp3file;  # add it.
            }
        }

        unless ($ignore_consistency) {
            my $check = `mp3_check 2>&1 "$mp3file" |grep "BAD_FRAMES "`;
            chomp($check);
            $check =~ /BAD_FRAMES          (\d*)/;
            unless ($1 eq "0") {
                print(" - [$mp3file] has internal corruption! $1 bad frames.\n");
            }
        }

        if (!($mp3file =~ /.mp3\Z/i)) {
            print(" - [$mp3file] does not have an .mp3 extension.\n");
            if ($interactive) {
                $mp3file = add_mp3_extension($mp3file);
            }
        }

        # !!! FIXME : Synchronize this with the lame id3 filler code...
        unless ($ignore_id3tags) {
            my $id3output = `id3tool "$mp3file"`;
            my $album = get_id3tag_field($id3output, "Album");
            if ($album eq "") {
                print(" - [$mp3file] has no album in the id3tag!\n");

                if ($interactive) {
                    change_album_name($mp3file);
                }
            }

            my $artist = get_id3tag_field($id3output, "Artist");
            if ($artist eq "") {
                print(" - [$mp3file] has no artist in the id3tag!\n");

                if ($interactive) {
                    change_artist_name($mp3file);
                }
            }

            my $title = get_id3tag_field($id3output, "Song Title");
            if ($title eq "") {
                print(" - [$mp3file] has no track title in the id3tag!\n");

                if ($interactive) {
                    change_track_title($mp3file);
                }
            }
        }
    }

    my $justname = substr($mp3file, $filenameidx);

    # !!! FIXME : Break this off to a new subroutine.
    unless ((-d $mp3file) && ($ignore_dir_names)) {
        unless ($ignore_spaces) {
            if ($justname =~ / /) {
                print(" - [$mp3file] contains spaces!\n");

                if ($interactive) {
                    if (getyn("replace with underscores?")) {
                        $justname =~ s/ /_/g;
                        if (!rename($mp3file, $dir . $justname)) {
                            print(" - RENAMING FAILED!\n");
                            $justname = substr($mp3file, $filenameidx);
                        } else {
                            $mp3file = $dir . $justname;
                        }
                    }
                }
            }

            if ($justname =~ /%[0-9]+/) {
                print(" - [$mp3file] contains percent sequences!\n");

                if ($interactive) {
                    if (getyn("replace with underscores?")) {
                        while ($justname =~ s/%[0-9]+/_/) {}
                        if (!rename($mp3file, $dir . $justname)) {
                            print(" - RENAMING FAILED!\n");
                            $justname = substr($mp3file, $filenameidx);
                        } else {
                            $mp3file = $dir . $justname;
                        }
                    }
                }
            }
        }

        unless ($ignore_risky_chars) {
            my $tmpname = $justname;
            my $strippedmp3 = ($tmpname =~ s/\.mp3\Z//);
            $strippedmp3 = ($strippedmp3) ? ".mp3" : "";
            while ($tmpname =~ /([^a-zA-Z_\d\- ])/) {
                my $risky_char = $1;
                my $charnum = ord($1);
                print(" - The char '$risky_char' (UNICODE $charnum) in [$justname] is risky.\n");
                if ($interactive) {
                    print("replace with what char(s)? [blank to delete] : ");
                    my $answer = getstr('');
                    $tmpname =~ s/[^a-zA-Z_\d\- ]/$answer/;
                    $justname = $tmpname . $strippedmp3;
                } else {
                    $tmpname =~ s/[^a-zA-Z_\d\- ]/_/;  # prevent infinite loop.
                }
            }

            if ($dir . $justname ne $mp3file) {
                if (rename($mp3file, $dir . $justname)) {
                    $mp3file = $dir . $justname;
                } else {
                    print(" - RENAMING FAILED!\n");
                    $justname = substr($mp3file, $filenameidx);
                }
            }
        }

        unless ($ignore_fnsize) {
            my $filenamesize = length($justname);
            while ($filenamesize > 31) {
                print(" - [$mp3file] is (" . $filenamesize . ") chars long, more than 31!\n");

                if ($interactive) {
                    $justname = shrink_file_name($justname, -d $mp3file);
                    if (!rename($mp3file, $dir . $justname)) {
                        print(" - RENAMING FAILED!\n");
                        $justname = substr($mp3file, $filenameidx);
                    } else {
                        $mp3file = $dir . $justname;
                        $filenamesize = length($justname);
                    }
                } else {
                    $filenamesize = 0;  # (*shrug*)
                }
            }
        }

        unless ($ignore_uppercase) {
            if ($justname =~ /[A-Z]/) {
                print(" - [$mp3file] has uppercase characters!\n");

                if ($interactive) {
                    if (getyn("Convert to lowercase?")) {
                        $justname =~ tr/[A-Z]/[a-z]/;
                        if (!rename($mp3file, $dir . $justname)) {
                            print(" - RENAMING FAILED!\n");
                            $justname = substr($mp3file, $filenameidx);
                        } else {
                            $mp3file = $dir . $justname;
                        }
                    }
                }
            }
        }
    }

    # we may now fail a previously passed test if we changed anything.
    if ($mp3file ne $origfile) {
        if ($tracknum =~ /^\d\d/) {
            delete $trackhash{$dir}{$tracknum};  # don't conflict with ourself.
        }

        # disable recursion, so we don't recheck the contents of a dir...
        my $tmp = $recurse;
        $recurse = 0;
        check_file($mp3file);
        $recurse = $tmp;
    }

    # Only reencode if everything else passed.
    handle_reencoding($mp3file);
}


# mainline.

if (open(CFGHANDLE, $ENV{HOME} . "/.mp3check")) {
    my @cfgoptions;
    while (<CFGHANDLE>) {
        chomp;
        1 while (s/\A //);             # trim spaces just in case.
        1 while (s/ \Z//);            # trim spaces just in case.
        if (!($_ eq "")) {
            push @cfgoptions, $_;
        }
    }

    close(CFGHANDLE);

    if (check_cmdline(@cfgoptions) != 0) {
        print("Non-option specified in ~/.mp3check!\n");
        exit(255);
    }
}

# actual command lines override config file.
if (check_cmdline(@ARGV) == 0) {   # no actual files specified?
    usage();
}

# !!! FIXME ... somehow...
if (($force_defaults) and (not $ignore_track_nums)) {
    print("BUG: You have to --ignore-track-nums if you --force-defaults.\n");
    exit(254);
}

$do_reencode = (($reencode_bitrate != -1) || ($reencode_freq != -1));

# id3tool is pretty important. If it isn't there, and the user hasn't
#  expressed that ID3 tags don't concern her, then bail.
if (($ignore_id3tags == 0) && (`which id3tool 2>&1` =~ /no id3tool in/)) {
    print(" - id3tool was not found in your PATH. You can get it at:\n");
    print(" -   http://www.freshmeat.net/projects/id3tool/\n");
    print(" - If you don't care about ID3 tags (even though you should),\n");
    print(" -  then you can disable this warning by using the\n");
    print(" -  --ignore-id3tags option.\n");
    print(" - id3tag checking has been disabled in this run of mp3check.\n");
    $ignore_id3tags = 1;
    $no_id3tool = 1;
}

# mp3_check is helpful, but not crucial.
if (($ignore_consistency == 0) && (`which mp3_check 2>&1` =~ /no mp3_check in/)) {
    print(" - mp3_check (a different tool) was not found in your PATH. You can\n");
    print(" -  get it at: http://www.freshmeat.net/projects/mp3_check/\n");
    print(" - If you don't care about your MP3's internal consistency, or you\n");
    print(" -  don't want to get mp3_check, then you can disable this warning\n");
    print(" -  by using the --ignore-consistency option.\n");
    print(" - Consistency checking has been disabled in this run of mp3check.\n");
    $ignore_consistency = 1;
}

# LAME is helpful, but not crucial.
if (($do_reencode) && (`which lame 2>&1` =~ /no lame in/))
{
    print(" - LAME (an MP3 encoder) was not found in your PATH. You can\n");
    print(" -  get it at: http://www.freshmeat.net/projects/lame/\n");
    print(" - You only need LAME if you plan on reencoding MP3s.\n");
    print(" - Reencoding has been disabled in this run of mp3check.\n");
    $do_reencode = 0;
}

if (($do_reencode) && ($no_id3tool)) {
    # No wonder they call it LAME. Reencoding eats the id3tag.
    print(" - Reencoding without id3tool presently destroys the id3tag!\n");
    print(" - Cowardly refusing to reencode without id3tool present!\n");
    $do_reencode = 0;
}

foreach(@ARGV) {
    if (!($_ =~ /^--/)) {   # make sure it's not a command line option...
        check_file($_);
    }
}

if ($examined_files == 0) {
    print(" - Did not examine any MP3 files!\n");
}

exit 0;

# end of mp3check.pl ...

