内容


功能丰富的 Perl:

趣谈 MP3 和 Perl,第 1 部分

使用 Perl 操纵和猜测 MP3

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 功能丰富的 Perl:

敬请期待该系列的后续内容。

此内容是该系列的一部分:功能丰富的 Perl:

敬请期待该系列的后续内容。

对于现在了解计算机的音乐爱好者而言,操纵 MP3 文件是一项必须具备的技能。虽然其他音乐文件格式已存在并在蓬勃发展着,但本文还是主要讨论 MP3 格式,因为众所周知,它是当今最流行的格式。但是,本文所讲述的一般方法也可用于处理其他允许使用标签(tag)的音乐文件格式。实际上,很多使用标签的文件格式都可以从类似本文中的 autotag.pl 程序中受益。欢迎您提出建议。

本文将一般性地讨论有关 Perl 的问题 ,特别关注 MP3 文件的操纵,并详细介绍了 autotag.pl 应用程序。

尽管已经有了 MP3::InfoMP3::ID3LibMusicBrainz::ClientAudioFile::Identify::MusicBrainz 模块,而且这些模块可能很有用,但我只使用 MP3::ID3Lib 的主要理由是因为它需要 id31ib 软件(请参阅 参考资料)。虽然 MP3::Info 是纯 Perl 语言编写的而且安装也很简单,但我发现 MP3::Tag 功能更强大。之所以没有使用 MusicBrainz::ClientAudioFile::Identify::MusicBrainz ,是因为 MusicBrainz 似乎是比 FreeDB 更不全面的已发行 CD 的数据库。在本文的结尾,将向读者介绍 ID3 标签加注模块和曲目信息模块的选择。我经过试验和失败而艰难获得的经验表明, MP3::TagWebService::FreeDB 是最佳的模块。

虽然 CDDB (Gracenote) 磁盘库非常全面,但我还是没有选择使用它。Gracenote 是一家拥有 CD 曲目列表的专有数据库(只允许对数据库执行搜索,不能大量下载)的公司。在 Gracenote 只拥有 CDDB 的早期,志愿者贡献了这些数据库的相当一部分内容。而 FreeDB 是一个志愿者经过有组织的努力提供的免费、无限制的 CD 曲目数据库。FreeDB 数据库的整个内容都可以下载,无版权限制 —— 因此,如果您愿意,可以建立自己的 FreeDB 服务器。

我不使用的模块并不是因为这些模块一定不好,因此,如果您喜欢,您可以使用它们。基于个人经验和上述原因,我只是更喜欢 MP3::TagWebService::FreeDB 。 实际的读写标签在函数中进行了抽象,因此,如果使用不同的模块读写 MP3 标签,就不需要更改很多内容。

我还应提一下,在 Linux 内部的 xtermEterm 终端模拟器中, Term::ReadLine::Gnu 模块比默认模块 Term::ReadLine::Perl 能更好地工作。如果您注意到在提示输入期望的文本时出现一些奇怪的行为,那么可能要将其安装在 Term::ReadLine 之上。

基本的 autotag.pl 函数

我把 autotag.pl 几个功能放在了不同的函数中。首先, contains_word_char() 是一个判断某些文本中是否包含某个词(在 Perl 中是 \w in Perl)中的字符的函数。该函数也会正确地处理未定义的值,尽管在警告打开时,常规表达式在匹配未定义的值时会输出警告信息。该函数是极为有用的,因为它不显示警告信息;为了不使用函数而又达到这个目的,您必须检查是否每次都定义了字符串。

清单 1. contains_word_char() 函数
# {{{ contains_word_char: return 1 if the text contains a word character
sub contains_word_char
{
 my $text = shift @_;
 return $text && length $text && $text =~ m/\w/;
}
# }}}

接下来是输入例程。这些程序相当长,它们试图处理程序所需要的用户交互的大多数情况。

