下面是我如何使用JAudiotagger库和我创建的Groovy脚本来分析我的音乐文件。
在我[之前的文章]中,我创建了一个分析音乐文件的目录和子目录的框架,使用了groovy.File 类,该类扩展并精简了java.File ,并简化了其使用。在这篇文章中,我使用开源的JAudiotagger库来分析音乐目录和子目录中的音乐文件的标签。
安装Java和Groovy
Groovy是基于Java的,需要安装Java。最近的、合适的Java和Groovy版本可能都在你的Linux发行版的软件库中。Groovy也可以直接从Apache基金会的网站上安装。对于Linux用户来说,一个不错的选择是SDKMan,它可以用来获取多个版本的Java、Groovy和许多其他相关工具。在这篇文章中,我使用了SDK的以下版本。
- Java:OpenJDK 11的11.0.12-open版本
- Groovy:版本3.0.8
回到问题上来
在我仔细翻录我的CD收藏和越来越多地购买数字下载的15年左右的时间里,我发现翻录程序和数字音乐下载供应商在给音乐文件打标签时都是一塌糊涂。有时,我的文件缺少对音乐播放器有用的标签,如ALBUMSORT 。有时,这意味着我的文件充满了我不关心的标签,如MUSICBRAINZ_DISCID ,导致一些音乐播放器以不明显的方式改变演示顺序,从而使一张专辑看起来有很多,或以奇怪的顺序排序。
鉴于我有近700张专辑中的近10,000首曲目,当我的音乐播放器能够以合理的可理解的顺序显示我的收藏时,那是相当不错的。因此,这个系列的最终目标是创建一些有用的脚本,以帮助识别缺失或不寻常的标签,并促进创建一个工作计划来解决标签问题。这个特别的脚本分析了音乐文件的标签,并创建了一个CSV文件,我可以把它加载到LibreOffice或OnlyOffice中去寻找问题。它不会查看丢失的cover.jpg 文件,也不会显示包含其他文件的专辑子目录,因为这在音乐文件层面上并不相关。
我的Groovy框架和JAudiotagger
再一次,从代码开始。和以前一样,我在脚本中加入了注释,反映了我通常给自己留下的(相对简略的)"注释说明"。
1 @Grab('net.jthink:jaudiotagger:3.0.1')
2 import org.jaudiotagger.audio.*
3 def logger = java.util.logging.Logger.getLogger('org.jaudiotagger');
4 logger.setLevel(java.util.logging.Level.OFF);
5 // Define the music library directory
6 def musicLibraryDirName = '/var/lib/mpd/music'
7 // These are the music file tags we are happy to see
8 // Some tags can occur more than once in a given file
9 def wantedFieldIdSet = ['ALBUM', 'ALBUMARTIST',
10 'ALBUMARTISTSORT', 'ARTIST', 'ARTISTSORT',
11 'COMPOSER', 'COMPOSERSORT', 'COVERART', 'DATE',
12 'GENRE', 'TITLE', 'TITLESORT', 'TRACKNUMBER',
13 'TRACKTOTAL', 'VENDOR', 'YEAR'] as LinkedHashSet
14 // Print the CSV file header
15 print "artistDir|albumDir|contentFile"
16 print "|${wantedFieldIdSet*.toLowerCase().join('|')}"
17 println "|other tags"
18 // Iterate over each directory in the music libary directory
19 // These are assumed to be artist directories
20 new File(musicLibraryDirName).eachDir { artistDir ->
21 // Iterate over each directory in the artist directory
22 // These are assumed to be album directories
23 artistDir.eachDir { albumDir ->
24 // Iterate over each file in the album directory
25 // These are assumed to be content or related
26 // (cover.jpg, PDFs with liner notes etc)
27 albumDir.eachFile { contentFile ->
28 // Initialize the counter map for tags we like
29 // and the list for unwanted tags
30 def fieldKeyCounters = wantedFieldIdSet.collectEntries { e ->
31 [(e): 0]
32 }
33 def unwantedFieldIds = []
34 // Analyze the file and print the analysis
35 if (contentFile.name ==~ /.*\.(flac|mp3|ogg)/) {
36 def af = AudioFileIO.read(contentFile)
37 af.tag.fields.each { tagField ->
38 if (tagField.id in wantedFieldIdSet)
39 fieldKeyCounters[tagField.id]++
40 else
41 unwantedFieldIds << tagField.id
42 }
43 print "${artistDir.name}|${albumDir.name}|${contentFile.name}"
44 wantedFieldIdSet.each { fieldId ->
45 print "|${fieldKeyCounters[fieldId]}"
46 }
47 println "|${unwantedFieldIds.join(',')}"
48 }
49 }
50 }
51 }
第1行是那些可怕的可爱的Groovy设施之一,它极大地简化了生活。事实证明,JAudiotagger的好心开发者在Maven中央资源库上提供了一个编译版本。在Java中,这需要一些XML仪式和配置。使用Groovy时,我只需使用@Grab注解,Groovy就能在幕后处理其余的事情。
第2行从JAudiotagger库中导入相关的类文件。
第3-4行配置JAudiotagger库以关闭日志记录。在我自己的实验中,默认级别是相当粗略的,任何使用JAudiotagger的脚本的输出都充满了日志信息。这很好,因为Groovy将脚本构建在一个静态主类中。我相信我不是唯一一个在某个实例方法中配置了日志记录器,却在实例方法返回后看到配置的垃圾被收集的人。
第5-6行来自第一部分介绍的框架。
第7-13行创建了一个LinkedHashSet,包含了我希望在每个文件中出现的标签列表(或者,至少,我可以在每个文件中出现)。我在这里使用了一个LinkedHashSet,这样标签就会被排序。
这是一个很好的时机来指出我到现在为止一直在使用的术语和JAudiotagger库中的类定义之间的差异。我所说的 "标签 "就是JAudiotagger所说的org.jaudiotagger.tag.TagField 实例。这些实例存在于org.jaudiotagger.tag.Tag 。因此,从JAudiotagger的角度来看,"标签 "是 "标签字段 "的集合。在本文的其余部分,我将遵循他们的命名惯例。
这个字符串的集合反映了之前用metaflac进行的一些挖掘。最后,值得一提的是,JAudiotagger的org.jaudiotagger.tag.FieldKey 使用"_"来分隔字段键,这似乎与org.jaudiotagger.tag.Tag.getFields() 所返回的字符串不兼容,所以我没有使用FieldKey 。
第14-17行打印CSV文件的标题。注意使用Groovy的*. 传播操作符,将toLowerCase() 应用于wantedFieldIdSet 的每个(大写)字符串元素。
第18-27行来自第1部分介绍的框架,下降到找到音乐文件的子目录中。
第28-32行为所需字段初始化一个计数器地图。我在这里使用计数器,因为一些标签字段在一个给定的文件中可以出现不止一次。注意使用wantedFieldIdSet.collectEntries ,以集合元素为键建立一个地图(键值e在括号内,因为它必须被评估)。我在这篇关于Groovy中地图的文章中详细解释了这一点。
第33行初始化了一个列表,用于积累不需要的标签字段ID。
第34-48行分析发现的任何FLAC、MP3或OGG音乐文件。
- 第35行使用Groovy的匹配操作符
==~和一个 "slashy "正则表达式来检查文件名模式。 - 第36行使用
org.jaudiotagger.AudioFileIO.read()读取音乐文件元数据到变量af中 - 第37-48行在元数据中发现的标签字段上循环。
- 第37行使用Groovy的
each()方法来遍历由af.tag.getFields()返回的标签字段列表,在Groovy中可以简写为af.tag.fields - 第38-39行计算所需标签字段ID的任何出现次数
- 第40-41行将一个不需要的标签字段ID的出现附加到不需要的列表中
- 第43-47行打印出计数和不需要的字段(如果有的话)。
- 第37行使用Groovy的
这就是了。
通常情况下,我将按以下方式运行这个程序。
$ groovy TagAnalyzer2.groovy > tagAnalysis2.csv
$
然后我把生成的CSV加载到电子表格中。例如,在LibreOffice Calc中,我进入工作表菜单,选择从文件中插入工作表。我将分隔符设为| 。在我的例子中,结果看起来是这样的。

我喜欢为一些音乐播放器定义ALBUMARTIST和ARTIST,这样当个别曲目的艺术家不同时,专辑中的文件就会被归为一组。这种情况发生在合辑中,也发生在一些有客座艺术家的专辑中,例如,ARTIST字段可能会说 "Tony Bennett and Snoop Dogg"(我想是我编的。 上图电子表格中的第22行和以后的行都没有指定专辑艺术家,所以我可能想在以后的工作中解决这个问题。
这是显示不需要的字段ID的最后一栏的样子。

请注意,这些标签可能有一定的意义,所以 "想要的 "列表被修改以包括它们。我将设置某种脚本来删除BPM、ARTWORKGUID、CATALOGUENUMBER、ISRC和PUBLISHER等字段ID。