#! /usr/bin/perl # Copyright Eric Koldinger, 2006 # kolding @ koldware.com # No warranty, expressed, or implied, if you use this program. Author is not responsible for any # damage it may do to your files or sanity. use CDDB::File; use MP3::Tag; use Audio::FLAC::Header; use Getopt::Long; use Unicode::String; use File::Spec; # Set up options processing $cddbDir = $ENV{"HOME"} . "/.cddb"; $cddbFile = undef; $cddbID = undef; $trackNumber = undef; $utf8 = 0; $help = 0; $globalCDDB = undef; $result = GetOptions( "db=s" => \$cddbDir, "cddb=s" => \$cddbFile, "id=s" => \$cddbID, "track=i" => \$trackNumber, "utf8!" => \$utf8, "help" => \$help ); if (!$result) { print "Invalid options\n"; } if ($help || !$result) { usage(); exit; } if (defined $cddbFile) { $globalCDDB = CDDB::File->new($cddbFile) || die "Could not open CDDB file $cddbFile\n"; } else { loadCDDB(); } $numFailures = 0; ## Now, parse the various files foreach $file (@ARGV) { my $result = 0; $result = setTagsMP3($file, $trackNumber, $cddbID, $cddb) if ($file =~ /.mp3$/);; $result = setTagsFLAC($file, $trackNumber, $cddbID, $cddb) if ($file =~ /.flac$/); if ($result ne 0) { chomp $result; print $result, "\n"; $numFailures++; } } $numFailures; sub setTagsFLAC { my ($file, $track, $discid, $cddb) = @_; print "Setting tags in $file\n"; my $flac = Audio::FLAC::Header->new($file); my $tags = $flac->tags(); my $cddb = $globalCDDB; my $discid = $cddbID; my $track = $trackNumber; ## If no CDDB file is specified, let's get one. if (!defined $cddb) { if (!defined $discid) { return "No CDDB DISCID (no ID3v2 Tag) available in $file. Must set on command line\n" unless (defined $tags->{"CDDB"}); # This is ugly. Only grab the first id, as they could be ID,ID,ID,ID ($discid) = split/,/, $tags->{"CDDB"}; } $cddb = findCDDB($discid) || return "Could not find CDDB entry for $discid\n"; } if (!defined $track) { $track = $tags->{"TRACKNUMBER"} || return "Could not get track in $file. Must set on command line\n"; $track =~ s/\/.*$//; } $trackInfo = findTrack($cddb, $track) || return "Could not find info for Disc $discid, Track $track\n"; my ($title, $pos) = parseTitle($cddb->title()); $tags->{"ARTIST"} = Unicode::String::latin1($trackInfo->artist())->utf8(); $tags->{"ALBUM"} = Unicode::String::latin1($title)->utf8(); $tags->{"TITLE"} = Unicode::String::latin1($trackInfo->title())->utf8(); $tags->{"GENRE"} = Unicode::String::latin1($cddb->genre())->utf8(); $tags->{"YEAR"} = $cddb->year(); $tags->{"TRACKNUMBER"} = $trackInfo->number() . "/" . $cddb->track_count(); $tags->{"CDDB"} = $cddb->id(); $tags->{"COMPILATION"} = 1 if ($cddb->artist ne $trackInfo->artist() || $cddb->artist eq "Various"); $tags->{"SET"} = $pos if (defined $pos); my $result = $flac->write(); return "Unable to write flac tag in $file. Result: $result" if ($result); 0; } sub setTagsMP3 { my ($file, $track, $discid, $cddb) = @_; print "Setting tags in $file\n"; my $mp3 = MP3::Tag->new($file) || return "Could not read $file\n"; $mp3->get_tags() || return "Could not get tags in $file\n"; my $id3v2 = $mp3->{ID3v2}; my $id3v1 = $mp3->{ID3v1}; my $cddb = $globalCDDB; my $discid = $cddbID; my $track = $trackNumber; ## If no CDDB file is specified, let's get one. if (!defined $cddb) { if (!defined $discid) { return "No CDDB DISCID (no ID3v2 Tag) available in $file. Must set on command line\n" unless (defined $id3v2); my ($txxx, $name) = $id3v2->get_frame("TXXX"); return "No CDDB DISCID (no TXXX CDDB frame) available in $file. Must set on command line\n" unless (defined $txxx); return "TXXX Frame is not CDDB: " . $txxx->{"Description"} if (!($txxx->{"Description"} eq "CDDB")); # This is ugly. Only grab the first id, as they could be ID,ID,ID,ID ($discid) = split/,/, $txxx->{"Text"}; } $cddb = findCDDB($discid) || return "Could not find CDDB entry for $discid\n"; } if (!defined $track) { $track = $mp3->track() || return "Could not get track in $file. Must set on command line\n"; } $trackInfo = findTrack($cddb, $track) || return "Could not find info for Disc $discid, Track $track\n"; # remove ID3v1. We don't need it. $id3v1->remove_tag() if (defined $id3v1); $id3v2 = $mp3->new_tag("ID3v2"); # if (!defined $id3v2); my ($title, $pos) = parseTitle($cddb->title()); $id3v2->add_frame("TALB", 0, translate($title)); $id3v2->add_frame("TPE1", 0, translate($trackInfo->artist())); $id3v2->add_frame("TIT2", 0, translate($trackInfo->title())); $id3v2->add_frame("TCON", 0, translate($cddb->genre())); $id3v2->add_frame("TYER", $cddb->year()); $id3v2->add_frame("TRCK", $trackInfo->number() . "/" . $cddb->track_count()); $id3v2->add_frame("TXXX", "CDDB", $cddb->id()); $id3v2->add_frame("TCMP", 1) if ($cddb->artist ne $trackInfo->artist() || $cddb->artist eq "Various"); $id3v2->add_frame("TPOS", $pos) if (defined $pos); # TIT2 (Title/songname/content description): 51 Phantom # TPE1 (Lead performer(s)/Soloist(s)): North Mississippi Allstars # TALB (Album/Movie/Show title): 51 Phantom # TCON (Content type): Blues (0) # TYER (Year): 2001 # TSIZ (Size): 128 # TRCK (Track number/Position in set): 1 # USER (Terms of use): frame $id3v2->write_tag(); $mp3->close(); 0; } # # Parse the title string. Look for Disc X of Y, or Disc X. # If it exists, pull it out, and return it as a position tag in a set. # sub parseTitle { my $title = shift; my $pos = undef; if ($title =~ /\s*(.*\w)\s*[\(\[]\s*[Dd]is[ck]\s*([0-9]+)\s*of\s*([0-9]+)\s*[\)\]]/) { $title = $1; $pos = "$2/$3"; } elsif ($title =~ /\s*(.*\w)\s*[\(\[]\s*[Dd]is[ck]\s*([0-9]+)\s*[\)\]]/) { $title = $1; $pos = "$2"; } return ($title, $pos); } sub trim { my $line = shift; $line = s/^\s+//; $line = s/\s+//; return $line; } sub translate { my $string = shift; if ($utf8) { $string = Unicode::String::utf8($string)->latin1(); } $string; } sub findCDDB { my $id = shift; return $globalCDDB if (defined $globalCDDB); return $CDDB{$id}; } sub findTrack { my $cddb = shift; my $trackNo = shift; foreach my $track ($cddb->tracks) { return $track if ($track->number == $trackNo); } return undef; } sub loadCDDB { opendir (DIRHANDLE, $cddbDir) || die "Could not open CDDB Database ($cddbDir) for reading\n"; my @files = readdir(DIRHANDLE); closedir (DIRHANDLE); foreach my $file (File::Spec->no_upwards(@files)) { my $filename = File::Spec->catfile($cddbDir, $file); next if (-d $filename); my $cddb = CDDB::File->new($filename) || die "Could not open and parse CDDB file $filename\n"; foreach my $id ($cddb->all_ids()) { $CDDB{$id} = $cddb; } } } sub usage { print ; } __DATA__ -d CDDB --db=s Where to find the CDDB database. -c file --cddb=file Explicit file to read data from. -i ID --id=id CDDB ID to use istead of from current tag. -t trk --track=trk Track number to use instead of from current tag. -h --help This screen.