CSDN博客

img wangaimin

perl与mp3

发表于2004/2/13 11:06:00  658人阅读

Teodor Zlatanov (tzz@bu.edu)
程序员,Gold Software Systems
2004 年 2 月

每一位自我陶醉的计算机和音乐爱好者都需要能够操纵 MP3 —— 娱乐性数字音乐的事实标准。在本文中,Ted 介绍了几种使用 autotag.pl 应用程序管理和操纵(搜索、标记、重命名和注释,等等)MP3 的方法。Ted 向读者详细介绍了此应用程序,描述了 CPAN 模块如何启用该应用程序。

对于现在了解计算机的音乐爱好者而言,操纵 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 之上。

MP3 标签的简单介绍
首先有音乐。然后出现计算机。计算机速度很慢且只能发出蜂鸣声。即使使用诸如 PC 扬声器这种很令人悲伤的工具(噢,我真希望成为 Apple 和 Amiga 用户),我们也编写程序生成音乐,在游戏和娱乐中使用。之后,声卡越来越好,办公室里到处发出环绕音响和 THX 认证的扬声器所发出的震耳欲聋的声音。

在硬件发展的同时,也产生了很多声音格式。.mid 适用于 MIDI 音调、.voc、.mod、.wav 等。专有的 MP3 格式(涉及到德国 Fraunhofer 学院拥有的很多专利)随着时间的推移流行开来 —— 它提供很好的压缩和性能。除 MP3 外还有许多格式,著名的有 Ogg Vorbis,但当今 MP3 似乎仍是音乐存储格式的最佳选择。

MP3 文件的一个优点是可以使用 ID3 标签来加注标签。文件内部是有关它的信息 —— 通常称为元数据。唱片集、艺术家、曲目名称、注释(使用 ID3 版本 1.1)甚至曲目数量都可以存储在 ID3 标签中,只要不超过特定字符数限制即可。

ID3 版本 1.1 的后续版本是 ID3 版本 2(简称为 ID3v2),除简单性外,后者几乎在所有方面都超过了前者。ID3v2 可以处理多种语言,在每个标签元素中存储任意长的数据,甚至将图片存储为标签的一部分。但不幸的是,使用 ID3v2 要了解到 TALB 是唱片集的名称,TIT2 是曲目数量。Ogg Vorbis 格式要花费很长时间才能识别,其中艺术家元素被称为...等等它吧...ARTIST!(公平地说,这仅仅是一个惯例 —— Ogg Vorbis 注释是无格式的)。不幸的是,现存的数十亿首 MP3 文件都不能在不损失质量的情况下转换为 Ogg Vorbis 格式或任何其他格式,因此,至少在接下来的 5 年里,您可能发现,我们不仅会使用下个“热门”格式,还要使用 MP3 文件。

我已非常努力地从实际的 ID3 标签中抽取标签作为内容。当时机来临时,修改 autotag.pl 会很容易,因此除 ID3 外,它还处理其他加注标签的格式。

基本的 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 的主循环,介绍该程序的一般用法。

参考资料

关于作者
Teodor Zlatanov 于 1999 年获取了波士顿大学计算机工程系硕士学位。他从 1992 年就开始做程序员,使用 Perl、Java、C 和 C++。他的兴趣在于开放源代码工作,致力于文本解析、3 层客户机-服务器数据库体系结构、UNIX 系统管理、CORBA 和项目管理。可以通过 tzz@bu.edu 与他联系。


阅读全文
0 0

相关文章推荐

img
取 消
img