#!/usr/bin/perl # Filename: mzmv # Author: David Madison # Description: Moves an mp3/ogg file according to it's tags # Requires: vorbiscomment for oggs # Requires: id3tool for mp3s use strict; ################################################## # Setup the variables ################################################## my $PROGNAME = $0; $PROGNAME =~ s|.*/||; my $ILL_CHARS = '~()!*?\"\'\/'; my $DEFAULT_DEST = "%artist%/%year,-%%album%/%0.2d=track,-%%title%.%post%"; ################################################## # Usage ################################################## sub usage { my $msg; foreach $msg (@_) { print "ERROR: $msg\n"; } print < .. Moves an mp3/ogg file according to it's tags -dest Specify destination format -v Set verbose mode -d Set debug mode -cp Copy, not move -spaces Don't convert spaces to '_' Destination Formats ------------------- The destination format can have %keys% that are replaced by the tag values Ex: %artist%/%title% -> U2/So_Cruel Special formats: %post% is the postfix (mp3/ogg/..) %year% is the year (calculated from date tag for oggs) %bpm% is set if found in the notes as "#bpm" %track% is guessed for mp3s if possible (from comments, etc..) You can specify sprintf formats for the tag: Ex: %0.3d=track%-%title% -> 006-So_Cruel You can also specify a trailing string, only if the tag exists: Ex: %track,-%%title% -> 6-So_Cruel, Another_Song Directories will be made as needed, and directories moved out of will be erased if empty. Default destination: $DEFAULT_DEST USAGE exit -1; } sub parse_args { my @songs; my $dest = $DEFAULT_DEST; my $arg; while ($#ARGV>=0) { $arg=shift(@ARGV); if ($arg =~ /^-h$/) { usage(); } if ($arg =~ /^-dest$/) { $dest = shift @ARGV; next; } if ($arg =~ /^-spaces$/) { $MAIN::SPACES = 1; next; } if ($arg =~ /^-cp$/) { $MAIN::COPY = 1; next; } if ($arg =~ /^-d$/) { $MAIN::DEBUG=1; next; } if ($arg =~ /^-v$/) { $MAIN::VERBOSE=1; next; } if ($arg =~ /^-/) { usage("Unknown option: $arg"); } push(@songs,$arg); } usage("No songs defined") if (!@songs); ($dest,@songs); } sub clean { my ($str) = @_; $str =~ s/\s+$//; $str =~ s|/|-|g; $str =~ s/[$ILL_CHARS]/_/g; $MAIN::SPACES ? ($str =~ s/\s+/ /g) : ($str =~ s/\s+/_/g); $str =~ s/_+/_/g; $str =~ s/_+$//; $str =~ s/^_+//; $str =~ s/\-+/\-/g; $str =~ s/\-+$//; $str =~ s/^\-+//; $str =~ s/è/e/g; $str =~ s/ü/u/g; $str; } ################################################## # Song info ################################################## sub ogg_info { my ($ogg) = @_; my %i; # Get song tags open(TAGS,"vorbiscomment \Q$ogg\E |") || die("[$PROGNAME] Couldn't run vorbiscomment [$ogg]\n"); ($i{title},$i{artist},$i{album},$i{note}) = ("unknown","unknown","unknown"); while() { chomp; next if /^\s*$/; die("[$PROGNAME] Bad vorbiscomment: [$_]") unless /^(\S+)=(.*)$/; my ($key,$val) = (lc($1),clean($2)); $i{$key} = $val; } close(TAGS); $i{year} = $i{date}; $i{year} =~ s/(\d{4}).*/$1/; $i{track} = $i{tracknumber}; \%i; } sub mp3_info { my ($mp3) = @_; my %i; # Get id3 info open(ID3,"id3tool \Q$mp3\E |") || die("[$PROGNAME] Couldn't run id3tool [$mp3]\n"); my $note; ($i{title},$i{artist},$i{album},$i{note}) = ("unknown","unknown","unknown"); while() { s/\s+$//; $i{title}=clean($1) if (/^Song Title:\s+(\S.*)/); $i{artist}=clean($1) if (/^Artist:\s+(\S.*)/); $i{album}=clean($1) if (/^Album:\s+(\S.*)/); $i{year}=clean($1) if (/^Year:\s+(\S.*)/); $i{track}=clean($1) if (/^Track:\s+(\S.*)/); $i{comment}=$1 if (/^Note:\s+(\S.*)/); } close(ID3); # Hack - save the track number if ripped by mp3c and filename is -.. if ($i{comment} =~ /gen by.*mp3c/i && $mp3 =~ m|/(\d+)-[^/]+$|) { $i{track} = $1; #system("id3tool \Q$mp3\E -n 'Track $i{track}'"); system("id3tool \Q$mp3\E -c $i{track}"); } elsif ($i{comment} =~ /Track #?(\d+)/i) { $i{track} = $1; } \%i; } my %NOTAGWARN; sub tagReplace { my ($tag, $i) = @_; my $format = ($tag =~ s/^([^=]+)=//) ? '%'.$1 : '%s'; my $post = ($tag =~ s/,(.+)//) ? $1 : ""; my $pre = ($tag =~ s/^(.+)<{$tag}; unless ($val) { print STDERR "[$PROGNAME] Warning: No tag $tag [$i->{path}]\n" unless $NOTAGWARN{$tag}++; return ""; } $pre.sprintf("$format", $val).$post; } sub dir { my ($path) = @_; return undef unless $path =~ m|/|; $path =~ s|/+$||; $path =~ s|/[^/]+$||; $path; } sub mkdirOf { my ($path) = @_; return if !defined $path || -d $path; mkdirOf(dir($path)); mkdir($path) } sub rmdirR { my ($path) = @_; return unless defined $path && -d $path; return unless rmdir($path); rmdirR(dir($path)); } ################################################## # Main code ################################################## sub handle { my ($old,$new) = @_; $MAIN::COPY ? system("/bin/cp",$old,$new) : rename($old,$new); } sub main { my ($dest,@songs) = parse_args(); foreach my $song ( @songs ) { if (! -f $song) { print STDERR "[$PROGNAME] Warning: Couldn't find song [$song]\n"; next; } ######################### # Get the info ######################### my $i; if ($song =~ /\.mp3$/) { $i = mp3_info($song); $i->{post} = "mp3"; } elsif ($song =~ /\.ogg$/) { $i = ogg_info($song); $i->{post} = "ogg"; } else { print STDERR "[$PROGNAME] Error: Unknown song type [$song]\n"; next; } if ($i->{artist} eq "unknown" || ($i->{title} eq "unknown" && $i->{album} eq "unknown")) { print STDERR "[$PROGNAME] Error: No tags for song [$song]\n"; #next; } print STDERR "[$PROGNAME] Warning: Missing tag(s) for song [$song]\n" if ($i->{title} eq "unknown" || $i->{artist} eq "unknown" || $i->{album} eq "unknown"); $i->{artist} =~ s/^The_//ig; $i->{path} = $song; $i->{bpm} = ($i->{comment} =~ /(\d+)\s?bpm/) ? $1 : undef; ######################### # Calculate the path ######################### my $new = $dest; while ($new =~ s/%([^%]+)%/tagReplace($1,$i)/eg) {} # Make the directories mkdirOf(dir($new)); if (-f $new) { print STDERR "[$PROGNAME] ERROR: $new already exists.\n"; next; } ######################### # Move it ######################### print "$song -> $new\n" if $MAIN::VERBOSE || $MAIN::DEBUG; handle($song,$new) unless $MAIN::DEBUG; print STDERR "[$PROGNAME] ERROR: $new already exists.\n" if $? && !$MAIN::DEBUG; # Remove the directory if it's empty rmdirR(dir($song)); } } main();