#!/usr/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/ # # 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. # # Command lines: # # --verbose to chatter a lot. # --recurse to decend into subdirs. # --interactive to attempt to clean up some stuff for you. # --ignore-playlists to not react to the presence of playlist files. # --ignore-fnsize to not draw attention to filenames > 31 characters. # # 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. # #----------------------------------------------------------------------------- # # Copyright (C) 2000 Ryan C. Gordon (icculus@lokigames.com) # # 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 # #----------------------------------------------------------------------------- # !!! FIXME TODO : Read in playlist, if it exists, and attempt to do # !!! FIXME TODO : auto track numbering in a given directory. # globals. $examined_files = 0; # Have we actually loooked at a file? $verbose = 0; # verbose output. $recurse = 0; # descent into directories. $interactive = 0; # Try to clean up stuff? $last_album = ""; # Last interactively entered album name. $last_artist = ""; # Last interactively entered artist name. $ignore_playlists = 0; # Don't bitch about playlist existance? $ignore_fnsize = 0; # Don't bitch about files/dir > 31 characters. # Don't capitalize these words in track titles. @no_cap = qw(and the of in for on a an to at am are so is as); # subroutines. # usage. woohoo. # FIXME: !!! This could be nicer... sub usage { die("USAGE: $0 [--verbose] [--recurse] [--interactive] [--ignore-fnsize] [--ignore-playlists] files...\n"); } # check command lines... sub check_cmdline { my $files_to_check = 0; foreach(@ARGV) { if (!($_ =~ /^--/)) { $files_to_check = 1; next; } if ($_ eq "--verbose") { $verbose = 1; print(" * Verbose output requested.\n"); next; } if ($_ eq "--recurse") { $recurse = 1; next; } if ($_ eq "--interactive") { $interactive = 1; next; } if ($_ eq "--ignore-fnsize") { $ignore_fnsize = 1; next; } if ($_ eq "--ignore-playlists") { $ignore_playlists = 1; next; } # other command line checks go here... # if you hit this, you have a bogus command line option. usage(); } if ($files_to_check == 0) { usage(); } } # 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 extention. sub shrink_file_name { my $arg1 = shift; my $filenameidx = rindex($arg1, '/') + 1; my $mp3path = substr($arg1, 0, $filenameidx); my $mp3file = substr($arg1, $filenameidx); 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 extention briefly. while ($mp3file =~ s/\.//) {} # remove extra periods. $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); } } # 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*) $mp3file = substr($mp3file, 0, 27) . ".mp3"; } } } print("Enter new file name. [$mp3file] : "); my $new_filename = ; chomp($new_filename); if ($new_filename eq "") { $new_filename = $mp3file; } $mp3file = $mp3path . $new_filename; if (!rename($arg1, $mp3file)) { print(" - RENAMING FAILED!\n"); $mp3file = $arg1; } return($mp3file); } sub change_album_name { 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 = ; 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 { 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 = ; 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 = ; 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(); 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(); chomp($answer); if (($answer eq "") || ($answer eq "n")) { $retval = 0; } if ($answer eq "y") { $retval = 1; } } return($retval); } sub add_mp3_extention { 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 change_track_title { 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 = ; 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 = ""; my $pos = index($id3output, $fieldname); if ($pos >= 0) { $retval = substr($id3output, $pos + length($fieldname)); $pos = index($retval, "\n"); $retval = substr($retval, 0, $pos - 1); while ($retval =~ s/ \Z//) {} } return($retval); } sub examine_directory { my $dname = shift; my $filenameidx = rindex($dname, '/') + 1; my $filenamesize = (length($dname) - $filenameidx); if (($filenamesize > 31) && (!$ignore_fnsize)) { print(" - [$dname] is (" . $filenamesize . ") chars long, more than 31!\n"); if ($interactive) { # !!! FIXME. print(" - Interactive mode can't fix directory names!\n"); } } if ($recurse) { recurse_dir($dname); } } sub examine_playlist { my $playlistfile = shift; if (!$ignore_playlists) { print(" - [$playlistfile] is probably an unnecessary playlist.\n"); if ( ($interactive) && (getny("Delete file [$playlistfile]?")) ) { if (!unlink($playlistfile)) { print(" - FAILED TO DELETE [$playlistfile]!\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 "") { $dir = "./"; } 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/skip/recurse it. examine_directory($mp3file); return; } if (($mp3file =~ /playlist\Z/i) || ($mp3file =~ /.m3u\Z/i) || ($mp3file =~ /.sfv\Z/i) || ($mp3file =~ /.nfo\Z/i)) { examine_playlist($mp3file); return; } $examined_files = 1; 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. } if (!($mp3file =~ /.mp3\Z/i)) { print(" - [$mp3file] does not have an .mp3 extention.\n"); if ($interactive) { $mp3file = add_mp3_extention($mp3file); } } my $id3output = `id3tool "$mp3file"`; while ($id3output =~ s/\t//) {} # remove tabs. 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 $filenamesize = (length($mp3file) - $filenameidx); while (($filenamesize > 31) && (!$ignore_fnsize)) { print(" - [$mp3file] is (" . $filenamesize . ") chars long, more than 31!\n"); if ($interactive) { $mp3file = shrink_file_name($mp3file); $filenamesize = (length($mp3file) - $filenameidx); } else { $filenamesize = 0; # (*shrug*) } } # 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. } check_file($mp3file); } } # mainline. # id3tool is VERY important. If it isn't there, bail. if (`which id3tool 2>&1` =~ /no id3tool in/) { print(" - id3tool was not found on in your PATH. You can get it at:\n"); print(" - http://www.freshmeat.net/projects/id3tool/\n"); exit 255; } check_cmdline(@ARGV); 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 ...