【Python】 获取MP3信息replica

replica

  初衷是想要整理iphone中的音乐。IOS(我自己的手机还是IOS8.3,新版本的系统可能有变化了)自带的音乐软件中所有音乐文件都存放在/var/mobile/Media/iTunes_Control里面。不过很令人抓狂的是首先这个目录被分隔成了从F00-Fxx的多个子目录,我的手机上总共到F49,mp3文件都放在这些子目录中。其次,mp3文件名全部都被点窜了,是看起来毫无规律的随机四位大写字母。每隔一段时间我都想从手机中把音乐备份出来然后放到电脑上,但是不知道文件名的话维护起来很麻烦。所以需要一个小程序来根据mp3的信息改变文件名。

  程序的目标非常简单,就是首先提取MP3文件中的信息,主要提取其歌名和演唱者两个字段,然后把这个文件重命名成 “歌名 - 演唱者.mp3”这种格式。

  

■  关于mp3信息的提取方法

  mp3除了音频信息的部分外,在文件的某些地方还会存放有关于这个音频的一些基本信息比如作者,创作时间,专辑名,曲序号,专辑图片等等。这些信息被统称为ID3标签。ID3标签被分成两代,第一代ID3v1只存储一些很简单的信息,占用文件末尾的128个字节(下面的直接读文件的就是默认是ID3v1的情况)。而ID3v2位于文件的开头,并且包含很全的信息比如加上一张专辑插图。

  网上搜到最多的使用ID3或者类似的一些模块进行提取,也有很多人根据mp3的文件结构特征直接对文件进行读取操作然后过滤出信息。因为我手上的资源很杂而且试了一下第二种方法发现很多文件提取信息都不全或者错误,所以还是用了第一种方法。下面这个是从网上摘来的,某种比较简洁的第二种方法示例:这个函数读取一个文件,然后从文件内容的特定位置解析出tag信息并返回一个字典。

def getID3(filename):
    fp = open(filename, 'r')
    fp.seek(-128, 2)

    fp.read(3) # TAG iniziale
    title   = fp.read(30)
    artist  = fp.read(30)
    album   = fp.read(30)
    anno    = fp.read(4)
    comment = fp.read(28)

    fp.close()

    return {'title':title, 'artist':artist, 'album':album, 'anno':anno}

  和网上很多人用ID3或者eyed3之类的模块不同,我用了个相对比较冷门的replica,他一共就只有三个模块文件cli.py , tagger.py , cloner.py。我们用到的主要是tagger。基本用法如下:

from replica import tagger

tags = tagger.get_tags("PATH.mp3")  #获取一个mp3文件的ID3标签信息

tagger.set_tags(tags,"ANOTHER.mp3")

  get_tags方法得到的是一个mutagen的MP3对象(replica是基于mutagen的,mutagen是更基本一些的音频处理模块)。这个对象所属的类应该实现了__getattr__方法,所以你可以像一个字典一样去访问这个对象中的一些键值。而如果打印这个这个对象看到的就是一个字典:

for k,v in tags.items():
    if k == u"APIC:":  #跳过U'APIC:'这个键是因为这个键的值是专辑图片,如果用字符来表示的话太大了这里显示不下
        continue
print k,v
print repr(k),repr(v) ###打印结果###
TDRC 2011
TIT2 ゆりゆららららゆるゆり大事件
ゆりゆららららゆるゆり 大事件
TRCK 1/4
TPE1 七森中☆ごらく部
TALB ゆりゆららららゆるゆり大事件
TSRC JPPC01101395
TCON Anime
TXXX:DISCID 28036204

