#!/usr/bin/perl -w # (C) 2003-2007 Willem Jan Hengeveld # Web: http://www.xs4all.nl/~itsme/ # http://wiki.xda-developers.com/ # # $Id: mp3tag 1502 2007-04-15 07:54:20Z itsme $ # # todo: # add option to apply 's/(xxx)..(yyy)/sprintf("%s-%s", $1, $2)/e' # type substitution patterns. # use MP3::Tag; use IO::File; use IO::Dir; $|=1; use Getopt::Long; my %g_newinfo; my $g_verbose=0; my $g_recurse=0; my $g_show_fields; my $g_import_pattern; my %g_types= ( t => { pattern=>qr/.*?/, name=>"title", format=>"%-30.30s" }, a => { pattern=>qr/.*?/, name=>"artist", format=>"%-20.20s" }, l => { pattern=>qr/.*?/, name=>"album", format=>"%-30.30s" }, y => { pattern=>qr/\d+/, name=>"year", format=>"%-4.4s" }, g => { pattern=>qr/.*?/, name=>"genre", format=>"%-10.10s" }, c => { pattern=>qr/.*?/, name=>"comment", format=>"%-30.30s" }, n => { pattern=>qr/\d+/, name=>"track", format=>"%-3.3s" }, ); sub usage { print <<__EOF__ Usage: mp3tag [-v] [-f FIELDS] -FIELD VALUE [files | wildcards] -v : g_verbose more -v -> more g_verbose -r : g_recurse into subdirectories, processing *.mp3 files -i PATTERN : import from path+filename EXAMPLE: "\%a \%y - \%l/\%n - \%t.mp3" will try to extract the specified fields from the filename. -f FIELDS : what fields (from a,y,l,...) to list in the output or * for 'aylnt' -a VALUE : set artist name -y VALUE : set year -l VALUE : set album title -n VALUE : set tracknumber -t VALUE : set song title -g VALUE : set genre -c VALUE : set comment __EOF__ } GetOptions( "t=s"=> sub { $g_newinfo{t}= $_[1]; }, "a=s"=> sub { $g_newinfo{a}= $_[1]; }, "l=s"=> sub { $g_newinfo{l}= $_[1]; }, "y=s"=> sub { $g_newinfo{y}= $_[1]; }, "g=s"=> sub { $g_newinfo{g}= $_[1]; }, "c=s"=> sub { $g_newinfo{c}= $_[1]; }, "n=s"=> sub { $g_newinfo{n}= $_[1]; }, "i=s"=> \$g_import_pattern, "f=s"=> \$g_show_fields, "v+" => \$g_verbose, "r" => \$g_recurse, ) or die usage(); die usage() if (!@ARGV) ; $g_show_fields= "aylnt" if ($g_show_fields && $g_show_fields eq "*"); my ($g_pathpattern, $g_pathvars)= parse_import_pattern($g_import_pattern) if ($g_import_pattern); for my $arg (@ARGV) { if ($arg =~ /\*/) { for my $filename (glob( $arg =~ /\// ? "\"$arg\"": $arg)) { processFile($filename); } } elsif (-d $arg) { processDirectory($arg); } elsif (-f $arg) { processFile($arg); } else { warn "'$arg' is neither a file or directory\n"; } } exit(0); sub parse_import_pattern { my ($pattern)= @_; my $re; my @vars; for my $part (split /(%\w|[\/\\]|\.)/, $pattern) { if ($part =~ /^%(\w)$/) { my $type= $1; if (!exists $g_types{$type}) { die "invalid type '$type' specified in import pattern\n"; } $re .= "($g_types{$type}{pattern})"; push @vars, $type; } elsif ($part =~ /^[\/\\]$/) { $re .= qr/[\/\\]/; } elsif ($part eq ".") { $re .= qr/\./; } else { $re .= $part; } } return ($re, \@vars); } sub processDirectory { my @subdirs= @_; while (@subdirs) { my $dirname= shift @subdirs; my $dir= IO::Dir->new($dirname); while (my $entry= $dir->read()) { next if ($entry eq "." || $entry eq ".."); my $fullpath= "$dirname/$entry"; if (-f $fullpath && $entry =~ /\.mp3$/i) { processFile($fullpath); } elsif ($g_recurse && -d $fullpath) { push @subdirs, $fullpath; } } $dir->close(); } } sub processFile { my ($fn)= @_; my %taginfo; my $mp3= MP3::Tag->new($fn); if (!$mp3) { warn "$fn: $!\n"; return; } # get info from path+filename $taginfo{filename}= parse_filename($fn) if ($g_import_pattern); $mp3->get_tags; # get id3v1 info if (!exists $mp3->{ID3v1}) { $mp3->new_tag("ID3v1"); } my $id3v1= $mp3->{ID3v1}; $taginfo{id3v1}= extract_id3_info($id3v1); # get id3v2 info if (!exists $mp3->{ID3v2}) { $mp3->new_tag("ID3v2"); } my $id3v2= $mp3->{ID3v2}; $taginfo{id3v2}= extract_id3_info($id3v2); # update id3v1, and id3v2 info for my $tags ($id3v1, $id3v2) { my $updatecount=0; for my $key (keys %g_newinfo) { my $fieldname= $g_types{$key}{name}; $tags->$fieldname($g_newinfo{$key}); $updatecount++; } for my $key (keys %{$taginfo{filename}}) { my $fieldname= $g_types{$key}{name}; $tags->$fieldname($taginfo{filename}{$key}); $updatecount++; } if ($updatecount) { $tags->write_tag; } } displayfields(\%taginfo, $fn) if ($g_show_fields); print "$fn\n\n" if ($g_verbose); writeinfo(\%taginfo) if ($g_verbose==1); writeid3v2($id3v2) if ($g_verbose>1); writeid3v1($id3v1) if ($g_verbose>1); print "\n\n" if ($g_verbose); } sub parse_filename { my ($fn)= @_; my %info; if (my @fields = ($fn =~ $g_pathpattern)) { return { map { ($g_pathvars->[$_] => $fields[$_]) } (0..$#fields) }; } else { warn "filename did not match pattern: $fn\n"; return {}; } } sub displayfields { my ($info, $fn)= @_; printf("%s : %s\n", join(", ", map { sprintf($g_types{$_}{format}, $info->{id3v2}{$_} || $info->{id3v1}{$_}); } (split //, $g_show_fields)), $fn); } sub writeinfo { my ($info)= @_; for my $key (keys %g_types) { printf("%-10s: %-30s %s\n", $g_types{$key}{name}, $info->{id3v1}{$key}, $info->{id3v2}{$key}); } } sub extract_id3_info { my ($id3)= @_; my %info; for my $key (keys %g_types) { my $fieldname= $g_types{$key}{name}; $info{$key}= $id3->$fieldname || ""; } return \%info; } sub writeid3v1 { my ($id3v1)= @_; for my $k (keys %$id3v1) { printf(" id3v1: %s => %s\n", $k, $id3v1->{$k}); } } sub writeid3v2 { my ($id3v2)= @_; my $fids= $id3v2->get_frame_ids('truename'); for my $f (keys %$fids) { my ($info, $name, @rest) = $id3v2->get_frame($f); for my $i ($info, @rest) { if (ref $i) { printf(" id3v2: %s => %s [ hash ]\n", $f, $name); for my $k (keys %$i) { printf(" %s => %s\n", $k, $i->{$k}); } } elsif (defined $i) { printf(" id3v2: %s => %s => %s\n", $f, $name, $i); } else { printf(" id3v2: %s => %s [ undef ]\n", $f, $name); } } if (!defined $info && !@rest) { printf(" id3v2: %s [ no info ]\n", $f); } } }