#!/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/ # # 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 = ; 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 = ; 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 = ; 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_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 = ; 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 = ; 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 () { 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 ...