#!/usr/bin/perl -w
use File::Copy;
use MP3::Tag;
# some settings for getting command-line options
use Getopt::Long;
Getopt::Long::Configure(qw/no_ignore_case_always ignore_case bundling/);
my %options = ( '--song=s' => "Name of the song",
'--album=s' => "Album",
'--artist=s' => "Artist",
'--comment=s' => "Comment",
'--track=i' => "Track",
'--genre=s' => "Genre",
'--year=i' => "Year",
'--removetag' => "Removes an existing tag",
'-q' => "Be quiet",
'-v' => "Be verbose",
'-f' => "Force",
'--show' => "Show the existing tag",
'--showgenres'=> "Show the existing genres (!!not yet!!)",
'--setfilename' => "Set filename from tag (according to format string)",
'--getfilename' => "Set tag according to filename (and format string)",
'--format=s' => "Set the format string for get/setfilenam (default: %a - %s.mp3)",
'--nospaces' => "Replace Spaces through _ in filenames",
'--test' => "Do NOT change the files. Only print which changes would be made",
'--skipwithoutv1' => "Don't do anything if no ID3v1 tag exists",
'--skipwithv1' => "Don't use --getfilename option if ID3v1 tags already exists",
);
# get the command line options
my %opt;
getoptions(\%opt, %options);
if (exists $opt{showgenres}) {
my $genres = MP3::Tag::genres();
print join (", ", @$genres) ."\n";
}
unless ($#ARGV >=0) {
print "error: Filename(s) missing\n" unless exists $opt{showgenres};
exit 0 if exists $opt{showgenres};
exit 1;
}
# is there only one or more files to work with?
$opt{single}=1 if $#ARGV==0;
# prepare v and q flag (v has higher priority)
delete $opt{q} if (exists $opt{v} && exists $opt{q});
# prepare for setfilename / getfilename (not allowed together)
if (exists $opt{setfilename} && exists $opt{getfilename}) {
print "error: Cannot use --setfilename and --getfilename together\n";
delete $opt{setfilename};
delete $opt{getfilename};
}
$opt{format} = "%a - %s.mp3" unless exists $opt{format};
my ($stencil, $details);
($stencil, $details) = formatstr_setfilename($opt{format}) if exists $opt{setfilename};
($stencil, $details) = formatstr_getfilename($opt{format}) if exists $opt{getfilename};
# loop for each file
chomp(@ARGV = <STDIN>) unless @ARGV;
for my $filename (@ARGV) {
# get the tags
$mp3 = MP3::Tag->new($filename);
unless (defined $mp3) {
print "Skipping $filename ...\n";
next;
}
$mp3->get_tags;
unless (exists $mp3->{ID3v1}) {
print "No ID3v1-Tag found\n" if exists $opt{show} || exists $opt{v};
next if exists $opt{skipwithoutv1};
$mp3->new_tag("ID3v1");
} else {
next if exists $opt{skipwithv1};
}
# deletet tag if wanted (option: --removetag)
$mp3->{ID3v1}->remove_tag if $opt{removetag};
# set tag if this is wanted
# option: --song, --artist, --album, --comment, --year, --genre, --track
$mp3->{ID3v1}->song($opt{song}) if exists $opt{song};
$mp3->{ID3v1}->artist($opt{artist}) if exists $opt{artist};
$mp3->{ID3v1}->album($opt{album}) if exists $opt{album};
$mp3->{ID3v1}->comment($opt{comment}) if exists $opt{comment};
$mp3->{ID3v1}->year($opt{year}) if exists $opt{year};
$mp3->{ID3v1}->genre($opt{genre}) if exists $opt{genre};
$mp3->{ID3v1}->track($opt{track}) if exists $opt{track};
if (exists $opt{song} || exists $opt{artist} || exists $opt{album} ||
exists $opt{comment} || exists $opt{year} || exists $opt{genre} || exists $opt{track}) {
if (exists $opt{test}) {
# do nothing, but show new tag
$opt{show} = 1;
} else {
if ($mp3->{ID3v1}->write_tag()) {
print "Tag written\n" unless exists $opt{q};
} else {
print "Couldn't write tag\n" unless exists $opt{q};
}
}
}
# show tag (option --show)
if ($opt{show}) {
print "\nID3v1-Tag: $filename\n" unless exists $opt{single};
print "New tag would be:\n" if exists $opt{test};
print " Song: " .$mp3->{ID3v1}->song . "\n";
print " Artist: " .$mp3->{ID3v1}->artist . "\n";
print " Album: " .$mp3->{ID3v1}->album . "\n";
print "Comment: " .$mp3->{ID3v1}->comment . "\n";
print " Year: " .$mp3->{ID3v1}->year . "\n";
print " Genre: " .$mp3->{ID3v1}->genre . "\n";
print " Track: " .$mp3->{ID3v1}->track . "\n";
}
# set filename from tag (option: --setfilename)
# with --format the format of the new filename can be set (see formatstr below)
if (exists $opt{setfilename}) {
unless (exists $opt{test}) { # check if there really exists a tag
$mp3->get_tags;
unless (exists $mp3->{ID3v1}) {
print "No ID3v1 Tag exists. Can't change $filename\n";
exit -1;
}
}
my $new = $stencil;
my $i=0;
foreach (@$details) {
my $txt = $mp3->{ID3v1}->{$_->{tag}};
$txt =~ s/ *$//;
$txt = substr $txt, 0, $_->{length} if exists $_->{length} && ((! exists $_->{fill}) || exists $_->{precise} );
$txt = $_->{fill} x ($_->{length}-length($txt)) . $txt if exists $_->{fill};
$new =~ s/%$i/$txt/;
$i++;
}
$new =~ s/ /_/g if exists $opt{nospaces};
print "Trying to rename $filename to $new\n" if exists $opt{v} && !exists $opt{test};
print "$filename => $new\n" if exists $opt{test};
if ( !exists $opt{test} && $new && checkpath($new)) {
$mp3->close;
move($filename, $new);
} else {
print "Cannot set filename from tag: $new is invalid\n" unless exists $opt{q} || exists $opt{test} ;
}
}
# set tag from filename (option: --getfilename)
# with --format the format of the filename can be set
if (exists $opt{getfilename}) {
my @matches;
if (@matches = ($filename =~ $stencil)) {
while(($key,$val)=each %$details) {
$mp3->{ID3v1}->$val($matches[$key]);
}
if (exists $opt{test} or exists $opt{v} or exists $opt{show}) {
print "\nID3v1-Tag: $filename\n" unless exists $opt{single};
print "After scanning filename, new tag would be:\n";
print " Song: " .$mp3->{ID3v1}->song . "\n";
print " Artist: " .$mp3->{ID3v1}->artist . "\n";
print " Album: " .$mp3->{ID3v1}->album . "\n";
print "Comment: " .$mp3->{ID3v1}->comment . "\n";
print " Year: " .$mp3->{ID3v1}->year . "\n";
print " Genre: " .$mp3->{ID3v1}->genre . "\n";
print " Track: " .$mp3->{ID3v1}->track . "\n";
}
unless (exists $opt{test}) {
if ($mp3->{ID3v1}->write_tag()) {
print "Tag written\n" unless exists $opt{q};
} else {
print "Couldn't write tag\n" unless exists $opt{q};
}
}
} else {
print"Couldn't analyze '$filename' with /$stencil/\n" unless exists $opt{q};
}
}
}
######################################## SUBS
# check if the path to this file exists
# if not, ask if missing dirs should be created
# return true/false if the path is ok
sub checkpath {
my $file = shift;
my $tree=""; my $treeok=1;
while ($file =~ /([^\/]*)\//g && $treeok) {
unless ($1 eq "" || -d $tree.$1) {
print "$tree$1/ doesn't exists!\n" if exists $opt{v};
$treeok=0;
last unless confirm("Should I create the directory $tree$1/");
if (mkdir $tree.$1, 0755) {
$treeok=1;
} else {
print "Cannot create directory $tree$1/\n";
}
}
$tree .=$1."/";
}
return $treeok;
}
# prints a question and waits for a [y]es or [n]o
# returns true (1) for [y] or false (o) for [n]
# with option -f (force) returns always true (1)
sub confirm {
return 1 if $opt{f};
my $question = shift;
print $question ." [y/n] ?";
my $key = <STDIN>;
chomp $key;
return ($key =~ /^[YyJj]/) ? 1 : 0;
}
#######################################
# converts formatstr into the internal dataformat
#
# the formatstr may include any text, valid for a filename
# also it contains symbols,which must start with a % and end with one of [salgyt]
# %s song, %a - artist %l - album, %y -year, %g - genre, %t -track
#
# each symbol can contain additional information:
# a length l, directly after the %
# a fill char, given as :x or !:x where x is the fillchar, and :x or !:x follows after the length
#
# * if there is only a length l given, the string will be max. l chars long,
# that means cut off after l chars if it is longer
# * if there is a length and a :x, then a string which is shorter then l chars,
# will be filled from left with the char x to meet the length l
# a longer sting will not be affected
# * if there is a length and a !:x, then a string shorter than l, will also filled
# from left with x, but a longer string than l, will be cut off at l
#
# eg track=3 artist=Abba song=Waterloo
# '%2:0t.$a - $s.mp3' will be translated to '03.Abba - Waterloo.mp3'
# '%t.$6!:_a - $6!:_s.mp3' will be translated to '3.__Abba - Waterl.mp3'
#
# intern format contains $stencil and @details
# the stencil contains text and %i makros. i is an integer, counting up from 0
# $details[i] contains {tag} which should be used to replace %i in the stencil
# {length}, {fill} and {precise} give some additional information for replacing
sub formatstr_setfilename {
my $format = shift;
my %tags = (s=>"song", a=>"artist", l=>"album", y=>"year", g=>"genre", t=>"track", c=>"comment");
my @fmt;
while ($format =~ /%([0-9]*)(?:(!)?:(.))?([salygct])/g) {
my $t;
$t->{length}=$1 if defined $1 && $1 ne "";
$t->{precise}=1 if defined $2 && $2 ne "";
$t->{fill}=$3 if defined $3 && $3 ne "";
$t->{tag} = $tags{$4} if defined $4 && $4 ne "";
push @fmt, $t if defined $4 && $4 ne "";
}
my $i=0;
$format =~ s/%([0-9]*)(?:(!)?:(.))?([salygt])/"%".$i++/eg;
return ($format, \@fmt);
}
sub formatstr_getfilename {
my $format = shift;
my $pos=0;
my %tags = (s=>"song", a=>"artist", l=>"album", y=>"year", g=>"genre", t=>"track", c=>"comment");
my %info;
while ($format =~ /%([salgcyt])/g) {
$info{$pos++}=$tags{$1};
}
$format =~ s/([\[\]*.?()])/\\$1/g;
$format =~ s/%[yt]/(\\d+)/g;
$format =~ s/%[salgc]/(.+?)/g;
return (qr!$format!, \%info);
}
########################################
sub getoptions {
my ($optref, %options) = @_;
unless ( GetOptions ($optref, keys %options) ) {
# found unknown option
# show usage of options
print "\nUSAGE: $0 " . join(" ", sort keys %options) ." file(s)\n\n";
my ($eq, $co, $neg) = (0,0,0);
foreach (sort keys %options) {
printf "%13s : %s\n", $_, $options{$_};
$eq = 1 if /=/;
$co = 1 if /:/;
$neg = 1 if /!/;
}
print "\n" if $eq || $co || $neg;
print " --switch! - switch may be negated as --noswitch\n" if $neg;
print " --switch=x - switch must be followed by a value x\n" if $eq;
print " --switch:x - switch can be followed by a value x\n" if $co;
print " / f - value must be a float\n".
" x = s - value must be a string\n".
" \\ i - value must be an integer\n" if $eq || $co;
# and exit because of unknown options
exit(0);
}
}