###repr结果### 'TDRC' TDRC(encoding=<Encoding.LATIN1: 0>, text=[u'2011'])
'TIT2' TIT2(encoding=<Encoding.UTF16: 1>, text=[u'\u3086\u308a\u3086\u3089\u3089\u3089\u3089\u3086\u308b\u3086\u308a\u5927\u4e8b\u4ef6'])
u'USLT::eng' USLT(encoding=<Encoding.UTF16: 1>, lang='eng', desc=u'', text=u'\u3086\u308a\u3086\u3089\u3089\u3089\u3089\u3086\u308b\u3086\u308a\r\u3086\u308a\u3086\u3089\u3089\u3089\u3089\u3086\u308b\u3086\u308a\r\u3086\u308a\u3086\u3089\u3089\u3089\u3089\u3086\u308b\u3086\u308a \u5927\u4e8b\u4ef6\r\u3066\u3093\u3066\u3053\u307e\u3044\u306e\u4eca\u65e5\u660e\u65e5\u5927\u7206\u767a\r\u305d\u3093\u3067\u8eab\u9577\u4f38\u3073\u306a\u3044\u3084\u3042\u3042\u3069\u3046\u3057\u3088\r\u7518\u3044\u3082\u306e\u98df\u3079\u3059\u304d\u30c6\u30fc\u30de\u30d0\u30fc\u30af\r\u307b\u3044\u3058\u3083\u96a0\u305b\u3088\u4e59\u5973\u3067\u3069\u3046\u3058\u3083\u308d\r\u90e8\u6d3b\u52d5\u672c\u756a\r\u3057\u3081\u3057\u3081\u7121\u9045\u523b\r\u672c\u696d\u5b78\u696d\u306a\u306b\u305d\u308c\r\u305d\u3093\u306a\u306e\u305c\u3093\u305c\u3093\u305c\u3093\u305c\u3093\u305c\u3093\u305c\u3093\u98df\u3079\u308c\u306a\u3044\r\u685c\u54b2\u304d \uff08\u685c\u54b2\u304d\uff09\r\u685c\u6563\u308a \uff08\u685c\u6563\u308a\uff09\r\u660e\u65e5\u3082\u3044\u3044\u65e5\u3068\u6b4c\u3046\u3088\r\u541b\u304c\u597d\u304d \uff08\u541b\u304c\u597d\u304d\uff09\r\u541b\u304c\u3044\u3044 \uff08\u541b\u304c\u3044\u3044\uff09\r\u660e\u65e5\u3082\u3044\u305f\u3044\u3068\u601d\u3046\u3088\r\u6700\u7d42\u624b\u6bb5\u3067\u5c40\u7720\u308a\r\u3086\u308a\u3086\u3089\u3089\u3089\u3089\u3086\u308b\u3086\u308a\r\u3086\u308a\u3086\u3089\u3089\u3089\u3089\u3086\u308b\u3086\u308a\r\u3086\u308a\u3086\u3089\u3089\u3089\u3089\u3086\u308b\u3086\u308a \u5927\u4e8b\u4ef6(\u3060\u3044\u3058\u3051\u3093)\r\u3044\u308d\u306f\u306b\u307b\u3078\u3068\u3067\u304a\u307f\u304f\u3058Get you\uff01\u3042\u308a\u3089\u3073\u3085\u30fc\r\u306b\u3083\u3093\uff01\u306b\u3083\u3093\uff01\u8001\u306b\u3083\u3093\uff01\u82e5\u306b\u3083\u3093\uff01\u7537\u306b\u3083\uff01\u5973\u3093\uff01\r\u5fc5\u6b7b\u306b\u5bc6\u66f8\u3092\u767a\u5c04\u3067\u5fc5\u4e2d\u6388\u696d\u4e2d\r\u3067\u3082\u306d\u3042\u3089\u3089\u3089\u30c1\u30e7\u30fc\u30af\u304c\u30df\u30b5\u30a4\u30eb\r\u864e\u7a74\u306b\u5165\u3089\u306a\u3044\r\u96e8\u306a\u3089\u30cf\u30ec\u30eb\u30e4\r\u30ab\u30e9\u30aa\u30b1\u5272\u9ad8\u306a\u306b\u305d\u308c\r\u305d\u3093\u306a\u306e\u305c\u3063\u305f\u3044\u305c\u3063\u305f\u3044\u305c\u3063\u305f\u3044\u98df()\u3079\u308c\u306a\u3044\uff01\uff01\r\u685c\u54b2\u304d \uff08\u685c\u54b2\u304d\uff09\r\u685c\u6563\u308a \uff08\u685c\u6563\u308a\uff09\r\u660e\u65e5\u3082\u3044\u3044\u65e5\u3068\u6b4c\u3046\u3088\r\u541b\u304c\u597d\u304d \uff08\u541b\u304c\u597d\u304d\uff09\r\u541b\u304c\u3044\u3044 \uff08\u541b\u304c\u3044\u3044\uff09\r\u660e\u65e5\u3082\u3044\u305f\u3044\u3068\u601d\u3046\u3088\r\u8fd1\u6240\u306e\u30ef\u30f3\u30b3\u3068\u683c\u95d8\r\u3086\u308a\u3086\u3089\u3089\u3089\u3089\u3086\u308b\u3086\u308a\r\u3086\u308a\u3086\u3089\u3089\u3089\u3089\u3086\u308b\u3086\u308a\r\u3086\u308a\u3086\u3089\u3089\u3089\u3089\u3086\u308b\u3086\u308a \u5927\u4e8b\u4ef6\r\u30c1\u30e3\u30a4\u30e0\u304c\u76ee\u899a\u307e\u3057\rKIRA\u2606KIRA\u2606TSUN-DELE\r\u751f\u30af\u30ea\u30fc\u30e0\u60d1\u661f \u306a\u306b\u305d\u308c\r\u305d\u308c\u306a\u3089\u307b\u3093\u3068\u306b\u307b\u3093\u3068\u306b\u307b\u3093\u3068\u306b\u3084\u3081\u308c\u306a\u3044\uff01\u3084\u3081\u308c\u306a\u3044\uff01\uff01\r\u685c\u54b2\u304d \uff08\u685c\u54b2\u304d\uff09\r\u685c\u6563\u308a \uff08\u685c\u6563\u308a\uff09\r\u660e\u65e5\u3082\u3044\u3044\u65e5\u3068\u6b4c\u3046\u3088\r\u541b\u304c\u597d\u304d \uff08\u541b\u304c\u597d\u304d\uff09\r\u541b\u304c\u3044\u3044 \uff08\u541b\u304c\u3044\u3044\uff09\r\u660e\u65e5\u3082\u3044\u305f\u3044\u3068\u601d\u3046\u3088\r\u6700\u7d42\u624b\u6bb5(\u3067\u5c40\u7720\u308a\r\u8fd1\u6240\u306e\u30ef\u30f3\u30b3\u3068\u683c\u95d8\r\u9769\u547d\u8d77\u3053\u3057\u3066\u5352\u696d\r\u3086\u308a\u3086\u3089\u3089\u3089\u3089\u3086\u308b\u3086\u308a\r\u3086\u308a\u3086\u3089\u3089\u3089\u3089\u3086\u308b\u3086\u308a\r\u3086\u308a\u3086\u3089\u3089\u3089\u3089\u3086\u308b\u3086\u308a \u5927\u4e8b\u4ef6\r')
'TRCK' TRCK(encoding=<Encoding.LATIN1: 0>, text=[u'1/4'])
'TPE1' TPE1(encoding=<Encoding.UTF16: 1>, text=[u'\u4e03\u68ee\u4e2d\u2606\u3054\u3089\u304f\u90e8'])
'TALB' TALB(encoding=<Encoding.UTF16: 1>, text=[u'\u3086\u308a\u3086\u3089\u3089\u3089\u3089\u3086\u308b\u3086\u308a\u5927\u4e8b\u4ef6'])
'TSRC' TSRC(encoding=<Encoding.UTF16: 1>, text=[u'JPPC01101395'])
'TCON' TCON(encoding=<Encoding.UTF16: 1>, text=[u'Anime'])
u'TXXX:DISCID' TXXX(encoding=<Encoding.UTF16: 1>, desc=u'DISCID', text=[u'28036204'])

  字典本身打印出来是这样:

