#!/usr/bin/perl use strict; use Music::Tag; use Getopt::Long; use File::Spec; use Data::Dumper; use Config::Options; use Pod::Usage; use utf8; our $VERSION = .28; our $options = Config::Options->new(); our $BASENAME = "musictag"; if ( $^X =~ /\/*([^\/]*)$/ ) { $BASENAME = $1; } $options->options( { trust_time => 0, trust_track => 0, trust_title => 0, inputplugins => ['Option'], plugins => [], outputplugins => [], ANSIColor => 1, AmazonLocale => "us", LevenshteinXS => 1, forcechange => 0, ignore_apic => 1, optionfile => [ $ENV{HOME} . "/.musictag/default.conf", $ENV{HOME} . "/.musictag/$BASENAME.conf" ], sort_regex => '[^A-Za-z0-9 _\-]', getinfo => "", presets => { clean => { plugins => [ 'File', 'MusicBrainz', 'Amazon', 'Lyrics' ], keepmtime => 1, presetdescription => '--plugin="File" --plugin="MusicBrainz" --plugin="Amazon" --plugin="Lyrics" --keepmtime', }, safeclean => { plugins => [ 'File', 'MusicBrainz', 'Amazon', 'Lyrics' ], keepmtime => 1, safe => 1, presetdescription => '--plugin="File" --plugin="MusicBrainz" --plugin="Amazon" --plugin="Lyrics" --keepmtime --safe', }, brainz => { inputplugins => ['MusicBrainz'], presetdescription => '--plugin="MusicBrainz"', }, brainzsort => { inputplugins => [ 'File', 'MusicBrainz' ], move => 1, dest => ".", presetdescription => '--plugin="File" --plugin="MusicBrainz" --move --dest="."', }, }, } ); my $cloptions = { plugins => [], outputplugins => [], inputplugins => [] }; our $clopts = [ [ "stdin" => \$cloptions->{stdin}, "Take filenames from standard input" ], [ "preset=s" => \$cloptions->{preset}, "Choose a preset list of options. See longhelp for details." ], [ "plugin=s@" => $cloptions->{plugins}, "Specifiy a plugin to add (input and outpout). " . "Plugin options can be expressed in the form option=value" ], [ "outputplugin=s@" => $cloptions->{outputplugins}, "Specify an output plugin." ], [ "inputplugin=s@" => $cloptions->{inputplugins}, "Specify an input plugin." ], [ "pluginoption=s%" => $cloptions, "Specifiy additional options for all plugins." ], [ "nochange" => \$cloptions->{nochange}, "Do not add default plugin to outputplugins." ], [ "trust_title" => \$cloptions->{trust_title}, "Trust title (over track number)" ], [ "trust_track" => \$cloptions->{trust_track}, "Trust track number (over title)" ], [ "trust_time" => \$cloptions->{trust_time}, "Trust track time more" ], [ "trust_totaltracks" => \$cloptions->{trust_totaltracks}, "Trust total number of tracks" ], [ "striptags" => \$cloptions->{strip_tag}, "Remove tag from file before writing new tag" ], [ "safe" => \$cloptions->{safe}, "Do not change artist, album, title, and track number" ], [ "verbose" => \$cloptions->{verbose}, "Produce more output" ], [ "quiet" => \$cloptions->{quiet}, "Shut up already." ], [ "keepmtime" => \$cloptions->{keepmtime}, "Attempt to keep mtime after tag chang." ], [ "sleeptime=f" => \$cloptions->{sleeptime}, "Sleep between iterations." ], [ "lyricsoverwrite" => \$cloptions->{lyricsoverwrite}, "Overwrite lyrics already saved." ], [ "coveroverwrite" => \$cloptions->{coveroverwrite}, "Overwrite cover artwork already saved." ], [ "forcechange" => \$cloptions->{forcechange}, "Resave tag no matter what" ], [ "printinfo" => \$cloptions->{printinfo}, "Dump raw tag info" ], [ "getinfo=s" => \$cloptions->{getinfo}, "Get a specific tag info " ], [ "move" => \$cloptions->{move}, "move file to sorted location" ], [ "cp" => \$cloptions->{cp}, "copy file to sorted location" ], [ "ln" => \$cloptions->{ln}, "link file to sorted location" ], [ "lns" => \$cloptions->{lns}, "symbolic link file to sorted location" ], [ "dest=s" => \$cloptions->{dest}, "set root path for sorted location" ], [ "nospace" => \$cloptions->{nospace}, "sort without spaces" ], [ "sort_regex=s" => \$cloptions->{sort_regex}, "Regex of characters to convert to underscore in filenames when sorting" ], [ "printoptions" => \$cloptions->{printoptions}, "Dump current options" ], [ "version" => sub { print "musictag $VERSION\n"; exit }, "Print current version number" ], [ "help" => \&help, "Your lookin' at it." ], [ "longhelp" => \&longhelp, "Help file with more detail." ], # NOTE: Help will look for the long help line and put a seperater in it for generated options. [ "asin=s" => \$cloptions->{asin}, "Set value: ASIN (Amazon Store ID)" ], [ "artist=s" => \$cloptions->{artist}, "Set value: artist" ], [ "album=s" => \$cloptions->{album}, "Set value: album" ], [ "title=s" => \$cloptions->{title}, "Set value: title" ], [ "track=i" => \$cloptions->{track}, "Set value: track" ], [ "disc=i" => \$cloptions->{disc}, "Set value: disc" ], [ "totaltracks=i" => \$cloptions->{totaltracks}, "Set value: totaltracks" ], [ "totaldiscs=i" => \$cloptions->{totaldiscs}, "Set value: totaldiscs" ], [ "secs=i" => \$cloptions->{secs}, "Set value: track duration in seconds" ], [ "duration=i" => \$cloptions->{duration}, "Set value: track duration in microseconds" ], ]; my %seenhash = map { $a=$_->[0]; $a=~s/=.+$//g; $a => $_->[1] } @{$clopts}; # Please don't set these; $seenhash{'tracknum'} = 1; $seenhash{'discnum'} = 1; $seenhash{'bytes'} = 1; $seenhash{'frames'} = 1; $seenhash{'frequency'} = 1; $seenhash{'bitrate'} = 1; foreach ( sort @{ Music::Tag->datamethods } ) { next if $seenhash{ $_ }; push @{$clopts}, [ $_ . '=s' => \$cloptions->{$_}, "Set value: $_" ]; } if ( exists $options->{optionfile} ) { $options->fromfile_perl( $options->{optionfile} ); } foreach my $k (keys %{$options->{presets}}) { push @{$clopts}, [ $k => sub {$cloptions->{preset} = $k }, "Preset option set: ". (exists $options->{presets}->{$k}->{presetdescription} ? $options->{presets}->{$k}->{presetdescription} : $k) ]; } Getopt::Long::GetOptions( map { $_->[0] => $_->[1] } @{$clopts} ); if ( exists $cloptions->{optionfile} ) { $options->fromfile_perl( $cloptions->{optionfile} ); } if ( $options->{getinfo} ) { $options->{verbose} = 0; $options->{quiet} = 1; } my ( $v, $p, $f ) = File::Spec->splitpath($0); if ( exists $options->{$f} ) { $options->options( $options->{$f} ); } if ( $cloptions->{preset} ) { $options->deepmerge( $options->{presets}->{ $cloptions->{preset} } ); } $options->deepmerge($cloptions); $options->merge( "plugins", [ split( /,/, join( ',', @{ $options->{plugins} } ) ) ] ); $options->merge( "inputplugins", [ split( /,/, join( ',', @{ $options->{inputplugins} } ) ) ] ); $options->merge( "outputplugins", [ split( /,/, join( ',', @{ $options->{outputplugins} } ) ) ] ); unless ( ( $options->{ANSIColor} ) && ( Music::Tag->_has_module("Term::ANSIColor") ) ) { print STDERR "Missing ANSIColor\n"; $options->{ANSIColor} = 0; } if ($cloptions->{printoptions}) { my $d = Data::Dumper->new( [ $options ] ); $d->Maxdepth(6)->Terse(1)->Sortkeys(1); binmode( STDOUT, ":utf8" ); print $d->Dump(); exit; } my @FILES = @ARGV; if ( $cloptions->{stdin} ) { while (<STDIN>) { chomp; push @FILES, $_; } } foreach (@FILES) { s/[^A-Za-z0-9]+$//g; if ( -d $_ ) { fix_dir($_); } elsif ( -f $_ ) { fix_file($_); } } sub help { print "musictag version $VERSION\n"; print "\nUsage: $^X [OPTION]... [FILES]...\n"; print "\nUpdate or view information about music files using Music::Tag\n"; print "Please see musictag --longhelp for more details.\n\n"; my $n=0; my $d=0; foreach ( @{$clopts} ) { if (($_->[2] =~ /^Set value:/) && (not $d)){ $d++; print "\nData Method Options:\n"; } if (($_->[2] =~ /^Preset option set:/) && (not $n)) { $n++; print "\nAvailable Preset Options:\n"; } my $command = $_->[0]; my %longhelp = ( s => "VALUE", i => "INT", 's@' => "VALUE", 's%' => "KEY=VALUE" ); $command =~ s/=(.+)$/'='. $longhelp{$1}/e; printf "--%-22s %s\n", $command, $_->[2]; } exit 1; } sub longhelp { pod2usage( -verbose => 2 ); } sub fix_dir { my $dir = shift; local *DIR; opendir( DIR, $dir ) or die "Couldn't open directory $dir\n"; while ( my $fname = readdir(DIR) ) { next if ( $fname =~ /^\./ ); my $filename = File::Spec->catfile( $dir, $fname ); if ( -d $filename ) { fix_dir($filename); } elsif ( ( $fname =~ /\.mp3$/i ) or ( $fname =~ /\.m4.$/i ) ) { fix_file($filename); } } closedir(DIR); } sub color { if ( $options->{ANSIColor} ) { return Term::ANSIColor::color(@_); } else { return ""; } } sub header { my $text = shift; return "" if ( $options->{quiet} ); my $left = 76 - length($text); return color('bold white') . "== " . color('green') . $text . color('bold white') . " " . "=" x $left . "\n" . color('reset'); } sub plugin_opts { my $pl = shift; my ( $plugin, $popts ) = split( ":", $pl ); my @opts = split( /[;]/, $popts ); my $ret = Config::Options->new(); foreach (@opts) { my ( $k, $v ) = split( "=", $plugin ); $ret->options( $k, $v ); } if ( exists $options->{$plugin} ) { $ret->options( $options->{$plugin} ); } return $ret; } sub fix_file { my $filename = shift; if ( $options->{sleeptime} ) { sleep $options->{sleeptime}; } return if ( $filename =~ /\.(jpg|gif|bmp|txt|nfo|html?|png|cda)/i ); print header("Processing $filename"); my $info = Music::Tag->new( $filename, $options ); return unless ($info); $info->get_tag(); my $backup_info = { artist => $info->artist, album => $info->album, title => $info->title, tracknum => $info->tracknum }; my @statback = stat($filename); my @inputplugins = (); my @outputplugins = (); if ( scalar @{ $options->{inputplugins} } ) { foreach ( @{ $options->{inputplugins} } ) { if ( $options->{verbose} ) { print "Adding input plugin $_"; } push @inputplugins, $info->add_plugin( $_, plugin_opts($_) ); } } foreach ( @{ $options->{plugins} } ) { my $p = $info->add_plugin( $_, plugin_opts($_) ); if ( $options->{verbose} ) { print "Adding plugin $_"; } push @inputplugins, $p; push @outputplugins, $p; } if ( scalar @{ $options->{outputplugins} } ) { foreach ( @{ $options->{outputplugins} } ) { if ( $options->{verbose} ) { print "Adding output plugin $_"; } push @outputplugins, $info->add_plugin( $_, plugin_opts($_) ); } } unless ( $options->{nochange} ) { push @outputplugins, $info->plugin->[0]; } foreach (@inputplugins) { $_->get_tag(); } if ( $options->{safe} ) { unshift @outputplugins, $info->add_plugin( "Option", $backup_info ); } if ( $options->{strip_tag} ) { foreach (@outputplugins) { $_->strip_tag(); } } if ( $info->changed or $options->{forcechange} ) { foreach (@outputplugins) { $_->set_tag(); } } $info->close(); if ( $options->{move} or $options->{cp} or $options->{ln} or $options->{lns} ) { do_move($info); } if ( $options->{printinfo} ) { print_info($info); } if ( $options->{getinfo} ) { foreach ( @{ Music::Tag->datamethods } ) { if ( lc($_) eq lc( $options->{getinfo} ) ) { my $method = lc($_); print $info->$method, "\n"; } } } $info = undef; print header("finished"); } sub do_move { my $info = shift; my $file = $info->filename; my ( $dest, $path ) = get_dest($info); return unless $path; unless ( -d $path ) { print "mkdir -p $path\n"; system( "mkdir", "-p", "$path" ); } if ( -e $dest ) { print "$dest exists, checking bitrate\n"; my $dinfo = Music::Tag->new($dest)->get_tag(); if ( $dinfo && $info ) { print "$file: ", $info->bitrate, " $dest: ", $dinfo->bitrate, "\n"; if ( $dinfo->bitrate >= $info->bitrate ) { print "Destination is equal or greater than source, not replacing\n"; return undef; } } print "Destination is lower bitrate, replacing\n"; } if ( $options->{move} ) { print "mv $file $dest\n"; system( "mv", "-f", "$file", "$dest" ); return 1; } if ( $options->{ln} ) { print "ln $file $dest\n"; system( "ln", "$file", "$dest" ); return 2; } if ( $options->{lns} ) { print "ln -s $file $dest\n"; system( "ln", "-s", "$file", "$dest" ); return 3; } if ( $options->{cp} ) { print "cp $file $dest\n"; system( "cp", "$file", "$dest" ); return 4; } return; } #This routine destorys values. Should be called before $info object set to be deleted! sub print_info { my $info = shift; if ($info) { if ( $info->picture ) { $info->picture->{_Data} = "[NOT PRINTED]"; } my $d = Data::Dumper->new( [ $info->data ] ); $d->Maxdepth(2)->Terse(1)->Sortkeys(1); binmode( STDOUT, ":utf8" ); print $d->Dump(); } } sub get_dest { my $info = shift; my $filename = $info->filename(); $filename =~ /\.([^\.]+)$/; my $suffix = $1 || "mp3"; if ($info) { my @dpath = ( $options->{dest}, filename_clean( $info->artist ), filename_clean( $info->album ) ); my $dfile = filename_clean( $info->title ); if ( $info->track ) { $dfile = $info->track . "-" . $dfile; } $dfile .= "." . $suffix; return File::Spec->catfile( @dpath, $dfile ), File::Spec->catdir(@dpath); } return undef; } sub filename_clean { my $in = shift; my $regex = $options->{sort_regex}; $in =~ s/$regex/_/g; $in =~ s/[^A-Za-z0-9 \-_]/_/g; if ( $options->{nospace} ) { $in =~ s/ /_/g; $in = lc($in); } return $in; } __END__ =head1 musictag musictag -- Quick access to Music::Tag features =head1 SYNOPSIS musictag --preset "clean" /path/to/mp3/file.mp3 This will perform all cleanup operations on mp3 file. =head1 OPTIONS =over 4 =item --stdin Take filenames from standard input in addition to command line. =item --preset=s Choose a preset list of options. Currently the following presets are defined by default =over 4 =item clean Is the equivalent of --plugin="File,MusicBrainz,Amazon,Lyrics" --keepmtime =item safeclean Is the equivalent of --plugin="File,MusicBrainz,Amazon,Lyrics" --keepmtime --safe =item brainz Is the equivalent of --plugin="MusicBrainz" =item brainzsort Is the equivalent of --plugin="File,MusicBrainz" --move=1 --dest="." =back =item --plugin Specifiy a plugin to add (input and outpout). Plugin options can be expressed in the form option=value. For example, to use Amazon with the German store, try --plugin="Amazon:Locale=de" =item --outputplugin Specify an output plugin. This is like --plugin except that it is ONLY used for output. For example, if you wanted to write cover art to a file but not read from a file you would use --plugin="Amazon" --outputplugin="File" =item --inputplugin=s@ Specify an input plugin. This is like --outputplugin for input. =item --pluginoption=s% Specifiy additional options for all plugins. These optons are specified in key-value form. For example --plugin="Amazon,MusicBrainz" --pluginoptions="trust_title=1" Would set the trusttitle option in both Amazon and MusicBrainz. =item --nochange Do not change the music file. Output plugins are still processed, just not the default plugin associated with the file extension. =item --trust_title Trust title (over track number). Used by MusicBrainz and Amazon plugins. =item --trust_track Trust track number (over title). Used by MusicBrainz and Amazon plugins. =item --trust_time Trust track time more. Used by MusicBrainz and Amazon plugins. =item --trust_totaltracks Trust total number of tracks. Used by MusicBrainz and Amazon plugins. =item --striptags Remove tag from file before writing new tag. Only used by MP3::Tag at the moment. Usefull to convert from id3v2.4 to id3v2.3 or to clean up cruft from tags. =item --safe Do not change artist, album, title, and track number. Usefull if all you want is a cover, and don't want to risk changing these. =item --verbose Produce more output. =item --quiet Shut up already. =item --keepmtime Attempt to keep mtime after tag change. Rarely works and requires you to run as root. =item --sleeptime=f Sleep between iterations. Keeps you from hammering Amazon or MusicBrainz. =item --lyricsoverwrite Overwrite lyrics already saved. =item --coveroverwrite Overwrite cover artwork already saved. =item --forcechange Resave tag no matter what. =item --printinfo Dump raw tag info to stdout. Very usefull. =item --getinfo=s Get a specific tag info. Great for scripts. =item --move Move file to sorted location. If --move is set and a --dest is set, will sort your files. For example: musictag --move --dest="/nicely/sorted/" /poorly/sorted This will resort the whole folder. =item --cp Copy file to sorted location. Same as move but copies. =item --ln Link file to sorted location. Same as move but links. =item --lns Symbolic link file to sorted location. Same as move but uses symbmolic links. =item --dest=s Set root path for sorted location. Will sort files in Artist/Album. =item --nospace sort without spaces. =item --sort_regex=s Regex of characters to convert to underscore in filenames when sorting =item --help Quick and dirty help. =item --longhelp Help file with more detail. =item B<Tag Set Options> Setting any of these will set the equivalent tag manually. =over 4 =item --track=i =item --disc=i =item --totaltracks=i =item --totaldiscs=i =item --secs=i =item --duration=i =item --artist=s =item --album=s =item --title=s =item --comment=s =item --tracknum=s =item --year=s =item --releasedate=s =item --sortname=s =item --albumartist=s =item --albumartist_sortname=s =item --mb_artistid=s =item --mb_albumid=s =item --mb_trackid=s =item --album_type=s =item --artist_type=s =item --lyrics=s =item --picture=s =item --url=s =item --genre=s =item --discnum=s =item --tempo=s =item --label=s =item --encoder=s =item --compilation=s =item --composer=s =item --copyright=s =item --rating=s =item --lastplayed=s =item --playcount=s =item --filename=s =item --asin=s =item --recorddate=s =item --country=s =item --mip_puid=s =item --originalartist=s =item --countrycode=s =item --artist_start=s =item --artist_end=s =item --encoded_by=s =item --songkey=s =item --disctitle=s =item --booklet=s =back =back =head1 CONFIGURATION FILE Configuration file is located at ~/.musictag/default.conf. See sample/default.conf for details. =head1 SEE ALSO L<Music::Tag::Amazon>, L<Music::Tag::File>, L<Music::Tag::FLAC>, L<Music::Tag::Lyrics>, L<Music::Tag::M4A>, L<Music::Tag::MP3>, L<Music::Tag::MusicBrainz>, L<Music::Tag::OGG>, L<Music::Tag::Option> =head1 AUTHOR Edward Allen III <ealleniii _at_ cpan _dot_ org> =head1 COPYRIGHT Copyright (c) 2007,2008 Edward Allen III. Some rights reserved. This program is free software; you can redistribute it and/or modify it under the terms of the Artistic License, distributed with Perl. =cut