#!/usr/bin/perl
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},
"Specify 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
,
"Specify 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."
],
[
"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
};
$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
;
}
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
;
}
Hide Show 301 lines of Pod