{u'APIC:':'关于图片信息的内容','TSRC': TSRC(encoding=<Encoding.UTF16: 1>, text=[u'JPPC01101395']), 'TCON': TCON(encoding=<Encoding.UTF16: 1>, text=[u'Anime']), u'TXXX:DISCID': TXXX(encoding=<Encoding.UTF16: 1>, desc=u'DISCID', text=[u'28036204']),xxxx还有一些,意思一下。。}

  结合各个键的值大概就可以猜出来这个键是什么意思了。对于没有设置某个标签信息的文件而言,它的tags对象中就不会有相关的键。值其实是一些对象,比如标题标签的键是TIT2,值就是一个TIT2对象,其有两个关键的属性,分别是TIT2.encoding和TIT2.text分别指出了显示标题时用的编码格式和标题文本组成的单个元素的列表。

  这些对象的全图鉴可以参见mutagen/id3/_frames.py的源码。只是关于如何改变或创建这些对象,改变一个既有的MP3的标签信息这一方面还有待研究(其实是mutagen的内容了)

  其实就论replica这个模块的用法的话就是以上了,然而我在之后的编写过程中又遇到了各种各样的坑,比如编码问题,windows系统对于文件名的要求等等,所以打算继续写下去。

 

■  关于文本信息的编码

  在tag信息的对象中,大多承载文字信息的对象都有encoding和text两个属性,且text属性中是一个单unicode元素的列表。其实这两个属性联合起来就表示了这个unicode应该用哪种编码进行encode才能成为原本的信息。

  首先来看encoding这属性。这个属性其实是维护了一个mutagen中的Encoding对象,这个对象可以在源码中看到(位于mutagen/id3/_specs.py),把四种编码分别用了一个数字表示,在知道了对应关系之后我们可以在自己的脚本里添加一个同样的字典使得使用更加方便:

