1#!/usr/local/bin/perl -w 2# 3#----------------------------------------------------------------------------- 4# 5# This is a perl script to check a collection of MP3 files to make sure 6# they are suitable for burning to an ISO9660 CD-ROM. I need this for 7# my Aiwa CDC-MP3 disc player. YMMV. --ryan. 8# 9# This script uses ID3tool, a command line program that can be found by 10# pointing your browser at http://www.freshmeat.net/projects/id3tool/ 11# 12# This script also uses mp3_check (note the difference), but can manage 13# without it. http://www.freshmeat.net/projects/mp3_check/ 14# 15# This script also uses LAME to reencode MP3s to new bitrates, but can 16# manage without it. http://www.freshmeat.net/projects/lame/ 17# 18# If everything is copacetic, this script will return (exit code 0), and 19# not say a thing. The script will only produce output if there's a problem 20# (or you used --verbose), and will exit with a non-zero error code. 21# 22# This is my first Perl program, and I spent as much time hunched over my 23# copy of "Programming Perl" as I spent hunched over my keyboard. I make 24# no promises that any of this is good, correct, or even sane programming 25# practice. Then again, not much in Perl seems to be good, correct, or sane 26# programming practice. Oh well. Enjoy. 27# 28# Thanks to Andi L6hmus for his suggestions, which made it into version 1.1. 29# Thanks to Mark Pulford, who maintains the FreeBSD ports package of mp3check. 30# Thanks to Joshua Kleiner, for suggesting reencoding via LAME and other stuff. 31# Thanks to Aurel Bodenmann, for urging me to add --force-default 32#----------------------------------------------------------------------------- 33# 34# Copyright (C) 2000 Ryan C. Gordon (icculus@clutteredmind.org) 35# 36# This program is free software; you can redistribute it and/or modify 37# it under the terms of the GNU General Public License as published by 38# the Free Software Foundation; either version 2 of the License, or 39# (at your option) any later version. 40# 41# This program is distributed in the hope that it will be useful, 42# but WITHOUT ANY WARRANTY; without even the implied warranty of 43# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 44# GNU General Public License for more details. 45# 46# You should have received a copy of the GNU General Public License 47# along with this program; if not, write to the Free Software 48# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 49# 50#----------------------------------------------------------------------------- 51# Changelog: 52# 1.0 : First release. 53# 1.1 : Added ~/.mp3check config file parsing. 54# Now uses mp3_check (no relation) for verifying mp3 file structure. 55# Now handles directories and can change their names if needed. 56# Added --ignore-dir-names option. 57# Added --ignore-consistency option. 58# Added --ignore-uppercase option. 59# Added --ignore-spaces option. 60# Added --ignore-id3tags option. 61# Added --ignore-all-files option. 62# Added --ignore-track-nums option. 63# Added --no versions of all the options. 64# Changed my email address. 65# Corrected spelling of "extension" all over the place. 66# Improved usage output. 67# Other tweaks, enhancements, and improvements. 68# 1.2 : When shrinking filenames, user input is no longer ignored. Whoops. 69# No longer complains that interactive mode can't fix directory names, 70# because it's no longer true. 71# No longer tries to append .mp3 to directories. 72# 1.3 : Now runs with "use strict" enabled. 73# 1.4 : Can now replaces all "%nnn" characters with underscores. 74# Added --delete-by-default option. 75# Added --ignore-risky-chars option. 76# 1.5 : Can now use lame (http://www.mp3dev.org/) for reencoding mp3s. 77# No longer rechecks the contents of a directory tree if a given 78# directory name is changed. 79# Correctly handles systems that don't have mp3_check/lame/id3tool. 80# 1.6 : Added --force-default. 81# Cleaned up some stuff, removed tabs from source. 82# Fixed --ignore-all-files. 83# "./mp3check --recurse ." doesn't tag the "." as a bad filename. 84#----------------------------------------------------------------------------- 85 86use strict; 87 88# !!! FIXME TODO : Read in playlist, if it exists, and attempt to do 89# !!! FIXME TODO : auto track numbering in a given directory. 90 91 92# globals. 93my $MP3CHECK_VERSION = "1.6"; 94 95my $examined_files = 0; # Have we actually loooked at a file? 96my $last_album = ''; # Last interactively entered album name. 97my $last_artist = ''; # Last interactively entered artist name. 98 99# these are flipped via command lines and the config file. 100my $verbose = 0; # verbose output. 101my $recurse = 0; # descent into directories. 102my $interactive = 0; # Try to clean up stuff? 103my $ignore_playlists = 0; # Don't bitch about playlist existance? 104my $ignore_all_files = 0; # Don't bitch about any non-MP3 file's existance? 105my $ignore_fnsize = 0; # Don't bitch about files/dir > 31 characters. 106my $ignore_id3tags = 0; # Don't examine ID3 tags. 107my $ignore_consistency = 0; # "mp3_check" is installed. 108my $ignore_uppercase = 1; # Don't bitch if filenames have capitals. 109my $ignore_spaces = 0; # Don't bitch if filenames have spaces. 110my $ignore_track_nums = 0; # Don't bitch if track numbers are poorly formed. 111my $ignore_dir_names = 0; # Don't bitch if dir names violate rules. 112my $ignore_risky_chars = 0; # Don't bitch if strange characters are used. 113my $delete_by_default = 0; # default to "y" for delete questions. 114my $do_reencode = 0; # Reencode MP3s. 115my $reencode_bitrate = -1; # change MP3s to a single bitrate. 116my $reencode_freq = -1; # change MP3s to a single sample frequency. 117my $no_id3tool = 0; # id3tool is missing. 118my $force_defaults = 0; # Immediately choose default in interactive mode. 119my %trackhash; 120 121# Don't capitalize these words in track titles. 122my @no_cap = qw(and the of in for on a an to at am are so is as); 123 124 125# subroutines. 126 127# usage. woohoo. 128sub usage { 129 print <<__EOF__; 130 131mp3check $MP3CHECK_VERSION Copyright 2001 Ryan C. Gordon 132 133This program is free software, covered by the GNU General Public License, 134and you are welcome to change it and/or distribute copies of it under 135certain conditions. There is absolutely no warranty for mp3check. 136 137 Program updates: http://www.icculus.org/mp3check/ 138 139USAGE: $0 [options] file1 file2 ... fileN 140 141 options: 142 --verbose Chatter a lot during processing. 143 --recurse Descend into subdirectories. 144 --interactive Ask questions, try to fix problems. 145 --ignore-spaces Don't complain if filenames have spaces. 146 --no-ignore-uppercase Don't complain if filenames have capital chars. 147 --ignore-dir-names Don't complain if directores have naming problems. 148 --ignore-fnsize Don't complain if filenames are too long. 149 --ignore-playlists Don't complain if playlists are present. 150 --ignore-all-files Don't complain about ANY non-MP3 files. 151 --ignore-risky-chars Don't complain if odd chars are used in filenames. 152 --ignore-id3tags Don't look at ID3 tag state at all. 153 --ignore-consistency Don't look at MP3 data structure. 154 --ignore-track-nums Don't look at format of track numbers. 155 --delete-by-default Default to "yes" when asking to delete a file. 156 --reencode-bitrate=N Reencode all MP3s at N kbits/second. 157 --no-reencode-bitrate Don't reencode MP3s' bitrates. 158 --reencode-freq=N Reencode all MP3s at N Hz sample frequency. 159 --no-reencode-freq Don't reencode MP3s' sample frequencies. 160 --force-defaults Automatically pick defaults in interactive mode 161 --help This information. 162 163 Command line options you use all the time can be added to the file 164 ~/.mp3check, one per line. Also, all these command lines have a "NO" 165 version, so, for example, you can specify --no-ignore-id3tags. This is 166 good for overriding the config file, which overrides the defaults. 167 168 YOU USE --force-defaults at your own risk! Files will be altered! 169 170__EOF__ 171 172 exit(255); 173 174} 175 176# check command lines...returns number of NON options. Aborts if there's a 177# unknown --option. 178sub check_cmdline { 179 my @toks = @_; 180 my $files_to_check = 0; 181 182 foreach (@toks) { 183 if (!(/^--/)) { # not an option. 184 $files_to_check++; 185 next; 186 } 187 188 if ($_ eq "--verbose") { 189 $verbose = 1; 190 print(" * Verbose output requested.\n"); 191 next; 192 } 193 194 if ($_ eq "--no-verbose") { 195 if ($verbose) { 196 print(" * Verbose output was requested, then disabled.\n"); 197 } 198 $verbose = 0; 199 next; 200 } 201 202 if ($_ eq "--recurse") { 203 $recurse = 1; 204 next; 205 } 206 207 if ($_ eq "--no-recurse") { 208 $recurse = 0; 209 next; 210 } 211 212 if ($_ eq "--interactive") { 213 $interactive = 1; 214 next; 215 } 216 217 if ($_ eq "--no-interactive") { 218 $interactive = 0; 219 next; 220 } 221 222 if ($_ eq "--ignore-fnsize") { 223 $ignore_fnsize = 1; 224 next; 225 } 226 227 if ($_ eq "--no-ignore-fnsize") { 228 $ignore_fnsize = 0; 229 next; 230 } 231 232 if ($_ eq "--ignore-playlists") { 233 $ignore_playlists = 1; 234 next; 235 } 236 237 if ($_ eq "--no-ignore-playlists") { 238 $ignore_playlists = 0; 239 next; 240 } 241 242 if ($_ eq "--ignore-id3tags") { 243 $ignore_id3tags = 1; 244 next; 245 } 246 247 if ($_ eq "--no-ignore-id3tags") { 248 $ignore_id3tags = 0; 249 next; 250 } 251 252 if ($_ eq "--ignore-consistency") { 253 $ignore_consistency = 1; 254 next; 255 } 256 257 if ($_ eq "--no-ignore-consistency") { 258 $ignore_consistency = 0; 259 next; 260 } 261 262 if ($_ eq "--ignore-uppercase") { 263 $ignore_uppercase = 1; 264 next; 265 } 266 267 if ($_ eq "--no-ignore-uppercase") { 268 $ignore_uppercase = 0; 269 next; 270 } 271 272 if ($_ eq "--ignore-spaces") { 273 $ignore_spaces = 1; 274 next; 275 } 276 277 if ($_ eq "--no-ignore-spaces") { 278 $ignore_spaces = 0; 279 next; 280 } 281 282 if ($_ eq "--ignore-all-files") { 283 $ignore_all_files = 1; 284 next; 285 } 286 287 if ($_ eq "--no-ignore-all-files") { 288 $ignore_all_files = 0; 289 next; 290 } 291 292 if ($_ eq "--ignore-track-nums") { 293 $ignore_track_nums = 1; 294 next; 295 } 296 297 if ($_ eq "--no-ignore-track-nums") { 298 $ignore_track_nums = 0; 299 next; 300 } 301 302 if ($_ eq "--ignore-dir-names") { 303 $ignore_dir_names = 1; 304 next; 305 } 306 307 if ($_ eq "--no-ignore-dir-names") { 308 $ignore_dir_names = 0; 309 next; 310 } 311 312 if ($_ eq "--ignore-risky-chars") { 313 $ignore_risky_chars = 1; 314 next; 315 } 316 317 if ($_ eq "--no-ignore-risky-chars") { 318 $ignore_risky_chars = 0; 319 next; 320 } 321 322 if ($_ eq "--delete-by-default") { 323 $delete_by_default = 1; 324 next; 325 } 326 327 if ($_ eq "--no-delete-by-default") { 328 $delete_by_default = 0; 329 next; 330 } 331 332 if ($_ eq "--help") { 333 usage(); 334 } 335 336 if ($_ eq "--no-help") { 337 print(" - Fine, I WON'T help you.\n"); # Yes, this is a joke. 338 next; 339 } 340 341 if (s/\A--reencode-bitrate=(.*)/$1/) { 342 if (/[^\d]/) { 343 print(" - Invalid bitrate specified. Won't reencode MP3s.\n"); 344 } else { 345 $reencode_bitrate = $_; 346 } 347 next; 348 } 349 350 if ($_ eq "--no-reencode-bitrate") { 351 $reencode_bitrate = -1; 352 next; 353 } 354 355 if (s/\A--reencode-freq=(.*)/$1/) { 356 if (/[^\d]/) { 357 print(" - Invalid sample rate specified. Won't reencode MP3s.\n"); 358 } else { 359 $reencode_freq = $_; 360 } 361 next; 362 } 363 364 if ($_ eq "--no-reencode-freq") { 365 $reencode_freq = -1; 366 next; 367 } 368 369 if ($_ eq '--force-defaults') { 370 $force_defaults = 1; 371 next; 372 } 373 374 if ($_ eq '--no-force-defaults') { 375 $force_defaults = 0; 376 next; 377 } 378 379 # other command line checks go here... 380 381 # if you hit this, you have a bogus command line option. 382 print("Unknown command line option: $_\n"); 383 usage(); 384 } 385 386 return($files_to_check); 387} 388 389 390# This tries to find the best way to shrink the filename to 31 or less 391# chars. First it removes extra dashes, underscores, and whitespace. Then it 392# tries to remove unneeded chars like apostrophes and parentheses. Then it 393# tries a hail mary smooshing of all separators: 01-an_mp3 file-with seps.mp3 394# becomes 01AnMp3FileWithSeps.mp3. If that STILL doesn't work, we give up, 395# and truncate the smooshed version to the first 27 chars of the filename 396# plus the .mp3 extension. 397sub shrink_file_name { 398 my $mp3file = shift; 399 my $is_directory = shift; 400 401 while ($mp3file =~ s/--/-/) {} # remove double dashes. 402 while ($mp3file =~ s/__/_/) {} # remove double underscores. 403 while ($mp3file =~ s/ / /) {} # remove double spaces. 404 405 if (length($mp3file) > 31) { # not good enough? 406 while ($mp3file =~ s/\'//) {} # remove apostrophes. 407 while ($mp3file =~ s/\(//) {} # remove parentheses. 408 while ($mp3file =~ s/\)//) {} # remove parentheses. 409 while ($mp3file =~ s/.mp3\Z//i) {} # remove extension briefly. 410 while ($mp3file =~ s/\.//) {} # remove extra periods. 411 412 unless ($is_directory) { 413 $mp3file = $mp3file . ".mp3"; # put extension back on. 414 } 415 416 if (length($mp3file) > 31) { # still not good enough? 417 while ($mp3file =~ s/-/ /) {} # convert dashes to spaces. 418 while ($mp3file =~ s/_/ /) {} # convert underscores to spaces. 419 while ($mp3file =~ s/^ //) {} # trim spaces just in case. 420 while ($mp3file =~ s/ \Z//) {} # trim spaces just in case. 421 while ($mp3file =~ s/ .mp3\Z/.mp3/i) {} # just in case. 422 423 if (length($mp3file) > 31) { # still not good enough? 424 my $pos = 0; 425 while (($pos = index($mp3file, " ")) > 0) { 426 # convert "my music file name.mp3" to "MyMusicFileName.mp3". 427 $mp3file = substr($mp3file, 0, $pos) . 428 uc(substr($mp3file, $pos + 1, 1)) . 429 substr($mp3file, $pos + 2); 430 } 431 } 432 433 unless ($ignore_uppercase) { 434 $mp3file =~ tr/[A-Z]/[a-z]/; 435 } 436 437 # put a dash between track number and title. 438 # Risk truncation, but oh well. If it's that close... 439 if ($mp3file =~ /^\d\d/) { 440 $mp3file = substr($mp3file, 0, 2) . "-" . substr($mp3file, 2); 441 } 442 443 if (length($mp3file) > 31) { # STILL not good enough? 444 # just truncate. (*shrug*) 445 if ($is_directory) { 446 $mp3file = substr($mp3file, 0, 31); 447 } else { 448 $mp3file = substr($mp3file, 0, 27) . ".mp3"; 449 } 450 } 451 } 452 } 453 454 print("Enter new file name. [$mp3file] : "); 455 return(getstr($mp3file)); 456} 457 458 459sub change_album_name { 460 return if ($ignore_id3tags); 461 462 my $mp3file = shift; 463 my $filenameidx = rindex($mp3file, '/') + 1; 464 my $go_ahead = 1; 465 my $new_album = ''; 466 467 do 468 { 469 $go_ahead = 1; 470 471 my $x = length($last_album); 472 print("Enter new album name. [$last_album] ($x/30 chars) : "); 473 $new_album = getstr($last_album); 474 475 if (length($new_album) > 30) { 476 my $trunc = substr($new_album, 0, 30); 477 print(" - [$new_album] is more than 30 characters!\n"); 478 print(" - It will have to be truncated to [$trunc].\n"); 479 unless (getyn("Proceed, with truncation?")) { 480 $go_ahead = 0; 481 } 482 } 483 } until ($go_ahead); 484 485 if ($new_album ne '') { 486 if (getny("Use [$new_album] for whole directory?")) { 487 $mp3file = "\"" . substr($mp3file, 0, $filenameidx) . 488 "\"*.[mM][pP]3"; 489 } else { 490 $mp3file = "\"$mp3file\""; 491 } 492 493 $last_album = $new_album; 494 495 $new_album =~ s/\\\"/\"/g; 496 $new_album =~ s/\"/\\\"/g; 497 498 `id3tool --set-album=\"$new_album\" $mp3file`; 499 } 500} 501 502sub change_artist_name { 503 return if ($ignore_id3tags); 504 505 my $mp3file = shift; 506 my $filenameidx = rindex($mp3file, '/') + 1; 507 my $go_ahead = 1; 508 my $new_artist = ''; 509 510 do 511 { 512 $go_ahead = 1; 513 514 my $x = length($last_artist); 515 print("Enter new artist name. [$last_artist] ($x/30 chars) : "); 516 $new_artist = getstr($last_artist); 517 518 if (length($new_artist) > 30) { 519 my $trunc = substr($new_artist, 0, 30); 520 print(" - [$new_artist] is more than 30 characters!\n"); 521 print(" - It will have to be truncated to [$trunc].\n"); 522 unless (getyn("Proceed, with truncation?")) { 523 $go_ahead = 0; 524 } 525 } 526 } until ($go_ahead); 527 528 if ($new_artist ne '') { 529 if (getny("Use [$new_artist] for whole directory?")) { 530 $mp3file = "\"" . substr($mp3file, 0, $filenameidx) . 531 "\"*.[mM][pP]3"; 532 } else { 533 $mp3file = "\"$mp3file\""; 534 } 535 536 $last_artist = $new_artist; 537 538 $new_artist =~ s/\\\"/\"/g; 539 $new_artist =~ s/\"/\\\"/g; 540 541 `id3tool --set-artist=\"$new_artist\" $mp3file`; 542 } 543} 544 545sub change_track_number { 546 if ($force_defaults) { 547 # !!! FIXME: fix this somehow...need an intelligent way to pick a 548 # !!! FIXME: sane default... 549 print("\nPROBLEM: Can't change track number when forcing defaults!\n"); 550 return; 551 } 552 553 my $mp3file = shift; 554 my $getout = 0; 555 my $new_track = ""; 556 557 my $filenameidx = rindex($mp3file, '/') + 1; 558 my $filename = substr($mp3file, $filenameidx); 559 560 while ($filename =~ s/^\d//) {} # trim off a previous track number. 561 while ($filename =~ s/^_//) {} # trim off a previous separator. 562 while ($filename =~ s/^-//) {} # trim off a previous separator. 563 while ($filename =~ s/^ //) {} # trim off a previous separator. 564 565 do { 566 print("Enter new track number. [00] : "); 567 $new_track = getstr('tooeasytoskipbyandassigntrack00.'); 568 while (length($new_track) < 2) { 569 $new_track = "0" . $new_track; 570 } 571 572 $getout = 1; 573 for (my $i = 0; (($getout) && ($i < length($new_track))); $i++) { 574 my $ch = substr($new_track, $i, 1); # FIXME: !!! better way to do this? 575 if (($ch lt '0') || ($ch gt '9')) { 576 $getout = 0; 577 } 578 } 579 } while (!$getout); 580 581 my $newfile = substr($mp3file, 0, $filenameidx) . 582 $new_track . '-'. $filename; 583 584 if (!rename($mp3file, $newfile)) { 585 print(" - RENAMING FAILED!\n"); 586 $newfile = $mp3file; 587 } 588 589 return($newfile); 590} 591 592sub getstr { 593 my $defstr = shift; 594 595 my $in = $defstr; 596 if ($force_defaults) { 597 print("$defstr\n"); 598 } else { 599 $in = <STDIN>; 600 chomp($in); 601 $in = $defstr if ($in eq ''); 602 } 603 604 return($in); 605} 606 607sub getyn { 608 my $promptstr = shift; 609 my $retval = -1; 610 611 while ($retval == -1) { 612 print("$promptstr [Y/n] : "); 613 614 my $answer = lc(getstr('y')); 615 if ($answer eq 'y') { 616 $retval = 1; 617 } 618 619 if ($answer eq 'n') { 620 $retval = 0; 621 } 622 } 623 return($retval); 624} 625 626sub getny { 627 my $promptstr = shift; 628 my $retval = -1; 629 630 while ($retval == -1) { 631 print("$promptstr [y/N] : "); 632 633 my $answer = lc(getstr('n')); 634 if ($answer eq 'y') { 635 $retval = 1; 636 } 637 638 if ($answer eq 'n') { 639 $retval = 0; 640 } 641 } 642 return($retval); 643} 644 645sub add_mp3_extension { 646 my $mp3file = shift; 647 my $newfile = $mp3file; 648 649 if (getyn("Append \".mp3\" to file name?")) { 650 $newfile = $newfile . ".mp3"; 651 652 if (!rename($mp3file, $newfile)) { 653 print(" - RENAMING FAILED!\n"); 654 $newfile = $mp3file; 655 } 656 } 657 658 return($newfile); 659} 660 661 662sub askdelete { 663 my $prompt = shift; 664 return( ($delete_by_default) ? getyn($prompt) : getny($prompt) ); 665} 666 667 668sub change_track_title { 669 return if ($ignore_id3tags); 670 671 my $mp3file = shift; 672 my $track_guess = $mp3file; 673 my $filenameidx = rindex($mp3file, '/') + 1; 674 675 $track_guess = lc(substr($track_guess, $filenameidx)); 676 677 # trim whitespace. 678 while ($track_guess =~ s/^ //) {} 679 while ($track_guess =~ s/ \Z//) {} 680 681 # lose ".MP3" at end. 682 $track_guess =~ s/.mp3\Z//i; 683 684 # For tracks such as "01. trackname.mp3"... 685 $track_guess =~ s/^\d\d\.\s//; 686 687 # lose track numbers, if there. 688 while ($track_guess =~ s/^\d//) {} 689 690 # turn '_' to spaces. 691 $track_guess =~ s/_/ /g; 692 693 # turn '-' to spaces. 694 $track_guess =~ s/-/ /g; 695 696 # Take a gamble on junk like "won_t" and "i_m" and "you_re" and "it_s" ... 697 while ($track_guess =~ s/\sm\b/\'m/i) {} 698 while ($track_guess =~ s/\st\b/\'t/i) {} 699 while ($track_guess =~ s/\sre\b/\'re/i) {} 700 while ($track_guess =~ s/\ss\b/\'s/i) {} 701 702 # A few others. 703 while ($track_guess =~ s/\shasnt/ hasn't/i) {} 704 while ($track_guess =~ s/\sdont/ don't/i) {} 705 while ($track_guess =~ s/\syoud/ you'd/i) {} 706 707 # Take a gamble on very simple roman numerals... 708 while ($track_guess =~ s/[iI]i/II/) {} 709 710 # Try to make acronyms captialize (U.S.A., etc.) 711 while ($track_guess =~ s/[\s\.][a-z]\./uc($&)/e) {} 712 713 # trim whitespace. 714 while ($track_guess =~ s/^ //) {} 715 while ($track_guess =~ s/ \Z//) {} 716 while ($track_guess =~ s/ / /) {} 717 718 # FIXME: !!! check for words split by capital letters (smooshing)... 719 720 # capitalize what we've got. 721 # FIXME: !!! There's got to be a cleaner way to do this. 722 my $pos = index($track_guess, " ") + 1; 723 while ($pos > 0) { 724 my $skip_capitalizing = 0; 725 my $pos2 = index($track_guess, " ", $pos); 726 my $tok = ""; 727 if ($pos2 == -1) { 728 $tok = substr($track_guess, $pos); 729 } else { 730 $tok = substr($track_guess, $pos, $pos2 - $pos); 731 } 732 733 foreach(@no_cap) { 734 if ($tok eq $_) { 735 $skip_capitalizing = 1; 736 last; 737 } 738 } 739 740 if (!$skip_capitalizing) { 741 my $fc = substr($track_guess, $pos, 1); 742 if (($fc eq "(") || ($fc eq "[")) { 743 $pos++; 744 } 745 746 $track_guess = substr($track_guess, 0, $pos) . 747 uc(substr($track_guess, $pos, 1)) . 748 substr($track_guess, $pos + 1); 749 } 750 751 $pos = index($track_guess, " ", $pos) + 1; 752 } 753 $track_guess = ucfirst($track_guess); # get first char, too. 754 755 756 # !!! FIXME : so much code duplication... 757 758 my $go_ahead = 1; 759 my $new_title = ""; 760 761 do 762 { 763 $go_ahead = 1; 764 765 my $x = length($track_guess); 766 print("Enter new track title. [$track_guess] ($x/30 chars) : "); 767 $new_title = getstr($track_guess); 768 if (length($new_title) > 30) { 769 my $trunc = substr($new_title, 0, 30); 770 print(" - [$new_title] is more than 30 characters!\n"); 771 print(" - It will have to be truncated to [$trunc].\n"); 772 unless (getyn("Proceed, with truncation?")) { 773 $go_ahead = 0; 774 } 775 } 776 } until ($go_ahead); 777 778 `id3tool --set-title=\"$new_title\" \"$mp3file\"`; 779} 780 781# recurse into a subdir. 782sub recurse_dir { 783 my $arg1 = shift; 784 785 if (!opendir(DIRH, $arg1)) { 786 print(" - Couldn't open directory [$arg1]!\n"); 787 return; 788 } 789 790 if ($verbose) { 791 print(" * Entering directory [$arg1] ...\n"); 792 } 793 794 my @dirfiles = readdir(DIRH); 795 closedir(DIRH); 796 797 foreach(@dirfiles) { 798 if (($_ eq ".") || ($_ eq "..")) { 799 next; 800 } 801 check_file("$arg1/$_"); 802 } 803 804 if ($verbose) { 805 print(" * Leaving directory [$arg1] ...\n"); 806 } 807} 808 809sub get_id3tag_field { 810 my $id3output = shift; 811 my $fieldname = shift; 812 my $retval = ""; 813 814 if ($id3output =~ s/.*\n$fieldname:\s*(.*?)\s*?\n.*/$1/s) { 815 $retval = $id3output; 816 } 817 818 if ($verbose) { 819 print(" * id3tag field [$fieldname] is [$retval].\n"); 820 } 821 822 return($retval); 823} 824 825sub examine_directory { 826 my $dname = shift; 827 if ($recurse) { 828 recurse_dir($dname); 829 } 830} 831 832 833sub examine_playlist { 834 my $playlistfile = shift; 835 836 if (!$ignore_playlists) { 837 print(" - [$playlistfile] is probably an unnecessary playlist.\n"); 838 839 if ( ($interactive) && (askdelete("Delete file [$playlistfile]?")) ) { 840 if (!unlink($playlistfile)) { 841 print(" - FAILED TO DELETE [$playlistfile]!\n"); 842 } 843 } 844 } 845} 846 847sub is_an_mp3_file { 848 my $mp3file = shift; 849 my $hasext = ($mp3file =~ /\.mp3\Z/i) ? 1 : 0; 850 return $hasext; 851} 852 853 854# determine if a file should be reencoded. 855sub should_do_reencode { 856 my $mp3file = shift; 857 858 return 0 if $no_id3tool; 859 return 0 if not $do_reencode; 860 return 0 if -d $mp3file; 861 return 1 if not $interactive; 862 863 my $question = "Reencode [$mp3file] at"; 864 my $comma = ""; 865 if ($reencode_bitrate != -1) { 866 $question = "$question$comma $reencode_bitrate kbits/second"; 867 $comma = ','; 868 } 869 870 if ($reencode_freq != -1) { 871 $question = "$question$comma $reencode_freq HZ"; 872 $comma = ','; 873 } 874 875 return(getyn("$question?")); 876} 877 878 879# determine what we should tell LAME to do when reencoding... 880sub calc_lame_commandline { 881 my $retval = "--mp3input"; 882 883 if ($reencode_bitrate != -1) { 884 $retval = "$retval -b $reencode_bitrate"; 885 } 886 887 if ($reencode_freq != -1) { 888 $retval = "$retval --resample $reencode_freq" 889 } 890 891 return($retval); 892} 893 894 895sub handle_reencoding { 896 my $mp3file = shift; 897 898 return if not should_do_reencode($mp3file); 899 if ($verbose) { 900 print(" * Reencoding [$mp3file] ...\n"); 901 } 902 903 my $id3output = `id3tool "$mp3file"`; 904 my $album = get_id3tag_field($id3output, "Album"); 905 my $artist = get_id3tag_field($id3output, "Artist"); 906 my $title = get_id3tag_field($id3output, "Song Title"); 907 my $note = get_id3tag_field($id3output, "Note"); 908 my $year = get_id3tag_field($id3output, "Year"); 909 my $genre = get_id3tag_field($id3output, "Genre"); 910 911 # strip genre down to numeric information. 912 # This info is given in hex, but id3tool wants it in decimal 913 # when setting the value, later... 914 $genre =~ s/.*? \((.*?)\)/$1/; 915 $genre = hex($genre); 916 917 my $lameargs = calc_lame_commandline(); 918 if ($verbose) { 919 print(" * Calling `lame $lameargs \"$mp3file\" \"$mp3file.tmp\"` ...\n"); 920 } 921 my $rc = `lame $lameargs "$mp3file" "$mp3file.tmp"`; 922 if ($rc) { 923 print(" - Failed to reencode!\n"); 924 unlink("$mp3file.tmp"); 925 } else { 926 927 my $id3toolcmdline = "id3tool"; 928 $id3toolcmdline = "$id3toolcmdline --set-artist=\"$artist\""; 929 $id3toolcmdline = "$id3toolcmdline --set-album=\"$album\""; 930 $id3toolcmdline = "$id3toolcmdline --set-title=\"$title\""; 931 $id3toolcmdline = "$id3toolcmdline --set-note=\"$note\""; 932 $id3toolcmdline = "$id3toolcmdline --set-year=\"$year\""; 933 $id3toolcmdline = "$id3toolcmdline --set-genre=\"$genre\""; 934 $id3toolcmdline = "$id3toolcmdline \"$mp3file.tmp\""; 935 936 if ($verbose) { 937 print(" * Resetting id3tag after reencode.\n"); 938 print(" * Command line is `$id3toolcmdline` ...\n"); 939 } 940 941 `$id3toolcmdline`; 942 943 if (!rename("$mp3file.tmp", "$mp3file")) { 944 print(" - Failed to replace file with reencoded copy!\n"); 945 } 946 } 947} 948 949 950# the actual examination of MP3 files is done here... 951sub check_file { 952 my $origfile = shift; 953 my $mp3file = $origfile; 954 my $tracknum = ""; 955 my $filenameidx = rindex($mp3file, '/') + 1; 956 my $dir = substr($mp3file, 0, $filenameidx); 957 if ($dir eq "") { 958 check_file("./$mp3file"); 959 return; 960 } 961 962 my $pos = 0; 963 964 if ( ($verbose) && (!(-d $mp3file)) ) { 965 print(" * checking [$mp3file] ...\n"); 966 } 967 968 if (! -e $mp3file) { # doesn't exist? Skip it. 969 print(" - [$mp3file] doesn't exist!\n"); 970 return; 971 } 972 973 if (-d $mp3file) { # a directory? Check/recurse it. 974 examine_directory($mp3file); 975 my $f = substr($mp3file, $filenameidx); 976 return if (($f eq '.') or ($f eq '..')); # skip metadirs. 977 } else { 978 # !!! FIXME : This should go through the filename validation 979 # !!! FIXME : routines if not deleted. 980 if (($mp3file =~ /playlist\Z/i) || ($mp3file =~ /.m3u\Z/i) || 981 ($mp3file =~ /.sfv\Z/i) || ($mp3file =~ /.nfo\Z/i)) { 982 examine_playlist($mp3file); 983 return; 984 } 985 986 # !!! FIXME : This should go through the filename validation 987 # !!! FIXME : routines if not deleted. 988 if (not is_an_mp3_file($mp3file)) { 989 if (not $ignore_all_files) { 990 print(" - [$mp3file] does not appear to be an mp3 file.\n"); 991 if ($interactive) { 992 if (askdelete("Delete [$mp3file]?")) { 993 if (!unlink($mp3file)) { 994 print(" - FAILED TO DELETE [$mp3file]!\n"); 995 } 996 } 997 } 998 } 999 return; 1000 } 1001 1002 $examined_files = 1; 1003 1004 unless ($ignore_track_nums) { 1005 if (!(substr($mp3file, $filenameidx) =~ /^\d\d/)) { 1006 print(" - [$mp3file] does not start with a two digit number.\n"); 1007 if ($interactive) { 1008 $mp3file = change_track_number($mp3file); 1009 } 1010 } 1011 1012 # check again, and add to list... 1013 1014 # FIXME: !!! Break this duplicate track number checking off into 1015 # FIXME: !!! it's own subroutine. 1016 # FIXME: !!! This can force you to change an correct track, and leave a 1017 # FIXME: !!! misnumbered track with the wrong name. 1018 $tracknum = substr($mp3file, $filenameidx, 2); 1019 if ($tracknum =~ /^\d\d/) { 1020 while (defined $trackhash{$dir}{$tracknum}) { 1021 if ($trackhash{$dir}{$tracknum} eq $mp3file) { 1022 last; # it's us; it's cool. 1023 } 1024 1025 print(" - [$mp3file] has the same track number as [$trackhash{$dir}{$tracknum}].\n"); 1026 if (!$interactive) { 1027 last; # just get out. 1028 } else { 1029 $mp3file = change_track_number($mp3file); 1030 } 1031 $tracknum = substr($mp3file, $filenameidx, 2); 1032 } 1033 } 1034 1035 if ($tracknum =~ /^\d\d/) { 1036 $trackhash{$dir}{$tracknum} = $mp3file; # add it. 1037 } 1038 } 1039 1040 unless ($ignore_consistency) { 1041 my $check = `mp3_check 2>&1 "$mp3file" |grep "BAD_FRAMES "`; 1042 chomp($check); 1043 $check =~ /BAD_FRAMES (\d*)/; 1044 unless ($1 eq "0") { 1045 print(" - [$mp3file] has internal corruption! $1 bad frames.\n"); 1046 } 1047 } 1048 1049 if (!($mp3file =~ /.mp3\Z/i)) { 1050 print(" - [$mp3file] does not have an .mp3 extension.\n"); 1051 if ($interactive) { 1052 $mp3file = add_mp3_extension($mp3file); 1053 } 1054 } 1055 1056 # !!! FIXME : Synchronize this with the lame id3 filler code... 1057 unless ($ignore_id3tags) { 1058 my $id3output = `id3tool "$mp3file"`; 1059 my $album = get_id3tag_field($id3output, "Album"); 1060 if ($album eq "") { 1061 print(" - [$mp3file] has no album in the id3tag!\n"); 1062 1063 if ($interactive) { 1064 change_album_name($mp3file); 1065 } 1066 } 1067 1068 my $artist = get_id3tag_field($id3output, "Artist"); 1069 if ($artist eq "") { 1070 print(" - [$mp3file] has no artist in the id3tag!\n"); 1071 1072 if ($interactive) { 1073 change_artist_name($mp3file); 1074 } 1075 } 1076 1077 my $title = get_id3tag_field($id3output, "Song Title"); 1078 if ($title eq "") { 1079 print(" - [$mp3file] has no track title in the id3tag!\n"); 1080 1081 if ($interactive) { 1082 change_track_title($mp3file); 1083 } 1084 } 1085 } 1086 } 1087 1088 my $justname = substr($mp3file, $filenameidx); 1089 1090 # !!! FIXME : Break this off to a new subroutine. 1091 unless ((-d $mp3file) && ($ignore_dir_names)) { 1092 unless ($ignore_spaces) { 1093 if ($justname =~ / /) { 1094 print(" - [$mp3file] contains spaces!\n"); 1095 1096 if ($interactive) { 1097 if (getyn("replace with underscores?")) { 1098 $justname =~ s/ /_/g; 1099 if (!rename($mp3file, $dir . $justname)) { 1100 print(" - RENAMING FAILED!\n"); 1101 $justname = substr($mp3file, $filenameidx); 1102 } else { 1103 $mp3file = $dir . $justname; 1104 } 1105 } 1106 } 1107 } 1108 1109 if ($justname =~ /%[0-9]+/) { 1110 print(" - [$mp3file] contains percent sequences!\n"); 1111 1112 if ($interactive) { 1113 if (getyn("replace with underscores?")) { 1114 while ($justname =~ s/%[0-9]+/_/) {} 1115 if (!rename($mp3file, $dir . $justname)) { 1116 print(" - RENAMING FAILED!\n"); 1117 $justname = substr($mp3file, $filenameidx); 1118 } else { 1119 $mp3file = $dir . $justname; 1120 } 1121 } 1122 } 1123 } 1124 } 1125 1126 unless ($ignore_risky_chars) { 1127 my $tmpname = $justname; 1128 my $strippedmp3 = ($tmpname =~ s/\.mp3\Z//); 1129 $strippedmp3 = ($strippedmp3) ? ".mp3" : ""; 1130 while ($tmpname =~ /([^a-zA-Z_\d\- ])/) { 1131 my $risky_char = $1; 1132 my $charnum = ord($1); 1133 print(" - The char '$risky_char' (UNICODE $charnum) in [$justname] is risky.\n"); 1134 if ($interactive) { 1135 print("replace with what char(s)? [blank to delete] : "); 1136 my $answer = getstr(''); 1137 $tmpname =~ s/[^a-zA-Z_\d\- ]/$answer/; 1138 $justname = $tmpname . $strippedmp3; 1139 } else { 1140 $tmpname =~ s/[^a-zA-Z_\d\- ]/_/; # prevent infinite loop. 1141 } 1142 } 1143 1144 if ($dir . $justname ne $mp3file) { 1145 if (rename($mp3file, $dir . $justname)) { 1146 $mp3file = $dir . $justname; 1147 } else { 1148 print(" - RENAMING FAILED!\n"); 1149 $justname = substr($mp3file, $filenameidx); 1150 } 1151 } 1152 } 1153 1154 unless ($ignore_fnsize) { 1155 my $filenamesize = length($justname); 1156 while ($filenamesize > 31) { 1157 print(" - [$mp3file] is (" . $filenamesize . ") chars long, more than 31!\n"); 1158 1159 if ($interactive) { 1160 $justname = shrink_file_name($justname, -d $mp3file); 1161 if (!rename($mp3file, $dir . $justname)) { 1162 print(" - RENAMING FAILED!\n"); 1163 $justname = substr($mp3file, $filenameidx); 1164 } else { 1165 $mp3file = $dir . $justname; 1166 $filenamesize = length($justname); 1167 } 1168 } else { 1169 $filenamesize = 0; # (*shrug*) 1170 } 1171 } 1172 } 1173 1174 unless ($ignore_uppercase) { 1175 if ($justname =~ /[A-Z]/) { 1176 print(" - [$mp3file] has uppercase characters!\n"); 1177 1178 if ($interactive) { 1179 if (getyn("Convert to lowercase?")) { 1180 $justname =~ tr/[A-Z]/[a-z]/; 1181 if (!rename($mp3file, $dir . $justname)) { 1182 print(" - RENAMING FAILED!\n"); 1183 $justname = substr($mp3file, $filenameidx); 1184 } else { 1185 $mp3file = $dir . $justname; 1186 } 1187 } 1188 } 1189 } 1190 } 1191 } 1192 1193 # we may now fail a previously passed test if we changed anything. 1194 if ($mp3file ne $origfile) { 1195 if ($tracknum =~ /^\d\d/) { 1196 delete $trackhash{$dir}{$tracknum}; # don't conflict with ourself. 1197 } 1198 1199 # disable recursion, so we don't recheck the contents of a dir... 1200 my $tmp = $recurse; 1201 $recurse = 0; 1202 check_file($mp3file); 1203 $recurse = $tmp; 1204 } 1205 1206 # Only reencode if everything else passed. 1207 handle_reencoding($mp3file); 1208} 1209 1210 1211# mainline. 1212 1213if (open(CFGHANDLE, $ENV{HOME} . "/.mp3check")) { 1214 my @cfgoptions; 1215 while (<CFGHANDLE>) { 1216 chomp; 1217 1 while (s/\A //); # trim spaces just in case. 1218 1 while (s/ \Z//); # trim spaces just in case. 1219 if (!($_ eq "")) { 1220 push @cfgoptions, $_; 1221 } 1222 } 1223 1224 close(CFGHANDLE); 1225 1226 if (check_cmdline(@cfgoptions) != 0) { 1227 print("Non-option specified in ~/.mp3check!\n"); 1228 exit(255); 1229 } 1230} 1231 1232# actual command lines override config file. 1233if (check_cmdline(@ARGV) == 0) { # no actual files specified? 1234 usage(); 1235} 1236 1237# !!! FIXME ... somehow... 1238if (($force_defaults) and (not $ignore_track_nums)) { 1239 print("BUG: You have to --ignore-track-nums if you --force-defaults.\n"); 1240 exit(254); 1241} 1242 1243$do_reencode = (($reencode_bitrate != -1) || ($reencode_freq != -1)); 1244 1245# id3tool is pretty important. If it isn't there, and the user hasn't 1246# expressed that ID3 tags don't concern her, then bail. 1247if (($ignore_id3tags == 0) && (`which id3tool 2>&1` =~ /no id3tool in/)) { 1248 print(" - id3tool was not found in your PATH. You can get it at:\n"); 1249 print(" - http://www.freshmeat.net/projects/id3tool/\n"); 1250 print(" - If you don't care about ID3 tags (even though you should),\n"); 1251 print(" - then you can disable this warning by using the\n"); 1252 print(" - --ignore-id3tags option.\n"); 1253 print(" - id3tag checking has been disabled in this run of mp3check.\n"); 1254 $ignore_id3tags = 1; 1255 $no_id3tool = 1; 1256} 1257 1258# mp3_check is helpful, but not crucial. 1259if (($ignore_consistency == 0) && (`which mp3_check 2>&1` =~ /no mp3_check in/)) { 1260 print(" - mp3_check (a different tool) was not found in your PATH. You can\n"); 1261 print(" - get it at: http://www.freshmeat.net/projects/mp3_check/\n"); 1262 print(" - If you don't care about your MP3's internal consistency, or you\n"); 1263 print(" - don't want to get mp3_check, then you can disable this warning\n"); 1264 print(" - by using the --ignore-consistency option.\n"); 1265 print(" - Consistency checking has been disabled in this run of mp3check.\n"); 1266 $ignore_consistency = 1; 1267} 1268 1269# LAME is helpful, but not crucial. 1270if (($do_reencode) && (`which lame 2>&1` =~ /no lame in/)) 1271{ 1272 print(" - LAME (an MP3 encoder) was not found in your PATH. You can\n"); 1273 print(" - get it at: http://www.freshmeat.net/projects/lame/\n"); 1274 print(" - You only need LAME if you plan on reencoding MP3s.\n"); 1275 print(" - Reencoding has been disabled in this run of mp3check.\n"); 1276 $do_reencode = 0; 1277} 1278 1279if (($do_reencode) && ($no_id3tool)) { 1280 # No wonder they call it LAME. Reencoding eats the id3tag. 1281 print(" - Reencoding without id3tool presently destroys the id3tag!\n"); 1282 print(" - Cowardly refusing to reencode without id3tool present!\n"); 1283 $do_reencode = 0; 1284} 1285 1286foreach(@ARGV) { 1287 if (!($_ =~ /^--/)) { # make sure it's not a command line option... 1288 check_file($_); 1289 } 1290} 1291 1292if ($examined_files == 0) { 1293 print(" - Did not examine any MP3 files!\n"); 1294} 1295 1296exit 0; 1297 1298# end of mp3check.pl ... 1299 1300