清单 2. get_tag() 函数
# {{{ get_tag: get a ID3 V2 tag, using V1 if necessary
sub get_tag
{
 my $file    = shift @_;
 my $upgrade = shift @_;
 my $mp3 = MP3::Tag->new($file);
 return undef unless defined $mp3;
 $mp3->get_tags();
 my $tag = {};
 if (exists $mp3->{ID3v2})
 {
  my $id3v2 = $mp3->{ID3v2};
  my $frames = $id3v2->supported_frames();
  while (my ($fname, $longname) = each %$frames)
  {
   # only grab the frames we know
   next unless exists $supported_frames{$fname};
   $tag->{$fname} = $id3v2->get_frame($fname);
   delete $tag->{$fname} unless defined $tag->{$fname};
   $tag->{$fname} = $tag->{$fname}->{Text} if $fname eq 'COMM';
   $tag->{$fname} = $tag->{$fname}->{URL} if $fname eq 'WXXX';
   $tag->{$fname} = '' unless defined $tag->{$fname};
  }
 }
 elsif (exists $mp3->{ID3v1})
 {
  warn "No ID3 v2 TAG info in $file, using the v1 tag";
  my $id3v1 = $mp3->{ID3v1};
  $tag->{COMM} = $id3v1->comment();
  $tag->{TIT2} = $id3v1->song();
  $tag->{TPE1} = $id3v1->artist();
  $tag->{TALB} = $id3v1->album();
  $tag->{TYER} = $id3v1->year();
  $tag->{TRCK} = $id3v1->track();
  $tag->{TIT1} = $id3v1->genre();
  if ($upgrade && read_yes_no("Upgrade ID3v1 tag to ID3v2 for $file?", 1))
  {
   set_tag($file, $tag);
  }
 }
 else
 {
  warn "No ID3 TAG info in $file, creating it";
  $tag = {
      TIT2 => '',
      TPE1 => '',
      TALB => '',
      TYER => 9999,
      COMM => '',
      };
 }
 print "Got tag ", Dumper $tag
  if $config->DEBUG();
 return $tag;
}
# }}}

惟一一个稍微与众不同的函数是 read_yes_no() ,可以给它一个 Y1 的默认参数来使默认值为真,任何其他的参数都会使默认值为假。这样,当用户按下回车键或者空格键时,我可以让 read_yes_no() 函数接受不同的默认值。另外,Backspace 键或 Delete 键将使默认值反转。这段代码不华丽,但很实用。

autotag.pl 的开头部分

应用程序 autotag.pl 以一些初始化例程开始。

清单 3. 初始化
use constant SEARCH_ALL   => 'all';
my %freedb_searches = (
   artist  => { keywords => [], abbrev => 'I', tagequiv => 'TPE1' },
   title   => { keywords => [], abbrev => 'T', tagequiv => 'TALB' },
   track   => { keywords => [], abbrev => 'K', tagequiv => 'TIT2' },
   rest    => { keywords => [], abbrev => 'R', tagequiv => 'COMM' },
      );
# maps ID3 v2 tag info to WebService::FreeDB info
my %info2freedb = (
   TALB  => 'cdname',
   TPE1  => 'artist',
      );
my %supported_frames = (
   TIT1 => 1,
   TIT2 => 1,
   TRCK => 1,
   TALB => 1,
   TPE1 => 1,
   COMM => 1,
   WXXX => 1,
   TYER => 1,
      );
my @supported_frames = keys %supported_frames;
my $term = new Term::ReadLine 'Input> '; # global input

EARCH_ALL 是一个常数,当用户想在任何地方搜寻一个词的时候,比如曲目名、艺术家名等,就会使用它。为了防止有人想把它改为另外某个值,我把它设定为常数,但它也可能已经被硬编码为“all”。

%freedb_searches 散列将 FreeDB 字段映射到有关它们的信息上,包括 ID3v2 标签元素。例如,它说明 FreeDB 怎样称呼那些在 MP3 标签中被称为“TPE1”的“artist”。在该散列条目中的“abbrev”字段被用来定义命令行开关,这样,随后我可以基于 %freedb_searches 信息定义一个 -artist 开关,它可以被简写为 -i

%info2freedb 散列将光盘中的所有曲目的 FreeDB 字段都映射到 ID3v2 字段。它们不是 %freedb_searches 中的字段,这是一种不同的映射,它表明,对于一个光盘集的所有曲目而言,“cdname”和 “artists”(也分别被称为“TALB”和“TPE1”)是相同的。

