#! /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 MP3::Tag; use Audio::FLAC::Header; use Getopt::Long; use Unicode::String; use File::Spec; use File::Path; use File::Copy; use Cwd; # Set up options processing $mp3base = "/srv/music/MP3/CD"; $flacbase = "/srv/music/FLAC"; $variousArtists = "VariousArtists"; $classicalName = "Classical"; $noArtist = "NoArtist"; $utf8 = 1; $compilation = 0; $classical = 0; $dorename = 1; $cleanUp = 0; $split = 0; $inplace = 0; @dragfiles = (); $result = GetOptions( "utf8!" => \$utf8, "base=s" => \$mp3base, "compilation" => \$compilation, "classical" => \$classical, "various=s" => \$variousArtists, "drag=s" => \@dragfiles, "rename!" => \$dorename, "clean!" => \$cleanUp, "split!" => \$split, "inplace!" => \$inplace, "flacbase=s" => \$flacbase, "mp3base=s" => \$mp3base, "help" => \$help ); if (!$result) { print "Invalid options\n"; } $flacbase = $mp3base unless ($split); if ($help || !$result) { usage(); exit; } if (@dragfiles == 0) { @dragfiles = ("cover.jpg", "disc.json"); } @dragfiles = split(/,/,join(',',@dragfiles)); ## Now, parse the various files foreach $file (@ARGV) { if (-d $file) { reorgDir($file); } else { renameFile($file); } } $numFailures; sub reorgDir { my $dir = shift; opendir DIRHANDLE, "$dir" || die "Could not open directory $dir\n"; my @names = readdir(DIRHANDLE); closedir DIRHANDLE; my ($artist, $album, $title, $genre, $track, $disc, $composer, $vaTag); my @composers; my @files = map { File::Spec->catfile($dir, $_); } File::Spec->no_upwards(@names); foreach my $path (@files) { next unless (-e $path); next if (-d $path); if ($path =~ /\.mp3$/) { ($artist, $album, $title, $genre, $composer, $track, $disc, $vaTag) = collectTagsMP3($path); } elsif ($path =~ /\.flac$/) { ($artist, $album, $title, $genre, $composer, $track, $disc, $vaTag) = collectTagsFlac($path); } push @composers, $composer if (defined $composer && !contains($composer, @composers)); } @composers = sort { $a cmp $b } @composers; if (@composers > 4) { $composers = join (" & ", $composers[0], "et al"); } else { $composers = join (" & ", @composers); } foreach my $path (@files) { next unless (-e $path); if (-d $path) { reorgDir($path); } else { renameFile($path, $composers); } } } sub renameFile { my $file = shift; my $composerString = shift; my $result; my $artist, $album, $title, $album, $track, $disc, $genre, $composer, $vaTag; my $suffix, $basedir; if ($file =~ /\.mp3$/) { ($artist, $album, $title, $genre, $composer, $track, $disc, $vaTag) = collectTagsMP3($file); $suffix = ".mp3"; $basedir = $mp3base; } elsif ($file =~ /\.flac$/) { ($artist, $album, $title, $genre, $composer, $track, $disc, $vaTag) = collectTagsFlac($file); $suffix = ".flac"; $basedir = $flacbase; } else { print "$file: Not tagged type\n"; return; } if (!defined $album) { $result = $artist; chomp $result; print $result, "\n"; $numFailures++; return; } my $clTag = 1 if ($genre =~ /^[Cc]lassical/); my $dir = makeDirName($artist, $vaTag, $clTag, $composerString); doRename($file, $dir, $album, $title, $track, $disc, $basedir, $suffix); } sub collectTagsFlac { my $file = shift; my $vaTag = 0; my $disc = undef; my $composer = undef; my $flac = Audio::FLAC::Header->new($file) || return "Could not read tags from $file"; my $tags = $flac->tags() || return "No tags found in $file"; my $artist = latin1($tags->{"ARTIST"}); my $album = latin1($tags->{"ALBUM"}) || return "No album tag in $file"; my $title = latin1($tags->{"TITLE"}) || return "No title tag in $file"; $title = $title . ", " . latin1($tags->{"PART"}) if (defined $tags->{"PART"}); my $genre = latin1($tags->{"GENRE"}) || return "No genre tag in $file"; my $track = int($tags->{"TRACKNUMBER"}) || return "No track tag in $file";; $composer = latin1($tags->{"COMPOSER"}); $disc = int ($tags->{"SET"}) if (defined $tags->{"SET"}); my $vaTag = 1 if (defined $tags->{"COMPILATION"}); return ($artist, $album, $title, $genre, $composer, $track, $disc, $vaTag); } sub collectTagsMP3 { my $file = shift; my $vaTag = 0; my $disc = undef; my $composer = undef; my $mp3 = MP3::Tag->new($file) || return "Could not read $file"; $mp3->get_tags() || return "Could not get tags in $file"; my $artist = utf8($mp3->artist()); my $album = utf8($mp3->album()) || return "No album tag in $file"; my $title = utf8($mp3->title()) || return "No title tag in $file"; my $genre = utf8($mp3->genre()) || return "No genre tag in $file"; my $track = $mp3->track() || return "No track tag in $file"; my $id3v2 = $mp3->{ID3v2}; my ($info, $name, @rest) = $id3v2->get_frame("TPOS") if (defined $id3v2); $disc = int ($info) if (defined $info); ($info, $name, @rest) = $id3v2->get_frame("TCOM") if (defined $id3v2); $composer = utf8($info) if (defined $info); ($info, $name, @rest) = $id3v2->get_frame("TCMP") if (defined $id3v2); $vaTag = 1 if (defined $info); return ($artist, $album, $title, $genre, $composer, $track, $disc, $vaTag); } sub makeDirName { my ($artist, $vaTag, $clTag, $clDir) = @_; $dir = munge($artist); $dir = munge($variousArtists) if ($vaTag || $compilation); if ($clTag || $classical) { $dir = munge($classicalName); $dir = File::Spec->catdir($dir, munge($clDir)) if (defined $clDir) } $dir = munge($noArtist) unless defined $dir; return $dir; } sub doRename { my ($file, $dir, $album, $title, $track, $disc, $basedir, $suffix) = @_; $track = int($track); $track = "0$track" if ($track < 10); $track = "$disc-$track" if (defined $disc); my $newdir; if ($inplace) { my $sink; ($sink, $newdir, $sink) = File::Spec->splitpath(Cwd::realpath($file)); } else { $newdir = File::Spec->catdir($basedir, $dir, munge($album)); } my $newfile = munge("$track.$title$suffix"); my $newpath = File::Spec->catfile($newdir, $newfile); if (Cwd::realpath($file) eq Cwd::realpath($newpath)) { print "Not moving $file. Name not changed\n"; return 0; } print "Moving $file to $newpath\n"; if ($dorename) { mkpath($newdir) || return "Could not create directory $newdir" unless (-d $newdir); } if (-e $newpath) { return "$file Target $newpath exists. Not moved." } else { rename($file, $newpath) if ($dorename) || return "Could not move $file to $newpath\n"; } drag($file, $newdir); purgeDir($file) if ($cleanUp); 0; } sub purgeDir { my $file = shift; my ($sink, $dir, $sink) = File::Spec->splitpath($file); opendir (DIR, $dir) || return; my $numFiles = File::Spec->no_upwards(readdir(DIR)); closedir DIR; if ($numFiles == 0) { print "Removing empty directory $dir\n"; rmtree($dir); } } $lastOldDir = undef; $lastNewDir = undef; sub drag { my ($file, $newdir) = @_; my ($sink, $olddir, $sink) = File::Spec->splitpath($file); if (($olddir ne $lastOldDir) || ($newdir ne $lastNewDir)) { doDrag($olddir, $newdir); $lastOldDir = $olddir; $lastNewDir = $newdir; } } sub doDrag { my ($olddir, $newdir) = @_; foreach my $file (@dragfiles) { my $oldpath = File::Spec->catfile($olddir, $file); if (-e $oldpath) { my $newpath = File::Spec->catfile($newdir, $file); if (Cwd::realpath($oldpath) eq Cwd::realpath($newpath)) { print "Not moving $oldpath. Name not changed\n"; next; } print "Dragging $oldpath to $newpath\n"; if ($dorename) { mkpath($newdir) || return "Could not create directory $newdir" unless (-d $newdir); } if (-e $newpath) { return "$file Target $newpath exists. Not moved." } elsif ($split) { copy($oldpath, $newpath) if ($dorename) || die "Could not copy $oldpath to $newpath\n"; } else { rename $oldpath, $newpath if ($dorename) || die "Could not move $oldpath to $newpath\n"; } } } } sub munge { #echo "$@" | sed s,:,\ -,g | tr \ / __ | tr -d \'\"\?\[:cntrl:\] my $name = shift; $name =~ s/[\$\"\'\?]//g; $name =~ s/[ \/]/_/g; $name =~ s/:/_-/g; return $name; } sub utf8 { my $name = shift; return undef if (!defined $name); $name = Unicode::String::latin1($name)->utf8() if ($utf8); return $name; } sub latin1 { my $name = shift; return undef if (!defined $name); $name = Unicode::String::utf8($name)->latin1() if (!$utf8); return $name; } sub contains { my $element = shift; my @list = @_; foreach my $listElem (@list) { return 1 if ($listElem eq $element); } return 0; } sub usage { print ; } __DATA__ -u --utf8 Create target filenames in UTF8 -b path --base=path Create Targets in path. Must be in the same filesystem as originals. -v name --various=name Name of Various Artists directory. -d --drag=s Drag files with this name along with music files. Multiple can be specified. -s --[no]split Split the files between flac and mp3 files. -i --[no]inplace Change the file names in the current directory only. Do not move to other dirs -r --rename Actually do the rename (--norename for dry run) -h --help This screen.