ENCODING = {0: "latin1", 1: "utf16", 2: "utf16be", 3: "utf8"}

 

  因为从相关对象中取到的属性本质就是一个数,比如TIT2.Encoding其实就是1.

  在四种编码格式中,比较特殊的是latin1这种格式。latin1就是ISO-8859-1,之前在en/decode以及print探索那篇文章中也提到过,被latin1 decode出来的unicode只能被latin1 encode成str,并且encode得到的东西的编码格式和最原先的是一样的。

 

■  windows中文件名的规则

  windows中的文件名不能含有字符 \/:*?"<>| 中的任意一个。如果含有这些错误的话在运用os.rename或者其他类似的重命名手段的时候会报错WindowsError123。另一方面,如果同目录下已经有相同名字文件存在时重命名会报错WindowsError183。

  一般而言,对windows上的文件进行命名的时候我们可以直接用unicode类型的字符串(如果愿意,也可以用gb这个系列的编码格式的字符串进行命名,不过能用unicode的情况下应该尽量用通用性更加好的unicode)。但是就这个程序而言,我们碰到了很多由latin1编码格式得到的unicode,很遗憾在处理如果用这些unicode直接去命名文件,会出现乱码。目前我能想到的解决办法是在进行命名之前进行一个判断,如果这个文本信息的编码格式是latin1的,那么在将其输出成文件名的一部分之前先把它encode("latin1"),如果不是,那么可以直接把它作为文件名的一部分去rename。

  

■  关于其他一些小改善

  至此整个小程序基本功能已经实现了,接下来就是一些业务逻辑的改善了。比如tag信息不全时怎么办,重命名失败时怎么办,增加处理进度提示,对于目录中非mp3文件的处理等等。最后整个脚本如下:

 

#!/usr/bin/env python
# coding=utf8
import os
import sys
import logging
import re
from replica import tagger

reload(sys)
sys.setdefaultencoding("gb18030")

ENCODING = {0: "latin1", 1: "utf16", 2: "utf16be", 3: "utf8"}

logging.basicConfig(filename="mp3.log", level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s ")


def modify_textual(string):
    return re.sub(u'[\\\/:\*\?"<>\|]', " ", string)


def process_one_song(songName):
    mp3 = tagger.get_tags(u"{songname}".format(songname=songName))
    try:
        artist = mp3.get("TPE1").text[0]
        enco_form = ENCODING.get(mp3.get("TPE1").encoding)
        if enco_form == "latin1":
            artist = artist.encode("latin1")
    except AttributeError as e:
        artist = u"未知艺术家"
    try:
        name = mp3.get("TIT2").text[0]
        enco_form = ENCODING.get(mp3.get("TIT2").encoding)
        if enco_form == "latin1":
            name = name.encode("latin1")
    except AttributeError as e:
        name = u"未知曲名"
    name = modify_textual(name)
    artist = modify_textual(artist)
    logging.info(u"{songname}".format(songname=songName) +
                 u"{filename}.mp3".format(
                         filename=os.path.dirname(songName) + os.sep.decode("utf8") + name + u"-" + artist))
    try:
        os.rename(u"{songname}".format(songname=songName), u"{filename}.mp3".format(
                filename=os.path.dirname(songName) + os.sep.decode("utf8") + name + u"-" + artist))
    except WindowsError as windowserror:
        logging.error(
                "error processing {name} for windows error [{error}]".format(name=songName, error=str(windowserror)))


def main():
    try:
        rootpath = sys.argv[1]
    except IndexError as e:
        rootpath = 'mp3'
    if not os.path.isdir(rootpath):
        print u"输入目录不存在"
        sys.exit(1)
    count = 0
    total = 0
    for root, dir, files in os.walk(rootpath):
        total += len(files)
    for root, dir, files in os.walk(rootpath):
        for file in files:
            filename = os.path.join(root.decode("gb18030"), file.decode("gb18030"))
            if os.path.splitext(file)[1] != '.mp3':
                os.remove(filename)
                continue
            count += 1
            print u"正在处理{percent:.0f}%的歌".format(percent=float(count) / total * 100)
            process_one_song(filename)

 

posted @ 2017-05-14 22:37  K.Takanashi  阅读(1825)  评论(0编辑  收藏  举报