#!/usr/local/bin/perl
#
#-----------------------------------------------------------------------------
#
# 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.
#-----------------------------------------------------------------------------
#
#  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.
#-----------------------------------------------------------------------------

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.5";

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 %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.
      --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.

__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;
	}

	# 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] : ");
    my $new_filename = <STDIN>;
    chomp($new_filename);

    if ($new_filename eq "") {
        $new_filename = $mp3file;
    }

    return($new_filename);
}


sub change_album_name {
    if ($ignore_id3tags) {
	return;
    }

    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 = <STDIN>;
	chomp($new_album);

	if ($new_album eq "") {
	    $new_album = $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 {
    if ($ignore_id3tags) {
	return;
    }

    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 = <STDIN>;
	chomp($new_artist);

	if ($new_artist eq "") {
	    $new_artist = $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 eq "") {
        $new_artist = $last_artist;
    }

    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 {
    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 = <STDIN>;
	chomp($new_track);

	if ($new_track eq "") {
	    $new_track = "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 getyn {
    my $promptstr = shift;
    my $retval = -1;

    my $answer = "";
    while ($retval == -1) {
	print("$promptstr [Y/n] : ");
	$answer = lc(<STDIN>);
	chomp($answer);

	if (($answer eq "") || ($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(<STDIN>);
	chomp($answer);

	if (($answer eq "") || ($answer eq "n")) {
            $retval = 0;
	}

	if ($answer eq "y") {
            $retval = 1;
	}
    }
    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 {
    if ($ignore_id3tags) {
	return;
    }

    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;
	    }
	}

	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 = <STDIN>;
	chomp($new_title);

	if ($new_title eq "") {
	    $new_title = $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 {
    return 1 if $ignore_consistency;  # oh well.

    my $mp3file = shift;
    my $check = `mp3_check 2>&1 "$mp3file" |grep "GOOD_FRAMES "`;
    chomp($check);
    $check =~ /GOOD_FRAMES         (\d*)/;
    return($1);  # returns number of good frames; 0 implies it's not an MP3.
}


# 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);
    } 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 $ignore_all_files) and (not is_an_mp3_file($mp3file))) {
	    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");
	    }
	}

	unless ($ignore_all_files) {
	    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 = <STDIN>;
		    chomp($answer);
		    $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;
	while (s/\A //) {}             # trim spaces just in case.
        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();
}

$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 ...

