#!/usr/bin/perl # mp3cddb # ------- # complete rewrite by: Peter Nelson # # originally: mp3tocddb.pl by Meng Weng Wong (MP3::Info) # modified by: # some code from: tagit.pl by Thomas Geffert (MP3::Tag) # # This program is free software; you can redistribute it # and/or modify it under the terms of the GNU General Public # License as published by the Free Software Foundation; either # version 2 of the License, or (at your option) any later # version. # # This program is distributed in the hope that it will be # useful, but WITHOUT ANY WARRANTY; without even the implied # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR # PURPOSE. # See the GNU General Public License for more details. # You should have received a copy of the GNU General Public # License (GPL) along with this program. if not, write to the # Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # use strict; use MP3::Info; use MP3::Tag; use CDDB; use File::Basename; use Cwd qw(abs_path cwd); use Term::ReadLine; use File::Path; my $term = new Term::ReadLine 'mp3cddb'; unless ($term->Features->{preput}) { print q~Error: mp3cddb requires a full featured Term::ReadLine. Either the ::Gnu or ::Perl versions should work ~; exit; } if ($ARGV[0] eq "-h" || $ARGV[0] eq "--help") { print q~ mp3cddb Version 1.0 performs cddb lookups and renaming of mp3s using directories as albums usage: mp3cddb [-h|--help] [-g|--genres] [-n|--nocddb] [-nr|--norename [directory [directory]] --help: print this help message --genres: print out a list of all accepted ID3v1 genres --nocddb: do not try to do any cddb lookup. Useful if the id3 tags are more accurate than the FreeDB data. --norename: do not rename any files, only set ID3 tags directory: a directory containing mp3s that are numerically in track order. if no directory is provided, the current working directory is used. ~; exit; } if ($ARGV[0] eq "-g" || $ARGV[0] eq "--genres") { print "Accepted ID3v1 Genres:\n\n"; print join("\n", sort @{MP3::Tag::ID3v1::genres()}) . "\n"; exit; } my $nocddb = 0; if($ARGV[0] eq "-n" || $ARGV[0] eq "--nocddb") { $nocddb = 1; shift @ARGV; } my $norename = 0; if($ARGV[0] eq "-nr" || $ARGV[0] eq "--norename") { $norename = 1; shift @ARGV; } my %Config; if (open(CONFIG, $ENV{"HOME"} . "/.mp3cddb")) { while () { next if (/^(\#.*|\s*)$/); chomp; my ($key, $val) = split(/\s+=\s+/, $_, 2); if (defined $Config{$key}) { print "Error: you have multiple $key declirations in ~/.mp3cddb\n"; exit; } $Config{$key} = $val; } close(CONFIG); if (!$Config{"RENAME"} && !$Config{"DONTRENAME"}) { print "You seem to want me to rename files, yet you don't tell me how to...\n" . "I might be smart, but I can't read your mind..only ~/.mp3cddb\n"; exit; } if($Config{"RENAME"}){ &check_config_rename($Config{"RENAME"}); } if($Config{"RENAMEVA"}){ &check_config_rename($Config{"RENAMEVA"}); } # thanks to irc://irc.openprojects.net/#perl ! # rlandrum: if($var =~ /\{([^\}]+)\}/ && (@x = ($1 =~ m/\%a[aty]/g)) && @x == 1) { # xsdg: perl -e '$txt = "%a - {%b -}%c"; if($txt =~ m#^[^}]* ([^{]* \{ [^}]* \%[^}]+ \})+ [^{}]*$#x){print "moo"}' # xsdg: moo } else { print "you don't seem to have a $ENV{'HOME'}/.mp3cddb config file.\n" . "I am now writing an example one there, be sure to read it and change as needed!\n"; &write_config; exit; } if (! @ARGV) { push(@ARGV, cwd); } # global hash of what we ended up doing my %result; # global array so that we can re-display errors (in case they get missed) my @errors; my $cddb = new CDDB (Debug=>0, Protocol_Version=>5) or die "unable to connect to CDDB: $!"; # having the complete absolute paths is a GOOD THING my @albums = map { abs_path($_) } @ARGV; ALBUM: foreach my $album (@albums) { next ALBUM unless (-d $album); my @cdtoc; my @cddb_query; my @tracks_in; my %disc_info; print "Working on $album\n"; # wow you really have to quote the hell out of stuff for glob my $quoted = $album; $quoted =~ s/([\[\]])/\\$1/g; foreach my $file (sort { $a=~m!^.*/[^/\d]*(\d+)[^/]+$!; my $c=$1; $b=~m!^.*/[^/\d]*(\d+)[^/]+$!; my $d=$1; $c <=> $d } glob quotemeta($quoted) . "/*.[Mm][Pp]3") { my $info = get_mp3info($file); push (@cdtoc, $info ); push (@tracks_in, basename($file)); } unless ($#tracks_in >= 1) { &do_error("There are 1 or fewer mp3s in '$album'", $album); next ALBUM; } my ($my_disc_id, $my_total_tracks, $my_total_time, @my_frames) = &build_cddb_query(@cdtoc); print "searching for discid: $my_disc_id, total tracks: $my_total_tracks, total time: ". int($my_total_time/60).":".($my_total_time%60)."\n"; my @discs; unless($nocddb) { @discs = $cddb->get_discs($my_disc_id, [@my_frames], $my_total_time); } my $newblank = 0; if (! @discs) { print q~ Disk was not found. Enter a DiscID then Genre (from freedb.org) to use, a url from freedb.org with the DiscID and Genre in it, `blank' to create a blank record, `id3' to aquire data form ID3 tags as a single-artist album, `id3va' to aquire artists from ID3 tags as a various-artist album, or leave empty to skip. ~; my $new_disc_id = $term->readline("DiscID: "); if ($new_disc_id eq "blank") { $newblank = 1; } elsif ($new_disc_id eq "id3") { $newblank = 2; } elsif ($new_disc_id eq "id3va") { $newblank = 3; } elsif (($new_disc_id =~ /^[a-f0-9]{8}$/i) && ((my $new_genre = $term->readline("Genre: ")) ne "")) { @{$discs[0]} = ($new_genre, $new_disc_id, ""); } elsif($new_disc_id =~ /cat=(.+?)\&id=([a-f0-9]{8})/i) { @{$discs[0]} = ($1, $2, ""); } else { &do_error("Skipped directory '$album'", $album); next ALBUM; } } my @ranking; if (!$newblank) { $|=1; # unbuffer output...buffer craps up the status output foreach my $disc (@discs) { print "."; # nice status if there are lots of CD's my ($genre, $cddb_id, $title) = @$disc; $disc_info{"$cddb_id ($genre)"} = $cddb->get_disc_details($genre, $cddb_id); unless($disc_info{"$cddb_id ($genre)"}) { &do_error("Could not find $cddb_id ($genre) for '$album'", $album); next ALBUM; } $disc_info{"$cddb_id ($genre)"}->{'dgenre'} ||= $genre; } print "\n"; # end status stuff $|=0; # can buffer again push (@my_frames, $my_total_time * 75); my @my_lengths = &offsets_to_seconds (@my_frames); my %distance; foreach my $cddb_id (sort keys %disc_info) { my $disc_time = ($disc_info{$cddb_id}->{'disc length'} =~ /(\d+)/)[0]; my $disc_id = $disc_info{$cddb_id}->{'discid'}; my @track_offsets = @{$disc_info{$cddb_id}->{'offsets'}}; push (@track_offsets, $disc_time * 75 + $track_offsets[0]); my @track_lengths = &offsets_to_seconds (@track_offsets); $distance{$cddb_id} = &sqr_distance (\@track_lengths, \@my_lengths); } @ranking = sort { $distance{$a} <=> $distance{$b} } keys %distance; } elsif($newblank == 1) { $disc_info{"new"}->{'dtitle'} = ""; foreach my $i (0..$#tracks_in) { $disc_info{"new"}->{'ttitles'}[$i] = ""; } @ranking = ("new"); } elsif($newblank == 2 || $newblank == 3) { $disc_info{"new"} = get_id3tags($album, ($newblank==3), @tracks_in); @ranking = ("new"); } if (my $choice = &prompt_best ($album, \@ranking, \%disc_info, \@tracks_in)) { &tag_files ($album, $disc_info{$choice}, \@tracks_in); $result{$album} = "tagged"; if ($Config{'DORENAME'} && !$norename) { if (&do_rename ($album, $disc_info{$choice}, \@tracks_in)) { $result{$album} .= " and renamed"; } } } else { print "skipping $album, moving on\n\n\n"; $result{$album} = "skipped"; } } print "Summary of actions:\n"; foreach my $album ( sort keys %result ) { print "$result{$album}\t $album\n"; } if (@errors) { print "\n\nThere seem to have been some errors:\n\n"; print "$_\n\n" foreach @errors; } exit; #################### # # # My subroutines # # # # influenced by # # Thomas Geffert's # # tagit.pl # #################### sub get_id3tags { my ($album, $va, @tracks_in) = @_; my $info; my $mp3 = MP3::Tag->new($album."/".$tracks_in[0]); my @temp = $mp3->autoinfo(); $info->{'dtitle'} = $temp[2]." / ".$temp[3]; $info->{'dyear'} = $temp[5]; $info->{'dgenre'} = $temp[6]; $info->{'comment'} = $temp[4]; $mp3->close(); foreach my $i (0..$#tracks_in) { my $mp3 = MP3::Tag->new($album."/".$tracks_in[$i]); my @temp = $mp3->autoinfo(); if($va) { $info->{'ttitles'}[$i] = $temp[2]." / ".$temp[0]; } else { $info->{'ttitles'}[$i] = $temp[0]; } $mp3->close(); } return $info; } # prints the error and adds it to the global array of errors # if $album is defined, it's a fatal error for that album and $result is updated as such sub do_error { my ($error, $album) = @_; print "$error\n\n\n"; push(@errors, $error); $result{$album} = "error"; } sub check_config_rename { my $rename=shift; unless ($rename =~ m! ^(?: # each start can be anything not special [^{}%]* # test to make sure {}'s match and has only 1 proper %tag (?: \{ [^{}%]* \% (?:a[tagy]|t[abnt]) [^{}%]* \} | # or test just proper %tag not in {}'s \% (?:a[tagy]|t[abnt]) ) )+ # end can be anything not special [^{}%]* $ !oix) # had enough RegEx fun for today? =) { print qq!You seem to have a bad RENAME[VA] line in ~/.mp3cddb. Please check: You have matching {}\'s You have only 1 %tag inside of each {} You are using the apropriate tags (%at, %ta, etc) !; exit; } if ($rename =~ m/\%t[abnt].*\//i) { print q!RENAME[VA] contains track-specific data (ta, tb, tn, tt) inside of a directory name. This will break the renaming, please limit directory names to whole-album data !; exit; } } sub do_rename { my ($album, $info, @tracks) = (shift, shift, @{+shift}); my ($new_dir, @new_files) = &new_names ($album, $info, \@tracks); if (&make_path($new_dir, $album)) { foreach my $num (0 .. $#tracks) { unless( rename "$new_dir/$tracks[$num]", "$new_dir/$new_files[$num]" ) { &do_error("Couldn't move '$album/$tracks[$num]' to\n'$new_dir/$new_files[$num]'\nReason: $!", $album); return undef; } } } else { #make_path errored or something return undef; } return 1; } sub make_path { my ($new_path, $old_path) = @_; return 1 if $new_path eq $old_path; mkpath(dirname($new_path), 0, 755); if ( -d $new_path ) { # $new_path already exists, lets move everything into it and get rid of $old_path my $album_glob = $old_path; $album_glob =~ s/(\s)/\\$1/oig; foreach my $file (glob("$album_glob/*")) { unless( rename $file, "$new_path/" . basename($file) ) { &do_error("Couldn't move '$file' to '$new_path/" . basename($file) . "'\nReason: $!", $old_path); return undef; } } if (glob("$album_glob/*")) { &do_error("There seems to be something left in '$old_path', yet it should be empty"); } else { unless( rmdir($old_path) ) { &do_error("Couldn't remove empty '$old_path'\nReason: $!"); } } } else { # $new_path doesn't exist, so we can just rename old to new and be done unless( rename $old_path, $new_path ) { &do_error("Couldn't move '$old_path' to '$new_path'\nReason: $!", $old_path); return undef; } } return 1; } # For my own reference # %at: Album Title # %aa: Album Artist # %ag: Album Genre # %ay: Album Year # %ac: Album Comment # %tn: Track Number (padded to 2 characters, 01..10..99) # %tt: Track Title # %ta: Track Artist (always set) # %tb: Track Artist (only set if different from Album) # {.*(%..).*}: optional block # %%, %{, %}: replaced by %, {, and } sub new_names { my ($album, $info, @tracks) = (shift, shift, @{+shift}); my @renamed; my %tag_info; # for %%, %{, and %} $tag_info{'%'} = '%'; $tag_info{'{'} = '{'; $tag_info{'}'} = '}'; # get whole-album vars ($tag_info{'aa'}, $tag_info{'at'}) = &split_title($info->{'dtitle'}); $tag_info{'ag'} = $info->{'dgenre'}; $tag_info{'ay'} = $info->{'dyear'}; $tag_info{'ac'} = $info->{'comment'}; my ($new_dir, $new_file); if($info->{'NoVA'} || !$Config{'RENAMEVA'}){ $Config{'RENAME'} =~ /^(.*?)(?:\/)?([^\/]+)$/o; ($new_dir, $new_file) = ($1, $2); } else { $Config{'RENAMEVA'} =~ /^(.*?)(?:\/)?([^\/]+)$/o; ($new_dir, $new_file) = ($1, $2); } if ($new_dir) { foreach my $key (keys %tag_info) { $tag_info{$key} =~ tr!\/\\\:\?\"|\*!-!; $tag_info{$key} =~ tr!<>![]!; $tag_info{$key} =~ tr! !_! if $Config{'UNDERSCORE'}; } # only album stuff (a[tagy]) is allowed in the dir name $new_dir =~ s! (?: \{ ([^%{}]*) \% (a[tagy]|[%{}]) ([^%{}]*) \} | ([^%{}]*) \% (a[tagy]|[%{}]) ([^%{}]*) ) ! ( $5 ? $4.$tag_info{$5}.$6 : ( $tag_info{$2} ? $1.$tag_info{$2}.$3 : '' ) )!xoge; # if it's complete (starts with /) it's fine, otherwise complete it from $album unless($new_dir =~ /^\//o) { $new_dir = dirname($album) . "/" . $new_dir; } } else { # if there's no new directory in the config, just use $album $new_dir = $album; } foreach my $tNumber (0 .. $#tracks) { $tag_info{'tn'} = sprintf("%02u", $tNumber + 1); if($info->{'NoVA'}) { ($tag_info{'tb'}, $tag_info{'tt'}) = (undef, $info->{'ttitles'}[$tNumber]); } else { ($tag_info{'tb'}, $tag_info{'tt'}) = &split_title($info->{'ttitles'}[$tNumber]); } # ta is always set, tb sometimes (VA cds) $tag_info{'ta'} = $tag_info{'tb'} || $tag_info{'aa'}; # we have to do this each time to hit the new track info foreach my $key (keys %tag_info) { $tag_info{$key} =~ tr!\/\\\:\?\"|\*!-!; $tag_info{$key} =~ tr!<>![]!; $tag_info{$key} =~ tr! !_! if $Config{'UNDERSCORE'}; } my $newname = $new_file; unless($newname =~ /\.\S{1,3}$/o) { $newname .= ".mp3"; } $newname =~ s! (?: \{ ([^%{}]*) \% (a[tagy]|t[abnt]|[%{}]) ([^%{}]*) \} | ([^%{}]*) \% (a[tagy]|t[abnt]|[%{}]) ([^%{}]*) ) ! ( $5 ? $4.$tag_info{$5}.$6 : ( $tag_info{$2} ? $1.$tag_info{$2}.$3 : '' ) )!xoge; push(@renamed, $newname); } return $new_dir, @renamed; } sub tag_files { my ($album, $info, @tracks) = (shift, shift, @{+shift}); my ($aArtist, $aTitle) = &split_title($info->{'dtitle'}); foreach my $tNumber (0 .. $#tracks) { my ($tArtist, $tTitle); if($info->{'NoVA'}) { $tTitle = $info->{'ttitles'}[$tNumber]; } else { ($tArtist, $tTitle) = &split_title($info->{'ttitles'}[$tNumber]); } # if there's a track artist, use it...otherwise use album artist $tArtist ||= $aArtist; my $mp3 = MP3::Tag->new($album . "/" . $tracks[$tNumber]); $mp3->get_tags(); if ($Config{"DELID3V1"} && exists $mp3->{ID3v1}) { $mp3->{ID3v1}->remove_tag(); } if ($Config{"DELID3V2"} && exists $mp3->{ID3v2}) { $mp3->{ID3v2}->remove_tag(); } if ($Config{"ID3V1"}) { # remove_tag on ID3v1 doesn't actually remove it, just blanks it.. #so we don't have to worry about recreating it $mp3->new_tag("ID3v1") unless exists $mp3->{ID3v1}; $mp3->{ID3v1}->song($tTitle); $mp3->{ID3v1}->artist($tArtist); $mp3->{ID3v1}->album($aTitle); $mp3->{ID3v1}->comment($info->{'comment'}) if $info->{'comment'}; $mp3->{ID3v1}->year($info->{'dyear'}) if $info->{'dyear'}; $mp3->{ID3v1}->genre($info->{'dgenre'}) if $info->{'dgenre'}; $mp3->{ID3v1}->track($tNumber + 1); $mp3->{ID3v1}->write_tag(); } if ($Config{"ID3V2"}) { # remove_tag on ID3v2 actually destorys the tag, so we have to re-create it when we delete it $mp3->new_tag("ID3v2") unless exists $mp3->{ID3v2} && !$Config{"DELID3V2"}; my $frameID = $mp3->{ID3v2}->get_frame_ids; # remove frame (does nothing if it doesn't exist) and then add new one $mp3->{ID3v2}->remove_frame("TIT2"); $mp3->{ID3v2}->add_frame("TIT2", $tTitle); # artist could be either TPE1 or TPE2, so nuke both $mp3->{ID3v2}->remove_frame("TPE1"); $mp3->{ID3v2}->remove_frame("TPE1"); $mp3->{ID3v2}->add_frame("TPE1", $tArtist); $mp3->{ID3v2}->remove_frame("TALB"); $mp3->{ID3v2}->add_frame("TALB", $aTitle); $mp3->{ID3v2}->remove_frame("COMM"); $mp3->{ID3v2}->add_frame("COMM", "ENG", "Comment", $info->{'comment'}) if $info->{'comment'}; $mp3->{ID3v2}->remove_frame("TYER"); $mp3->{ID3v2}->add_frame("TYER", $info->{'dyear'}) if $info->{'dyear'}; $mp3->{ID3v2}->remove_frame("TCON"); $mp3->{ID3v2}->add_frame("TCON", $info->{'dgenre'}) if $info->{'dgenre'}; $mp3->{ID3v2}->remove_frame("TRCK"); $mp3->{ID3v2}->add_frame("TRCK", ($tNumber + 1) ); $mp3->{ID3v2}->write_tag(); } } } sub prompt_best { my $album = shift; my @ranking = @{+shift}; my %disc_info = %{+shift}; my @tracks_in = @{+shift}; my $current = $ranking[0]; SELECT: while (1) { if ($#ranking) { print "\nMore than one possible matches are available.\n" . "The following is a list of all possible ones, sorted by my best guess:\n"; foreach my $choice_num (0 .. $#ranking) { print join("\t", '', $choice_num + 1, $disc_info{$ranking[$choice_num]}->{'dgenre'}, $disc_info{$ranking[$choice_num]}->{'dtitle'}, $disc_info{$ranking[$choice_num]}->{'discid'}) . "\n"; } print "\nThis is my best guess or your current selection:\n\n"; } else { print "\nHere is the only available match:\n\n"; } my $var_artist = &print_info($disc_info{$current}); if($var_artist) {$disc_info{$current}->{'NoVA'} = 0;} else {$disc_info{$current}->{'NoVA'} = 1;} while (1) { print "\nChoices: use this match (Y), "; print "view a different match (1-" . ($#ranking + 1) . "), " if $#ranking; print "edit album info (e), edit track listing (t), "; print "show example renamed filenames (s), " if $Config{'DORENAME'}; print "or disregard this album entirely (n)\n"; print "This appears to be a Various Artist CD, select (a) if the artist (1st) " . "and track name (2nd) to be switched or (v) if this really isn't a VA cd.\n" if $var_artist; my $key = $term->readline("? "); if ($key =~ /^y$/oi || $key eq "") { return $current; } elsif (1 <= $key && $key <= ($#ranking + 1)) { $current = $ranking[$key - 1]; next SELECT; } elsif ($key =~ /^n$/oi) { return undef; } elsif ($key =~ /^e$/oi) { &edit_disc ($disc_info{$current}); next SELECT; } elsif ($key =~ /^t$/oi) { &edit_track ($disc_info{$current}); next SELECT; } elsif ($key =~ /^s$/oi) { my($new_dir, @renamed) = &new_names ($album, $disc_info{$current}, \@tracks_in); foreach my $file (@renamed) { print "$new_dir/$file\n"; } } elsif ($var_artist && $key =~ /^a$/oi) { &swap_titles ($disc_info{$current}); next SELECT; } elsif ($var_artist && $key =~ /^v$/oi) { $disc_info{$current}->{'NoVA'} = 1; next SELECT; } } } } sub edit_disc { my ($info) = @_; my ($artist, $real_title) = &split_title($info->{'dtitle'}); my ($input1, $input2); $input1 = $term->readline("Artist: ",$artist); $input2 = $term->readline("Title: ",$real_title); $info->{'dtitle'} = $input1 . " / " . $input2; print "Make sure this matches the --genres list to be able to write to ID3V1 tags\n"; $info->{'dgenre'} = $term->readline("Genre: ",$info->{'dgenre'}); $info->{'dyear'} = $term->readline("Year: ",$info->{'dyear'}); $info->{'comment'} = $term->readline("Comment ", $info->{'comment'}); } sub edit_track { my ($info) = @_; foreach my $track_number (0 .. $#{$info->{'ttitles'}}) { my ($track_artist, $track_title); if($info->{'NoVA'}) { $track_title = $info->{'ttitles'}[$track_number]; } else { ($track_artist, $track_title) = &split_title($info->{'ttitles'}[$track_number]); } if ($track_artist || $info->{'prompt'}) { $track_artist = $term->readline("Track " . ($track_number + 1) .". Artist ", $track_artist); } $track_title = $term->readline("Track " . ($track_number + 1) .". Title ", $track_title); $info->{'ttitles'}[$track_number] = ($track_artist?"$track_artist / " :'') . $track_title; } } sub print_info { my ($info) = @_; my ($artist, $real_title) = &split_title($info->{'dtitle'}); my $var_artist = 0; print "Artist:\t$artist\nTitle:\t$real_title\n"; print "Year:\t$info->{'dyear'}\n" if $info->{'dyear'}; print "Genre:\t$info->{'dgenre'}\n" if $info->{'dgenre'}; print "Comment:\t$info->{'comment'}\n" if $info->{'comment'}; my @track_titles = @{$info->{'ttitles'}}; foreach my $track_number (0 .. $#track_titles) { my ($artist, $title); if($info->{'NoVA'}) { $title = $track_titles[$track_number]; } else { ($artist, $title) = &split_title($track_titles[$track_number]); } printf "%2d. %-40s%-s\n", ($track_number + 1), ($artist?$artist:$title), ($artist?$title:''); $var_artist = 1 if $artist; } return $var_artist; } sub swap_titles { my ($info) = @_; foreach my $track_number (0 .. $#{$info->{'ttitles'}}) { my ($artist, $title) = &split_title($info->{'ttitles'}[$track_number]); $info->{'ttitles'}[$track_number] = "$title / $artist" if $artist; } } sub split_title { local $_ = shift; my ($artist, $title); if (/(.*?)\s*\/\s*(.*)/) { ($artist, $title) = ($1, $2) } elsif (/(.*?)\s+-+\s+(.*)/) { ($artist, $title) = ($1, $2) } else { ($artist, $title) = (undef, $_) } for ($artist, $title) { s/^\s*//; s/\s*$// } return ($artist, $title); } ####################### # # # CDDB Lookup Stuff # # # # by Meng Weng Wong # ####################### sub sqr_distance { my @vector1 = @{+shift}; my @vector2 = @{+shift}; my $total = 0; foreach my $dimension (0 .. ($#vector1 < $#vector2 ? $#vector1 : $#vector2)) # too much paranoia never hurt anyone { my $difference = abs($vector1[$dimension] - $vector2[$dimension]); my $square = $difference ** 2; $total += $square; } return $total; } sub frames_to_ss { my $frames = shift; my $ss = int($frames / 75); return $ss; } sub ss_to_mmss { my $ss = shift; my $mm = $ss / 60; $ss = $ss % 60; return sprintf ("%02d:%02d", $mm, $ss); } sub offsets_to_seconds { my @offsets = @_; my @track_lengths = (); while (@offsets > 1) { unshift(@track_lengths, pop (@offsets) - $offsets[-1]); } return map { &frames_to_ss ($_) } @track_lengths; } sub build_cddb_query { my @cdtoc = @_; my $count = 1; foreach (@cdtoc) { my ($mm, $ss) = ($_->{MM}, $_->{SS}); } my $discid = cddb_discid(@cdtoc); my @frames = &invent_frame_numbers(@cdtoc); my $total_time = &total_time(@cdtoc); my $total_tracks = @cdtoc; my $login = $ENV{USER}; my $hostname = &hostname; use Sys::Hostname; $hostname = `hostname` if $hostname !~ /\./; my $client_name = "mp3cddb"; my $client_version = "beta"; return ( $discid, $total_tracks, $total_time, @frames ); } sub cddb_sum { my ($n, $ret) = (shift, 0); for (split //, $n) { $ret += $_ } return $ret; } sub total_time { my @cdtoc = @_; my $total_time = 0; foreach my $track (@cdtoc) { my $track_time = $track->{MM} * 60 + $track->{SS}; $total_time += $track_time; } return $total_time; } sub cddb_discid { my @cdtoc = @_; my $n = 0; my $total_time = 0; foreach my $track (@cdtoc) { my $track_time = $track->{MM} * 60 + $track->{SS}; $n += &cddb_sum($total_time); $total_time += $track_time; } return sprintf("%08x", ($n % 0xFF) << 24 | $total_time << 8 | @cdtoc); } sub invent_frame_numbers { my @cdtoc = @_; my $n = 0; my $total_time = 0; foreach my $track (@cdtoc) { $track->{FRAME_OFFSET} = $total_time * 75; my $track_time = $track->{MM} * 60 + $track->{SS}; $total_time += $track_time; } return map { $_->{FRAME_OFFSET} } @cdtoc; } ################################### # # # Write the example config file # # # ################################### sub write_config{ open(WRITE, ">" . $ENV{"HOME"} . "/.mp3cddb") || die "couldn't write example config file!"; print WRITE q~# mp3cddb example configuration file # YOU MUST ENABLE ONE OF THE RENAME LINES OR ELSE THE PROGRAM WILL NOT RUN! # to enable one of the lines remove the # (comment) from before it, or add your own to the bottom # options are 1 for true, 0 for false # ID3V1 / ID3V2: write ID3 V1/2 tags ID3V1 = 1 ID3V2 = 1 # DELID3V1 / DELID3V2: delete any existing ID3 V1/2 # if both this and above are set, then it will remove any existing tag and write a new clean one DELID3V1 = 0 DELID3V2 = 0 # DORENAME: Rename the Files DORENAME = 1 # UNDERSCORE: convert all spaces to underscores in file names UNDERSCORE = 0 # RENAME: defines how to rename the files / directory # Available variables: # %at: Album Title # %aa: Album Artist # %ag: Album Genre # %ay: Album Year # %ac: Album Comment # %tn: Track Number (padded to 2 characters, 01..10..99) # %tt: Track Title # %ta: Track Artist (always set) # %tb: Track Artist (only set if different from Album) # In case you actually want a literal %, {, or } there are the following: # %%, %{, %}: replaced by %, {, and } # You can make a portion optional by enclosing in within {}, for example: # %aa - {%ay - }%at # Will write either "Artist - Title" or "Artist - Year - Title" # NOTE: do NOT put more than one variable within a single {}! # Here are a few examples, showing options and how directories are handled # READ THROUGH ALL OF THEM to get an idea of how exactly you want yours to be # hint: just look for the output that closest resembles your current output! # Each example shows the results on the following 2 files: # /mp3s/New Downloads/into the unknown-Bad_religion-aps/01-its_only_over_when-bad_religion-aps.mp3 # /mp3s/Ska/VA-Ska_Sucks/Ska Sucks- 01- Dance Hall Crashers- He Wants Me Back.mp3 # 1. Ignore directories: if RENAME has no directory info, only the files # inside a directory are renamed # Very simple #RENAME = %tn - %tt # /mp3s/New Downloads/into the unknown-Bad_religion-aps/01 - Its Only Over When.mp3 # /mp3s/Ska/VA-Ska_Sucks/01 - He Wants Me Back.mp3 # Show artist only when different from album #RENAME = %tn - {%tb - }%tt # /mp3s/New Downloads/into the unknown-Bad_religion-aps/01 - Its Only Over When.mp3 # /mp3s/Ska/VA-Ska_Sucks/01 - Dance Hall Crashers - He Wants Me Back.mp3 # Always shows artist #RENAME = %tn - %ta - %tt # /mp3s/New Downloads/into the unknown-Bad_religion-aps/01 - Bad Religion - Its Only Over When.mp3 # /mp3s/Ska/VA-Ska_Sucks/01 - Dance Hall Crashers - He Wants Me Back.mp3 # 2. Rename directory: if RENAME has some directory information, then then the # directory above the files will also be renamed. if RENAME contains more # than one directory, it will create the extra ones # Again, simple example: #RENAME = %aa - %at/%tn - %tt # /mp3s/New Downloads/Bad Religion - Into the Unknown/01 - Its Only Over When.mp3 # /mp3s/Ska/VA - Ska Sucks/01 - He Wants Me Back.mp3 # Show year, if it's defined #RENAME = %aa - {%ay - }%at/%tn - %tt # /mp3s/New Downloads/Bad Religion - 1983 - Into the Unknown/01 - Its Only Over When.mp3 # /mp3s/Ska/VA - Ska Sucks/01 - He Wants Me Back.mp3 # More than one directory: #RENAME = %aa/%at/%tn - %tt # /mp3s/New Downloads/Bad Religion/Into the Unknown/01 - Its Only Over When.mp3 # /mp3s/Ska/VA/Ska Sucks/01 - He Wants Me Back.mp3 # Combination with more than 1 directory, optional year, optional track artist #RENAME = %aa/{%ay - }%at/%tn - {%tb - }%tt # /mp3s/New Downloads/Bad Religion/1983 - Into the Unknown/01 - Its Only Over When.mp3 # /mp3s/Ska/VA/Ska Sucks/01 - Dance Hall Crashers - He Wants Me Back.mp3 # 3. Absolute directory: if RENAME is an absolute directory, then the # directory is moved to the proper location # Again, simple example: #RENAME = /mp3s/%aa - %at/%tn - %tt # /mp3s/Bad Religion - Into the Unknown/01 - Its Only Over When.mp3 # /mp3s/VA - Ska Sucks/01 - He Wants Me Back.mp3 # All of the above exactly, just add a /something to the front of them... # My personal format: #RENAME = /mp3s/%ag/%aa - {%ay - }%at/%tn - {%tb - }%tt # /mp3s/Punk/Bad Religion - 1983 - Into the Unknown/01 - Its Only Over When.mp3 # /mp3s/Ska/VA - Ska Sucks/01 - Dance Hall Crashers - He Wants Me Back.mp3 ~; close(WRITE); }