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