#!/usr/bin/perl -w use strict; use warnings; # Munge an MP3 file into a nice directory structure, derived from MP3 tags # Actual file format comes from ~/.griprc, if present, or defaults to # Artist/Album/Tracknum - Title.mp3 # Also fix up mp3 tags where possible, using CDDB and file location and # anything else that seems plausible. # Waider May 2000 # # 15/05/2003 Removed dependency on id3convert # 25/11/2003 Added hooks to XMMS via external program # early 2011 Added some support for refiling into iTunes-friendly shape # # Search on freedb for artist/album: # http://www.freedb.org/freedb_search.php?words=&allfields=NO&fields=artist&fields=title&allcats=YES&grouping=none # fields can also have: track (track name), rest (full-text search over the rest of the data) # # FreeDB HTTP Query Format: (when you know the genre and the ID) # Note, there's another interface that only appears to work directly # on www.freedb.org and not any of the mirrors. # http://www.freedb.org/~cddb/cddb.cgi?cmd=cddb+read+rock+12345678&hello=joe+my.host.com+xmcd+2.1&proto=1 # # FIXMEs: # Should be more aware of directory trees - use File::Find? # Store old filename in COMMENT field? # smarter about finding files when we know the album # what about files with multiple matches? # what about files with > 1 page of matches? # unknown album & unknown artist should probably do an exact title match # option to do exact or fuzzy search # proper fuzzy search? # clean up code! # Various / Various Artists handling # Provide track number on command line. # If filename = title, be smarter about retaining rather than truncating # (not an issue if I hurry up and do ID3v2.3 tagging) # Pull discid file if present, then find cddb file or look up CDDB and parse. # Create new discid file if not present. # Move discid files around or delete them as necessary # # Needs more work: # Duplicate/Namespace collision should ignore MP3 tags # (need to extract raw mp3 data, I guess) # -> this is working now, but using external programs to do the # stripping and comparison (id3convert, from id3lib, and cmp). # Comparison can be done just fine in Perl, but the stripping # really requires a smarter MP3Info module. # # match track numbers where available # -> yes, but what if the track number is wrong, ted? # # Broken as designed: # why can't I use CDDB.pm? # -> because cddb.com's HTML ID's != cddb id's. losers. # -> HOWEVER. We could fake up a CDDB id using jwz's code, then look up # based on that. Chances are it should be pretty close to the real ID. # # Remove any extraneous info like older ID tags # -> for now, could simply discard v2.2 info since it screws up XMMS' # notion of the display name; XMMS uses its own ID3 parser # (hello! id3lib!) which can't hack v2.3 tags. # Hacking on MPEG::MP3Info has helped this. use MP3::Info; use MP3::ID3Lib; use File::Basename; use LWP::UserAgent; use URI::Escape; use File::Find; use File::Copy; use POSIX; use Getopt::Long; use CDDB; use Data::Dumper; use File::Basename; use File::Path; use File::Spec; use Encode qw(:all); my $cddb = 0; # by default, don't use CDDB. my $dryrun = 1; # don't REALLY move stuff my $recursive = 0; # skip the find my $verbose = 0; # be kinda quiet my $mp3root = "$ENV{'HOME'}/lib/mp3"; # http://nostatic.org/grip/doc/ar01s04.html#gripswitches my $mp3fileformat = "%a/%d/%t - %n%x"; my $no_lower_case = 1; my $no_underscore = 1; my $allow_these_chars = ""; my $frobcase = 0; my $force = 0; # rename, even if there are missing data my $debug = 0; my $itunes = 1; # Read .grip file if it's present if ( open( GRIPRC, "$ENV{'HOME'}/.grip" )) { griprc: while ( ) { # things we're interested in, which I could probably parse with an eval. # mp3fileformat if ( /^mp3fileformat (.*)$/) { $mp3fileformat = $1; next griprc; } # no_lower_case if ( /^no_lower_case (.*)$/) { $no_lower_case = $1; next griprc; } # no_underscore if ( /^no_underscore (.*)$/) { $no_underscore = $1; next griprc; } # allow_these_chars if ( /^allow_these_chars (.*)$/) { $allow_these_chars = $1; next griprc; } } close( GRIPRC ); } my $doxmms = 0; my $swap = 0; my ( $artist_override, $album_override, $title_override, $tracknum_override, $mp3root_override ); GetOptions( "cddb!" => \$cddb, "dry-run!" => \$dryrun, "dryrun!" => \$dryrun, "force!" => \$force, "recursive!" => \$recursive, "verbose!" => \$verbose, "artist:s" => \$artist_override, "album:s" => \$album_override, "title:s" => \$title_override, "tracknum:s" => \$tracknum_override, "swap!" => \$swap, # Grip stuff "mp3fileformat:s" => \$mp3fileformat, "no_lower_case!" => \$no_lower_case, "no_underscore!" => \$no_underscore, "allow_these_chars:s" => \$allow_these_chars, "xmms!" => \$doxmms, "debug!" => \$debug, "itunes!" => \$itunes, "mp3root=s" => \$mp3root_override, ) or die; # Refigure mp3root - it's the deepest we can get without hitting a "%" name. # I'm just too fucking clever by far. if ( $mp3root_override ) { $mp3root = $mp3root_override; } else { ( $mp3root ) = $mp3fileformat =~ m|^(.*?)/[^/]*%|; } # Allow these chars: $allow_these_chars = "[^A-Za-z0-9" . quotemeta( $allow_these_chars ); $allow_these_chars .= " " if $no_underscore; $allow_these_chars .= ""; # lowercase $allow_these_chars .= "ŁāÁɁȁʁˁρ́΁́ҁԁՁցЁӁفځہ܁сƁǁ؁ށ݁"; # uppercase $allow_these_chars .= "_"; # itunes if ( $itunes ) { $allow_these_chars .= "()':\\-&,\\[\\]\\?\\/\\!\"\\.\@\\*\%\#\$;^\\+"; $mp3fileformat = File::Spec->join( $mp3root, "%a/%d/%t %n%x" ); } $mp3fileformat =~ s/%(.)/%{$1}/g; $allow_these_chars .= "]"; # cache area for Web-retrieved CDDB listings ! -d "$ENV{'HOME'}/.cddb-web" && mkdir "$ENV{'HOME'}/.cddb-web"; # Piping hot $| = 1; if ( $#ARGV == -1 ) { die "Usage: $0 mp3file(s)\nStopped"; } if ( $recursive ) { $#ARGV = -1; # clear all the args. # now load up ARGV with all the files we can find from here. find( { wanted => sub { push @ARGV, $File::Find::name if !-d $File::Find::name; }, no_chdir => 1 }, '.'); } my ( $cddb_file ); for my $mp3file ( @ARGV ) { my ( $artist, $album, $title, $tracknum, $genre, $year, $comment, $title_with_tracknum ); print "Looking at $mp3file\n" if $debug; # If we've been passed a directory, go dig into it. Screw the recursive # flag, really. if ( -d $mp3file ) { print "$mp3file is a directory, scanning it...\n" if $verbose; find( { wanted => sub { push @ARGV, $File::Find::name if !-d $File::Find::name; }, no_chdir => 1 }, $mp3file); next; # so we don't end up processing the damn thing! } $mp3file =~ s|^\./||; next if !-f $mp3file; # because sometimes we nuke 'em. my $did = dirname( $mp3file ) . "/discid"; if ( -f $did or $mp3file =~ m|/discid$| ) { # because it's special if ( open( DISCID, "$did" )) { my $discid = ; chomp( $discid ); if ( -f "$ENV{HOME}/.cddb/$discid" ) { if ( open( CDDB, "$ENV{HOME}/.cddb/$discid" )) { my @lines = ; my $disc_details = CDDB::parse_xmcd_file( \@lines ); ( $artist, $album ) = $disc_details->{dtitle} =~ /^(.*) \/ (.*)$/; } } } else { warn "failed to open $mp3file: $!"; } } print "Processing $mp3file\n" if $verbose; # Hum. Let's see what's in there: my $tag = get_mp3tag( $mp3file ); # what tag version? if ( defined( $tag->{'TAGVERSION'})) { print " MP3 tags v ". $tag->{'TAGVERSION'} . "\n" if $verbose; # id3lib doesn't pull the TCMP frame because it's empty? $tag->{TCMP} = get_tcmp( $mp3file ); my $id3 = new MP3::ID3Lib( $mp3file ); foreach my $frame (@{$id3->frames}) { my $code = $frame->code; my $description = $frame->description; my $value = $frame->value; if ( $verbose ) { print " $code / $description: $value\n"; } if ( $code eq 'TRCK' ) { $tag->{TRACKNUM} = $value; if ( $tag->{TRACKNUM} =~ s@/(\d+)@@ ) { $tag->{TRACKMAX} = $1; } } if ( $code eq 'TPOS' ) { $tag->{TPA} = $value; } } } else { print " No MP3 tags found\n"; } # Play with the file's extension my ( $oldname, $ext ) = $mp3file =~ m|^(.*)(\.[^./]+)$|; $oldname ||= $mp3file; # if there's no extension... $ext ||= ".mp3"; $ext = lc( $ext ); my $type = get_file_type( $mp3file ); if ( $type eq "audio/x-aiff" ) { $ext = '.aiff'; } elsif ( $type eq "audio/x-wav" ) { $ext = '.wav'; } elsif ( $type eq "audio/mp4" ) { $ext = '.m4a'; } elsif ( $type eq "audio/mpeg" ) { $ext = ".mp3" unless $ext =~ /\.mp[234]/; # XXX } else { warn "unknown type $type for $mp3file\n"; $ext = ".mp3" unless $ext =~ /\.mp[234]/; # XXX } # Successive sources of artist if ( $artist_override ) { $artist = $artist_override; } $artist ||= $tag->{'ARTIST'} || ""; if ( defined( $artist ) and ref $artist ) { $artist = $tag->{ARTIST}->[-1]; } $artist ||= "Unknown_Artist"; # Album name $album = $album_override; $album ||= $tag->{'ALBUM'} || ""; if ( defined $album and ref $album ) { $album = $tag->{ALBUM}->[-1]; } if (!$album or ( $album eq "Unknown_Album" )) { # try and figure out from where we are my (undef, $dirs, undef ) = fileparse( $mp3file, '\.[^.\/]*' ); my @dirs = reverse split( "/", $dirs ); $album = shift @dirs; $album = "" if $album eq "."; # file is in cwd $tag->{'ALBUM'} = $album; if ( $artist eq "Unknown_Artist" ) { $artist = shift @dirs; $tag->{'ARTIST'} = $artist; $artist ||= ""; $artist =~ s/_/ /g; } } $album ||= "Unknown_Album"; # hackety hack $artist =~ s/_/ /g; $album =~ s/_/ /g; # Get the title $title = $title_override; $title ||= $tag->{'TITLE'} || ""; if ( defined $title and ref $title ) { $title = $tag->{TITLE}->[-1]; } $title ||= basename( $oldname ); print " We're starting with a title of $title\n" if $verbose; # Harsh, but workable if ( $tag->{TAGVERSION} and $tag->{TAGVERSION} !~ /v2\./ ) { # ugly if ( not $title_override and length( $title ) == 30 and length( basename( $oldname )) > length( basename( $title ))) { print " Using filename because it looks like your tag is truncated\n" if $verbose; $title = basename( $oldname ); if ( $artist and $title =~ /^$artist\b/i ) { print " Removing artist from filename\n" if $verbose; $title =~ s/^$artist\b//i; } } } # Do some cleanup, maybe my ( undef, $mp3title ) = $title =~ m|^\s*(\d{1-3})[\-\.\_ ]+([^0-9]+?)|; if ( defined( $mp3title ) and $mp3title and ( $mp3title ne $title )) { $mp3title =~ s/_/ /g; # taking liberties XXX only if requested? $tag->{'TITLE'} = $mp3title; } # See if we have a track number: $tracknum = $tracknum_override; # Obvious place $tracknum ||= $tag->{'TRACKNUM'}; if ( ref $tracknum ) { $tracknum = $tag->{TRACKNUM}->[-1]; } # Title contains tracknum? if ( !defined( $tracknum ) or $tracknum !~ /^\s*\d+\s*$/ ) { ( $tracknum, undef ) = $title =~ m|^\s*(\d{1,3})[\-\.\_ ]+([^0-9,]+?)|; if ( !$tracknum ) { undef( $tracknum ); } else { print " Found tracknum $tracknum in title\n" if $verbose and defined( $tracknum ); $title =~ s/^\s*(\d{1,3})[\-\.\_ ]+//; } } elsif ( defined( $tracknum )) { if ( !defined( $title_override ) and $title =~ s/^[-_\. ]*0?$tracknum[-_\. ]+// ) { print " Removing track number from title\n" if $verbose; } } # Or maybe the filename? if ( !defined( $tracknum ) or $tracknum !~ /^\s*\d+\s*$/ ) { ( $tracknum, undef ) = basename( $oldname ) =~ m|^\s*(\d{1,3})[\-\.\_ ]+([^0-9,]+?)|; # waitasec if ( !$tracknum ) { undef( $tracknum ); } else { print " Found tracknum $tracknum in filename\n" if $verbose and defined( $tracknum ); } } # What about having it stuffed into the comment? if ( !defined( $tracknum ) or $tracknum !~ /^\s*\d+\s*$/ ) { $tracknum = $tag->{'COMMENT'}; $tracknum ||= ""; if ( ref $tracknum ) { $tracknum = $tag->{COMMENT}->[-1]; } $tracknum =~ s|^\s*(\d{1,3})[\-\.\_ ]+([^0-9,]+?)|$1|; } # Well, then! if ( !defined( $tracknum ) or $tracknum !~ /^\s*\d+\s*$/ ) { # Bugger that, then. FIXME CDDB can help! CDDB is definitive, I guess. $tracknum = 0; } # Do a CDDB lookup if ( $cddb ) { $cddb_file = ask_cddb( $tag->{'ARTIST'}||"", $album, $title, $tracknum ); } else { $cddb_file = undef; } $title ||= $oldname; # fallback $title =~ s/ +/ /; # hack # still taking liberties $title =~ s/_/ /g unless $itunes; # xxx # and now if ( $frobcase ) { $title =~ s/\b(\w)(\w+)\b/\U$1\E$2/g; $artist =~ s/\b(\w)(\w+)\b/\U$1\E$2/g; $album =~ s/\b(\w)(\w+)\b/\U$1\E$2/g; } print " After all that, the title is: $title\n" if $verbose; # finally, check for swaps... if ( $swap ) { my $tmp = $artist; $artist = $title; $title = $tmp; } # Fix the tagging $year = $tag->{'YEAR'} || ""; if ( defined( $year ) and ref $year ) { $year = $tag->{YEAR}->[-1]; } $comment = ""; # screw comments $genre = $tag->{'GENRE'} || ""; if ( defined( $genre ) and ref $genre ) { $genre = $tag->{GENRE}->[-1]; } $tracknum =~ s/ \- //; if ( $tracknum !~ /^\d+$/ || (( $tracknum < 1 ) || ( $tracknum > 255 ))) { $tracknum = ""; } if ( ! $dryrun and $mp3file !~ m|/discid$| and !$itunes ) { remove_mp3tag( $mp3file, 'ALL' ); my $id3 = new MP3::ID3Lib( $mp3file ); $id3->add_frame( "TIT2", $title ) if $title; $id3->add_frame( "TPE1", $artist ) if $artist; $id3->add_frame( "TALB", $album ) if $album; $id3->add_frame( "TYER", $year ) if $year; $id3->add_frame( "COMM", $comment ) if $comment; $id3->add_frame( "TCON", $genre ) if $genre; my $trck = $tracknum; if ( defined( $tracknum ) && defined( $tag->{TRACKMAX} )) { $trck = $tracknum . "/" . $tag->{TRACKMAX}; } $id3->add_frame( "TRCK", $trck ) if defined( $trck ); $id3->commit(); } elsif ( $itunes ) { print " iTunes mode: not retagging\n" if $verbose; } if ( $verbose ) { print " $title ($artist/$album)\n"; } # tweak for filesystem $artist =~ s/$allow_these_chars//ig; $title =~ s/$allow_these_chars//ig; $album =~ s/$allow_these_chars//ig; # cleanup if ( $mp3file =~ m|/discid$| ) { $title = "discid"; $ext = ""; } else { if ( !$force and ( !$artist or !$album or !$title or !$tracknum )) { print " Can't rename $mp3file\n"; print " - missing artist\n" if !$artist; print " - missing title\n" if !$title; print " - missing album\n" if !$album; print " - missing tracknum\n" if !$tracknum; next; } } $tracknum = sprintf( "%02d", $tracknum ) if $tracknum =~ /^\d+$/; $artist =~ s/^$/Unknown Artist/g; $album =~ s/^$/Unknown Album/g; $title =~ s/^$/Unknown Track/g; # tweak tracknum to include disc if $tag->{TPOS} / $tag->{TPA} if ( $tag->{TPA} and $tag->{TPA} ne "1/1" ) { my ( $tpos ) = $tag->{TPA} =~ /^(\d+)/; $tracknum = $tpos . "-$tracknum"; } $title_with_tracknum = $title; if ( $tracknum =~ /^\d+$/ && $tracknum > 0 ) { $title_with_tracknum = sprintf( "%02d - %s", $tracknum, $title ); } # ok, let's make this iso8859-1 (finally finally) if ( $itunes ) { from_to( $artist, "iso-8859-1", "unicode" ); from_to( $album, "iso-8859-1", "unicode" ); from_to( $title, "iso-8859-1", "unicode" ); } else { utf8::downgrade( $artist ); utf8::downgrade( $album ); utf8::downgrade( $title ); } # feck if ( $itunes ) { $artist =~ s@/@_@g; $album =~ s@/@_@g; $title =~ s@/@_@g; $title_with_tracknum =~ s@/@_@g; } if ( $tag->{TCMP} and $itunes ) { print " part of a compilation\n" if $verbose; $artist = 'Compilations'; # xxx } # use mp3fileformat my $newname = $mp3fileformat; $newname =~ s/%{a}/$artist/g; $newname =~ s/%{d}/$album/g; $newname =~ s/%{t}/$tracknum/g; $newname =~ s/%{x}/$ext/g; my $newname_with_tracknum = $newname; $newname =~ s/%{n}/$title/g; $newname_with_tracknum =~ s/%{n}/$title_with_tracknum/g; if ( $itunes ) { $newname =~ s/[\*":\?]/_/g; $newname_with_tracknum =~ s/[\*":\?]/_/g; # WEIRD. $newname =~ s@(^|/)\.@/_@g; $newname_with_tracknum =~ s@\./@_/@g; $newname =~ s@/(^|/)\.@/_@g; $newname_with_tracknum =~ s@\./@_/@g; } if (($mp3file ne $newname_with_tracknum ) and ( $newname ne $newname_with_tracknum ) and -f "$newname_with_tracknum" and compare_mp3_files( $mp3file, $newname_with_tracknum )) { my @file1 = stat( $mp3file ); my @file2 = stat( $newname_with_tracknum ); if ( $file1[0] != $file2[0] or $file1[1] != $file2[1] ) { print "(1) Identical files, nuking second one:\n> $mp3file\n> $newname_with_tracknum\n"; #unlink "$newname_with_tracknum" unless $dryrun; } } # Check new name against existing files AND old name if ( -f "$newname" ) { if (( $mp3file eq "$newname" ) || ( $mp3file eq "./$newname" )) { # nothin' doin' print " File is already in place\n" if $verbose; } else { # Namespace collision. Check if the files are identical. if ( compare_mp3_files( $mp3file, $newname )) { my @file1 = stat( $mp3file ); my @file2 = stat( $newname ); if ( $file1[0] != $file2[0] or $file1[1] != $file2[1] ) { print "(2) Identical files, nuking first one:\n> $mp3file\n> $newname\n"; #unlink "$mp3file" unless $dryrun; # also, see if the old directory is empty, and clean it up if it is my ( $pdir ) = $mp3file =~ m|^(.*)/[^/]+$|; if ( defined( $pdir )) { rmdir $pdir; ( $pdir ) = $pdir =~ m|^(.*)/[^/]+$|; if ( defined( $pdir )) { rmdir $pdir if defined( $pdir ); } } } else { print " File is already in place at $newname\n" if $verbose; } } else { print "Namespace collision:\n> $mp3file\n> $newname\n"; # check file quality my $info1 = get_mp3info( $mp3file ); my $info2 = get_mp3info( $newname ); if ( defined( $info1 ) and defined( $info2 )) { printf( "$mp3file: %dKHz %d bits %ds\n", $info1->{FREQUENCY}, $info1->{BITRATE}, $info1->{SECS} ); printf( "$newname: %dKHz %d bits %ds\n", $info2->{FREQUENCY}, $info2->{BITRATE}, $info2->{SECS} ); } } } } else { print "$mp3file -> $newname\n"; if ( !$dryrun ) { mkpath( [ dirname $newname] , 0, 0755 ); if ( move( "$mp3file", "$newname" )) { # Clean up permissions, since sometimes they're bogued out chmod 0644, "$newname"; # Tell XMMS what we've done, if need be my ( $old, $new ) = ( $mp3file, $newname ); $old = getcwd() . "/$old" unless $old =~ m@^/@; $new = getcwd() . "/$new" unless $new =~ m@^/@; system( "xmms-frob.pl", "-start", "-mode=rename", $old, $new ) if $doxmms; # also, see if the old directory is empty, and clean it up if it is my ( $pdir ) = $mp3file =~ m|^(.*)/[^/]+$|; if ( defined( $pdir )) { rmdir $pdir; ( $pdir ) = $pdir =~ m|^(.*)/[^/]+$|; if ( defined( $pdir )) { rmdir $pdir if defined( $pdir ); } } } else { print "$mp3file: rename failed: $!\n"; } } } print "\n" if $verbose; } # Jan 2003: Complete rewrite to use FreeDB instead. Go FreeDB! sub ask_cddb { my ( $tmpartist, $tmpalbum, $tmptitle, $tmptracknum ) = @_; my ( @results ); my %diskinfo; my @qbits; $tmpartist =~ s/_/ /g; $tmpalbum =~ s/_/ /g; if ( $tmpartist eq "Unknown Artist" ) { $tmpartist = ""; } else { push @qbits, $tmpartist; } if ( $tmpalbum eq "Unknown Album" ) { $tmpalbum = ""; } else { push @qbits, $tmpalbum; } $tmptitle =~ s/_/ /g; # what fules you are. push @qbits, $tmptitle; my $querystring = join( " / ", @qbits ); $querystring =~ s/\(/%28/g; $querystring =~ s/\)/%29/g; $querystring =~ uri_escape( $querystring ); $querystring .= "&field=artist" if $tmpartist; $querystring .= "&field=title" if $tmpalbum; $querystring .= "&field=track" if $tmptitle; my $ua = new LWP::UserAgent; $ua->env_proxy(); my $req = new HTTP::Request GET => "http://www.freedb.org/freedb_search.php?words=$querystring"; print "Asking FreeDB about $querystring: " if $verbose; my $res = $ua->request( $req ); if ( $res->is_success ) { my $content = $res->content; print $content; die; } else { print "HTTP error.\nanalyze this:\n"; print "-" x 79, "\n", $res->content, "\n", "-" x 79, "\n"; } return $results[ 0 ]; } # Horrifically compare the actual mp3 data in two mp3 files. # by side effect, use far too much disk space. sub compare_mp3_files { my ( $file1, $file2 ) = @_; my ( $name1, $name2 ) = ( "tmp1", "tmp2" );# xxx generate # 1. duplicate both files $debug and print STDERR "copying $name1\n"; copy( $file1, "/tmp/$name1" ) || return 0; $debug and print STDERR "copying $name2\n"; copy( $file2, "/tmp/$name2" ) || return 0; # 2. Strip any mp3 info out of both files for my $file ( $name1, $name2 ) { remove_mp3tag( "/tmp/$file", 'ALL' ); } # 3. Compare files $debug and print STDERR "comparing $name1 and $name2\n"; my $result = `cmp "/tmp/$name1" "/tmp/$name2"`; $debug and print STDERR $result . "\n"; # 4. Clean up. !$debug and unlink( "/tmp/$name1" ); !$debug and unlink( "/tmp/$name2" ); return $result eq ""; } # Remove leading & trailing space from a string. sub trim { my ( $string ) = @_; $string =~ s/^\s+//; $string =~ s/\s+$//; $string; } # the following chunk of code comes more or less verbatim from # # Gronk, Copyright (c) 2000 by Jamie Zawinski # # Permission to use, copy, modify, distribute, and sell this software and its # documentation for any purpose is hereby granted without fee, provided that # the above copyright notice appear in all copies and that both that # copyright notice and this permission notice appear in supporting # documentation. No representations are made about the suitability of this # software for any purpose. It is provided "as is" without express or # implied warranty. sub capitalize { my ($s) = @_; $s =~ s/_/ /g; # capitalize words, from the perl faq... $s =~ s/((^\w)|(\s\w))/\U$1/xg; $s =~ s/([\w\']+)/\u\L$1/g; return $s; } sub cddb_sum { # a number like 2344 becomes 2+3+4+4 (13). my ($n) = @_; my $ret = 0; while ($n > 0) { $ret += ($n % 10); $n /= 10; } return $ret; } sub compute_discid { my @frames = @_; my $tracks = $#frames + 1; my $n = 0; my @start_secs; my $i; for ($i = 0; $i < $tracks; $i++) { $start_secs[$i] = POSIX::floor ($frames[$i] / 75); } for ($i = 0; $i < $tracks-1; $i++) { $n = $n + cddb_sum ($start_secs[$i]); } my $t = $start_secs[$tracks-1] - $start_secs[0]; my $id = ((($n % 0xFF) << 24) | ($t << 8) | $tracks-1); return sprintf ("%08x", $id); } sub handle_directory { my ( $artist, $album, $genre, @songs ) = @_; my @frames; my $leader = 150; # typical leader value my $i = 0; foreach my $song (@songs) { # $secs[ $i ] = 0; $frames[ $i ] = 0; # $secs[$i] = `mp3info -f '%S' $dir/$song`; # $frames[$i] = POSIX::ceil (($secs[$i] + 0.5) * 75); $i++; } print "# xmcd CD database file generated by mp3name.pl\n"; print "# \n"; print "# Track frame offsets:\n"; my $total = $leader; for ($i = 0; $i <= $#songs; $i++) { print "# $total\n"; $total += $frames[$i]; } print "# \n"; print "# Disc length: " . POSIX::floor($total / 75) . " seconds\n"; print "# \n"; print "# Revision: 3\n"; print "# Submitted via: mp3name\n"; print "# \n"; print "# WARNING: These track offsets and this discid are fiction.\n"; print "# These numbers are a guess, based on existing MP3\n"; print "# files, not based on data from an actual Compact Disc.\n"; print "# \n"; print "# \n"; print "DISCID="; print compute_discid ($leader, @frames); print "\n"; # screw capitalisation. if cddb fucks up, fine. # if ($dir =~ m@([^/]+)/([^/]+)$@) { # $artist = capitalize($1); # $album = capitalize($2); # } elsif ($dir =~ m@([^/]+)$@) { # $album = capitalize($1); # } print "DTITLE=$artist / $album\n"; print "DGENRE=$genre\n"; for ($i = 0; $i <= $#songs; $i++) { print "TTITLE$i="; $_ = $songs[$i]; $_ = capitalize($_); print "$_\n"; } print "EXTD=\n"; for ($i = 0; $i <= $#songs; $i++) { print "EXTT$i=\n"; } print "PLAYORDER=\n"; } sub get_file_type { my $mp3file = shift; print " getting filetype..." if $verbose; my $pid = open( my $FILE, "-|" ); die $! if !defined( $pid ); if ( !$pid ) { open STDERR, ">&", STDOUT; exec( "/usr/bin/file", "-b", "-I", $mp3file ); } my $type = <$FILE>; waitpid( $pid, 0 ); close( $FILE ); $type =~ s/; charset=[^ ]+//; $type = trim( $type ); print "$type\n" if $verbose; $type; } sub get_tcmp { my $mp3file = shift; print " getting TCMP..." if $verbose; my $pid = open( my $FILE, "-|" ); die $! if !defined( $pid ); if ( !$pid ) { open STDERR, ">&", STDOUT; exec( "/sw/bin/id3info", $mp3file ); } my $tcmp = grep /=== TCM?P\b/, <$FILE>; waitpid( $pid, 0 ); close( $FILE ); print $tcmp . "\n" if $verbose; $tcmp; }