我将用 %supported_frames 散列和 @supported_frames 列表来表示我支持哪些 ID3v2 标签元素。我是从该列表生成了这个散列,而不是从该散列中获得这个列表(解释两者之间的差别离题太远,所以不再赘述)。大规模加注标签时,以及在编写 ID3v2 标签时,都要用到已获支持的框架(我只是修改已获支持的框架而已)。

最后,为了让用户在整个应用程序中输入数据,我创建了一个 Term::ReadLine 对象。

下面,我初始化 AppConfig 选项,这样做虽然加重了我的负担,但是有益的。

清单 4. AppConfig 的初始化
# {{{ set up AppConfig and process -help
my $config = AppConfig->new();
$config->define(
   DEBUG       =>
   { ARGCOUNT => ARGCOUNT_ONE, DEFAULT => 0, ALIAS => 'D' },
   CONFIG_FILE       =>
   { ARGCOUNT => ARGCOUNT_ONE, DEFAULT => 0, ALIAS => 'F' },
   HELP        =>
   { ARGCOUNT => ARGCOUNT_NONE, DEFAULT => 0, ALIAS => 'H' },
   DUMP        =>
   { ARGCOUNT => ARGCOUNT_NONE, DEFAULT => 0 },
   ACCEPT_ALL  =>
   { ARGCOUNT => ARGCOUNT_NONE, DEFAULT => 0, ALIAS => 'C' },
   DRYRUN      =>
   { ARGCOUNT => ARGCOUNT_NONE, DEFAULT => 0, ALIAS => 'N' },
   GUESS_TRACK_NUMBERS_ONLY  =>
   { ARGCOUNT => ARGCOUNT_NONE, DEFAULT => 0, ALIAS => 'G' },
    STRIP_COMMENT_ONLY =>
    { ARGCOUNT => ARGCOUNT_NONE, DEFAULT => 0, ALIAS => 'SC' },
   MASS_TAG_ONLY =>
   { ARGCOUNT => ARGCOUNT_HASH, ALIAS => 'M' },
   RENAME_ONLY =>
   { ARGCOUNT => ARGCOUNT_NONE, DEFAULT => 0, ALIAS => 'RO' },
   RENAME_MAX_CHARS =>
   { ARGCOUNT => ARGCOUNT_ONE, DEFAULT => 30},
   RENAME_FORMAT =>
   { ARGCOUNT => ARGCOUNT_ONE, DEFAULT => '%a-%t-%n-%c-%s.mp3'},
   RENAME_BADCHARS =>
   { ARGCOUNT => ARGCOUNT_LIST, ALIAS => 'RB' },
   RENAME_REPLACECHARS =>
   { ARGCOUNT => ARGCOUNT_LIST, ALIAS => 'RR' },
   RENAME_REPLACEMENT =>
   { ARGCOUNT => ARGCOUNT_ONE, DEFAULT => '_' },
   FREEDB_HOST =>
   { ARGCOUNT => ARGCOUNT_ONE, DEFAULT => 'http://www.freedb.org', },
   OR =>
   { ARGCOUNT => ARGCOUNT_NONE, DEFAULT => '0', },
   SEARCH_ALL()  =>
   { ARGCOUNT => ARGCOUNT_LIST, ALIAS => 'A' },
      );
foreach my $search (keys %freedb_searches)
{
 $config->define($search => {
      ARGCOUNT => ARGCOUNT_LIST,
      ALIAS => $freedb_searches{$search}->{abbrev},
      });
}
$config->args();
$config->file($config->CONFIG_FILE())
 if $config->CONFIG_FILE();
unless (scalar @{$config->RENAME_BADCHARS()})
{
 push @{$config->RENAME_BADCHARS()}, split(//, "\"`!'?&[]()/;\n\t");
}
unless (scalar @{$config->RENAME_REPLACECHARS()})
{
 push @{$config->RENAME_REPLACECHARS()}, split(//, " ");
}
if ($config->HELP())
{
 print <<EOHIPPUS;
$0 [options] File1.mp3 File2.mp3 ...
Options:
 -help (-h)          : print this help
 -config_file (-f) N : use this config file, see AppConfig module docs for format
 -debug (-d) N       : print debugging information (level N, 0 is lowest)
 -dump               : just dump the list of albums and tracks within them
 -dryrun (-n)        : do everything but modify the MP3 files
 -freedb_host H      : set the FreeDB host, default "www.freedb.org"
 -or                 : search for keyword A or keyword B, not A and B as usual
 -accept_all (c)     : accept all search results for consideration for each file,
                       also accept all renames without asking
 -rename_badchars (-rb) A -rb B     : characters A and B to remove when renaming
 -rename_replacechars (-rr) A -rr B : characters A and B to replace
                                      when renaming
 -rename_maxchars N : use at most this many characters from a tag
                      element when renaming, default: ${\$config->RENAME_MAX_CHARS()}
 -rename_replacement X : character to use when replacing,
                      default: [${\$config->RENAME_REPLACEMENT()}]
 -rename_format (-f) F : format for renaming; default "${\$config->RENAME_FORMAT()}"
                         %a -> Artist
                         %t -> Track number
                         %n -> Album name
                         %c -> Comment
                         %s -> Song title
 -guess_track_numbers_only (-g) : guess track numbers using the file
                     name, then exit
 -rename_only (-ro)  : rename tracks using the given format (see
                       -rename_format), then exit
 -mass_tag_only (-m) A=X -m B=Y : mass-tag files (tag element A is X,
                                  B is Y), then exit (tag elements
                                  available: @supported_frames)
 -strip_comment_only (-sc) : strip comments and URLs, then exit
Repeatable options (you can specify them more than once, K is the keyword):
 -all (-a)    K : search everywhere
 -artist (-i) K : search for these artists
 -title (-t)  K : search for these titles
 -track (-k)  K : search for these tracks
 -rest (-r)   K : search for these keywords everywhere else
Note that the repeatable options are cumulative, so artist A and title
B will produce matches for A and B, not A or B. In the same way,
artist A and artist B will produce matches for A and B, not A or B.
If you want to match A or B terms, use -or, for instance:
$0 -or -artist "pink floyd" -artist "fred flintstone"
EOHIPPUS
 exit;
}
# }}}

是的,所有这些代码都是初始化命令行选项的。通过使用 AppConfig ,可以在整个程序中使用和修改这些选项。使用 AppConfig 还有很多好处,不过这些内容超出了本文的范围(有关 AppConfig 的更多信息,请参阅 参考资料)。

另外,我还使用 %freedb_searches 散列中的条目来创建合适的配置选项,这样可以使用户和程序员更轻松一些。

在加载配置文件以后,如果用户指定了它,那么就用有意义的默认值来植入字符置换数组和不良字符数组。

最后,处理 -help 开关。注意,通过变量插入不同选项的默认值是如何在帮助文本内打印出来大的。这样就形成了可读性非常强的帮助信息。我总是在增加新的特性之后(但有时候是在之前)立即更新帮助信息。我认为,帮助文本应该和程序的功能同步,否则人们将不理解程序,也不知道帮助文本说了些什么。autotag.pl 程序特别需要更多的文档说明,POD 风格的文档应该比较合适。在您阅读本文时,这样的文档可能已经有了。POD 文档是脚本的一部分,因此下载的 autotag.pl(请参阅 参考资料)将包括 POD 文档(如果我已经将它写入的话)。

与 ID3v2 标签相关的函数

get_tag() 函数是 autotag.pl 的基本函数。给出一个 MP3 文件名,它就会根据该文件构建一个散列标签。如果标签只是 ID3v1 标签, get_tag() 函数将会免费把它升级为 ID3 标签(多么好的交易!)。如果没有 ID3 标签, get_tag() 函数将创建一个。而且, get_tag() 知道分别查看 COMM 和 WXXX 元素的 Text 和 URL 子元素。

清单 5. get_tag() 函数
# {{{ get_tag: get a ID3 V2 tag, using V1 if necessary
sub get_tag
{
 my $file    = shift @_;
 my $upgrade = shift @_;
 my $mp3 = MP3::Tag->new($file);
 return undef unless defined $mp3;
 $mp3->get_tags();
 my $tag = {};
 if (exists $mp3->{ID3v2})
 {
  my $id3v2 = $mp3->{ID3v2};
  my $frames = $id3v2->supported_frames();
  while (my ($fname, $longname) = each %$frames)
  {
   # only grab the frames we know
   next unless exists $supported_frames{$fname};
   $tag->{$fname} = $id3v2->get_frame($fname);
   delete $tag->{$fname} unless defined $tag->{$fname};
   $tag->{$fname} = $tag->{$fname}->{Text} if $fname eq 'COMM';
   $tag->{$fname} = $tag->{$fname}->{URL} if $fname eq 'WXXX';
   $tag->{$fname} = '' unless defined $tag->{$fname};
  }
 }
 elsif (exists $mp3->{ID3v1})
 {
  warn "No ID3 v2 TAG info in $file, using the v1 tag";
  my $id3v1 = $mp3->{ID3v1};
  $tag->{COMM} = $id3v1->comment();
  $tag->{TIT2} = $id3v1->song();
  $tag->{TPE1} = $id3v1->artist();
  $tag->{TALB} = $id3v1->album();
  $tag->{TYER} = $id3v1->year();
  $tag->{TRCK} = $id3v1->track();
  $tag->{TIT1} = $id3v1->genre();
  if ($upgrade && read_yes_no("Upgrade ID3v1 tag to ID3v2 for $file?", 1))
  {
   set_tag($file, $tag);
  }
 }
 else
 {
  warn "No ID3 TAG info in $file, creating it";
  $tag = {
      TIT2 => '',
      TPE1 => '',
      TALB => '',
      TYER => 9999,
      COMM => '',
      };
 }
 print "Got tag ", Dumper $tag
  if $config->DEBUG();
 return $tag;
}
# }}}

set_tag() 函数是 get_tag() 函数的兄弟。它编写 ID3v2 标签,查看 COMM 和 WXXX 框架的子元素。它接受散列引用,比如 get_tag() 函数可能产生的那些散列引用。

清单 6. set_tag() 函数
# {{{ set_tag: set a ID3 V2 tag on a file
sub set_tag
{
 my $file = shift @_;
 my $tag  = shift @_;
 my $mp3 = MP3::Tag->new($file);
 print Dumper $tag;
 my $tags = $mp3->get_tags();
 my $id3v2;
 if (ref $tags eq 'HASH' && exists $tags->{ID3v2})
 {
  $id3v2 = $tags->{ID3v2};
 }
 else
 {
  $id3v2 = $mp3->new_tag("ID3v2");
 }
 my %old_frames = %{$id3v2->get_frame_ids()};
 foreach my $fname (keys %$tag)
 {
  $id3v2->remove_frame($fname)
   if exists $old_frames{$fname};
  if ($fname eq 'WXXX')
  {
   $id3v2->add_frame('WXXX', 'ENG', 'FreeDB URL', $tag->{WXXX}) ;
  }
  elsif ($fname eq 'COMM')
  {
   $id3v2->add_frame('COMM', 'ENG', 'Comment', $tag->{COMM}) ;
  }
  else
  {
   $id3v2->add_frame($fname, $tag->{$fname});
  }
 }
 $id3v2->write_tag();
 return 0;
}
# }}}

print_tag_info() 函数简单地打印输出标签的摘要。不像我在 autotag.pl 程序中的其他地方使用的 Data::Dumper 函数(必须指出,有时没有必要使用), print_tag_info() 函数可以提供漂亮的、面向用户的散列标签元素的打印输出。注意,该函数接受散列引用,而不是实际的文件名。

给出文件名和某些可能的 ID3 标签信息, guess_track_number() 函数和 guess_artist_and_track() 函数会尽力工作。注意, guess_track_number() 函数知道曲目的数量很少大于 30。

清单 7. print_tag_info()、 guess_track_number()、和 guess_artist_and_track() 函数
# {{{ print_tag_info: print the tag info
sub print_tag_info
{
 my $filename = shift @_;
 my $tag      = shift @_;
 my $extra    = shift @_ || 'Track info';
 # argument checking
 return unless ref $tag eq 'HASH';
 print "$extra for '$filename':\n";
 foreach (keys %$tag)
 {
  printf "%10s : %s\n", $_, $tag->{$_};
 }
}
# }}}
# {{{ guess_track_number: guess track number from ID3 tag and file name
sub guess_track_number
{
 my $filename = shift @_;
 my $tag      = shift @_ || return undef;
 $filename = basename($filename);   # directories can contain confusing data
 # first try to guess the track number from the old tag
 if (exists $tag->{TRCK} && contains_word_char($tag->{TRCK}))
 {
  my $n = $tag->{TRCK} + 0;    # fix tracks like 1/10
  return $n;
 }
 elsif ($filename =~ m/([012]?\d).*\.[^.]+$/)
                     # now look for numbers in the filename (0 through 29)
 {
  print "Guessed track number $1 from filename '$filename'\n"
   if $config->DEBUG();
  return $1;
 }
 return undef; # if all else fails, return undef
}
# }}}
# {{{ guess_artist_and_track: guess artist and track from file name
sub guess_artist_and_track
{
 my $filename = shift @_;
 my $artist;
 my $track;
 $filename = basename($filename);   # directories can contain confusing data
 if ($filename =~ m/([^-_]{3,})\s*-\s*(.{3,})\s*\.[^.]+$/)
 {
  print "Guessed artist $1 from filename '$filename'\n"
   if $config->DEBUG();
  $artist = $1;
  $track = $2;
 }
 return ($artist, $track);
}
# }}}

我使用从 FreeDB 搜索中返回的数据来生成带有适当元素的匿名散列。虽然 WebService::FreeDB 字段和 ID3v2 标签元素之间的映射是试验性的,但它工作得很好。

清单 8. make_tag_from_freedb() 函数
# {{{ make_tag_from_freedb: make the ID3 tag info from a FreeDB entry
sub make_tag_from_freedb
{
 my $disc  = shift @_;
 my $track = shift @_;
 # argument checking
 return undef unless $track =~ m/^\d+$/;
 # note that the user inputs track "1" but WebService::FreeDB gives us that
 # track at position 0, so we decrement $track
 $track--;
 return undef unless exists $disc->{trackinfo};
 return undef unless exists $disc->{trackinfo}->[$track];
 my $track_data = $disc->{trackinfo}->[$track];
 return {
      TIT1 => $disc->{genre},
      TIT2 => $track_data->[0],
      TRCK => $track+1,
      TPE1 => $disc->{artist},
      TALB => $disc->{cdname},
      TYER => $disc->{year},
      WXXX => $disc->{url},
      COMM => $disc->{rest}||'',
   };
}
# }}}

大规模加注标签、大规模重命名、剥离注释和猜测曲目数量

autotag.pl 的主要功能是识别 MP3 文件。但在这个过程中,往往需要对很多组文件进行小的调整。输入 Four Autotagging Horsemen。

剥离注释是非常简单的过程。我使用 get_tag() 获得散列标签,清空 COMM 和 WXXX 字段,以及使用 set_tag() 将该标签写回。实际上,注释剥离可能已经通过大规模标签加注完成了,但这个函数使用得非常频繁,以至于使我感到有必要为它设置一个独立的选项。

猜测曲目数量也使相当简单的。获取散列标签,在该文件和散列标签上使用 guess_track_number() 函数,请求确认,然后将该标签写回到文件中。

在一系列文件上对多个键(例如 TALB)进行大规模标签加注操作。例如:

autotag.pl -mt "TALB=Best" *.mp3

于是,所有具有 mp3 扩展名的文件都在其 ID3v2 标签中指定了 TALB 值。当您拥有某个艺术家的全部乐曲的目录时,以及希望用该艺术家的名字标记所有这些乐曲时,采用大规模标签加注的方式是非常合适的。只有受支持的标签元素才可以大规模加注标签。再一次进行这样的过程:获取散列标签、进行修改,然后将它写回。这样做目的是使它的维护简单便利。

清单 9. 大规模加注标签、注释剥离和猜测曲目数量
# {{{ handle the one-shot options
if ($config->GUESS_TRACK_NUMBERS_ONLY() ||
    $config->STRIP_COMMENT_ONLY() ||
    scalar keys %{$config->MASS_TAG_ONLY()})
{
 foreach my $file (@ARGV)
 {
  my $tag = get_tag($file, 1);
  unless (defined $tag)
  {
   warn "No ID3 TAG info in '$file', skipping";
   next;
  }
  next if $config->DRYRUN();
  # delegate stripping comments to the mass tagging function
  if ($config->STRIP_COMMENT_ONLY())
  {
   $config->MASS_TAG_ONLY()->{COMM} = '';
   $config->MASS_TAG_ONLY()->{WXXX} = '';
  }
  if (scalar keys %{$config->MASS_TAG_ONLY()})
  {
   foreach (keys %{$config->MASS_TAG_ONLY()})
   {
    unless (exists $supported_frames{$_})
    {
     warn "Unsupported tag element $_ requested for mass tagging, skipping";
     next;
    }
    $tag->{$_} = $config->MASS_TAG_ONLY()->{$_};
   }
   set_tag($file, $tag);
  }
  else
  {
   my $track_number_guess = guess_track_number($file, $tag);
   next if $config->DRYRUN();
   if (defined $track_number_guess &&
              read_yes_no("Is track number $track_number_guess OK for '$file'?", 1))
   {
    $tag->{TRCK} = $track_number_guess;
    set_tag ($file, $tag);
   }
   else
   {
    warn "Could not guess a track number for file $file, sorry";
   }
  }
 }
 exit 0;
}
# }}}

噢,该介绍大规模重命名选项了。我之所以将这个问题留在最后,是因为这个问题最复杂。对于每个重命名参数而言,我将标签值中的每个“%”都表示为“{{{%}}}”,因为不这样的话,当后面跟随一个特殊的重命名参数时,“%”字符就可能被曲解。例如,用“100%true”作为曲目名,我们来看一看它如何变成“100%TRACKNAMErue”的,这里 TRACKNAME 是我从该散列标签中获取的曲目名。

大规模重命名也可消除不良的字符,代之以某些带有“_”的字符,以确保文件名合理。最后,除非通过命令行给出 -caccept_all )选项,否则 autotag.pl 将询问是否可以对文件重命名。

清单 10. 大规模重命名
# {{{ handle the -rename_only option
if ($config->RENAME_ONLY())
{
 foreach my $file (@ARGV)
 {
  my $tag = get_tag($file, 1);
                 # the extra parameter will ask us about upgrading V1 to V2
  unless (defined $tag)
  {
   warn "No ID3 TAG info in '$file', skipping";
   next;
  }
  my %map = (
     '%c' => 'COMM',
     '%s' => 'TIT2',
     '%a' => 'TPE1',
     '%t' => 'TALB',
     '%n' => 'TRCK',
    );
  my $name = $config->RENAME_FORMAT();
  foreach my $key (keys %map)
  {
   my $tagkey = $map{$key};
   my $replacement = '';
   if (exists $tag->{$tagkey})
   {
    $replacement = substr $tag->{$tagkey}, 0, $config->RENAME_MAX_CHARS();
                    # limit to N characters
    if ($tagkey eq 'TRCK' && $replacement =~ m/^\d$/)
    {
     $replacement = "0$replacement";
    }
   }
   $replacement =~ s/%/{{{%}}}/g;
                    # this is how we preserve %a in the fields, for example
   $name =~ s/$key/$replacement/;
  }
  $name =~ s/{{{%}}}/%/g;   # turn the {{{%}}} back into % in the fields
  print "The name after % expansion is $name\n" if $config->DEBUG();
  foreach my $char (map { quotemeta } @{$config->RENAME_BADCHARS()})
  {
   $name =~ s/$char//g;
  }
  print "The name after character removals is $name\n" if $config->DEBUG();
  my $newchar = quotemeta $config->RENAME_REPLACEMENT();
  foreach my $char (map { quotemeta } @{$config->RENAME_REPLACECHARS()})
  {
   $name =~ s/$char/$newchar/eg;
  }
  print "The name after character replacements is $name\n" if $config->DEBUG();
  if ($name eq $file)
  {
   # do nothing
   print "Renaming $file is unnecessary, it already answers to our high standards\n"
    if $config->DEBUG();
  }
  elsif (-e $name)
  {
   warn "Could not use name $name, it's already taken by an existing
                        file or directory $file";
  }
  elsif ($config->ACCEPT_ALL() || read_yes_no("Is name $name OK for '$file'?", 1))
  {
   next if $config->DRYRUN();
   print "Renaming $file -> $name\n";
   rename($file, $name);
  }
  else
  {
   # do nothing
  }
 }
 exit 0;
}
# }}}

结束语

本文的第 2 部分将讨论 autotag.pl 的主循环,介绍该程序的一般用法。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=22096
ArticleTitle=功能丰富的 Perl:: 趣谈 MP3 和 Perl,第 1 部分
publish-date=12182003