Python 2.7 + Scripting Bridge 导出 iTunes Library 里音乐的 MetaInfo 与封面到 MongoDB

作为某个 Project 的一部分~(暂时不透露是什么,嘻嘻(⁎⁍̴̛ᴗ⁍̴̛⁎) ) 需要把 iTunes 里面的所有音乐的 MetaInfo 和封面导出到 MongoDB 中

MongoDB 上次已经已经在 Raspberry Pi 4 上编译部署好了~在 Raspberry Pi 4 上安装 64-bit MongoDB Server 服务

然后再配合很久以前玩过的 在 Python 里使用 Scripting Bridge 与 iTunes 交互,就可以达到目标了233333

当然需要注意的是,这里要使用的是 macOS 自带的 Python 2.7,因为 ScriptingBridge 只安装在了自带的 Python 2.7 里

真正代码的话,其实整体来说很简单,需要考虑的点是如何做到不重复写封面,因为——

  1. 目前 Scripting Bridge 与 iTunes 交互时,只能一首音乐一首音乐的依次遍历,不能直接按照专辑遍历
  2. 同一张专辑里,有的音乐可能包含多张封面
  3. 不同的专辑可能被我 assgin 过相同的封面

综合这几点考虑的话,那就只能每次拿到有封面的音乐之后,对它的每一张封面都计算 SHA256 摘要(这里暂且认为 SHA256 的空间足够大,不会产生碰撞),并在放进 global_sha256 前,检查是否已经有相同的 SHA256 存在其中。如果没有的话,才保存图片到磁盘中,并放到那首歌的 MetaInfo 中;如果在 global_sha256 中有的话,那么就再看那首歌的 MetaInfo 中有没有这个 SHA256(因为也许有人不小心添加了两张一样的封面到音乐里)。

在遍历完所有音乐之后,把这些 MetaInfo 写入到 JSON 文件中~(就像下面这样

{
  "album": "Cutie Panther", 
  "name": "夏、終わらないで。", 
  "artist": "BiBi (南條愛乃, Pile, 徳井青空)", 
  "cover": [
    "44f9b56091c7ca5b011cd9cb306eab21d4f854300c96347a0a7f3538cbeb9dcd-1"
  ], 
  "composer": "渡辺和紀", 
  "year": 0, 
  "sha256": [
    "44f9b56091c7ca5b011cd9cb306eab21d4f854300c96347a0a7f3538cbeb9dcd"
  ]
}

最后再导进 MongoDB 数据库就可以啦(当然需要安装一下 pymongo 库)~主要的就分为 2 个 stage ♪(´ε` )

python2.7 -m pip install --user pymongo
python2.7 iTunes.py -s 1
python2.7 iTunes.py --host raspberrypi.local -s 2

在传到 Raspberry Pi 的 MongoDB 之后验证一下~

#!/usr/bin/python
# -*- coding: utf-8 -*-

import argparse
import json
import hashlib
import hmac
import io
from pymongo import MongoClient
from ScriptingBridge import NSImage, SBApplication


def cover_filename(cover_data):
    """Compute the filename for the given cover

    This function accepts a data buffer and computes its SHA256

    Parameters
    ----------
    cover_data : data buffer
        Data buffer of any cover

    Returns
    -------
    signature_sha : str
        The SHA256 hex digest of the given input data
    """

    signature_sha = hmac.new("".encode('utf-8'), cover_data, hashlib.sha256).hexdigest()
    return signature_sha


def export_cover(save_at = "database.json"):
    """Export all covers exist in iTunes library

    Parameters
    ----------
    save_at : str
        Save the metainfo to a JSON file (None indicates 'do not save')

    Returns
    -------
    database : list
        A list that contains all metainfo of exported covers
    """

    # 允许的文件后缀
    allowed_suffixes = ["m4a", "mp3"]
    # 所有的信息
    database = []
    cover_info = {}
    #  iTunes 获取所有的项目
    iTunes = SBApplication.applicationWithBundleIdentifier_("com.apple.iTunes")
    tracks = iTunes.sources()[0].playlists()[0].tracks()
    # 一共有多少项
    tracks_amount = len(tracks)
    # 依次遍历
    for index in range(tracks_amount):
        # 显示当前正在处理的
        track = tracks[index]
        print(unicode("[{}/{}] {}").format(index + 1, tracks_amount, track.name().strip()))

        # 是否是音乐文件
        if str(track.location())[-3:] not in allowed_suffixes:
            # 若不是则到下一项
            continue

        # 获取封面
        artworks = track.artworks()
        # 是否有至少一张封面图
        if len(artworks) == 0:
            # 若不是则到下一项
            continue

        #  track 所对应的信息
        info = {
            "name": track.name().strip(),
            "artist": track.artist().strip(),
            "year": track.year(),
            "composer": track.composer().strip(),
            "album": track.album().strip()
        }

        # 计算对应的 SHA256
        info["sha256"] = []

        #  track 所对应的封面
        for artwork in artworks:
            # 获取 NSImage 的数据
            data = artwork.data()
            if isinstance(data, NSImage):
                data = data.TIFFRepresentation()
                # 获取该封面的 SHA256
                cover_sha256 = cover_filename(data)
                # 如果这张封面已经有了
                if cover_sha256 in info["sha256"]:
                    # 就看下一个封面
                    continue
                # 否则就将当前封面的 SHA256 加入进去
                info["sha256"].append(cover_sha256)
                # 否则生成文件名
                filename = "{}-{}".format(cover_sha256, len(info["sha256"]))
                # 写入目录
                data.writeToFile_atomically_("cover/{}.tiff".format(filename), True)

        # 是否确实读到并保存了一张或以上的封面
        if len(info["sha256"]) == 0:
            # 没有则到下一项
            continue

        # 将该 track 添加到数据库
        database.append(info)

    # 保存 JSON
    if save_at is not None:
        with io.open(save_at, "w", encoding = "utf-8") as f:
            f.write(unicode(json.dumps(database, ensure_ascii = False, indent = 2)))

    # 返回数据库
    return database


def import_to_mongodb(data_source, host = "localhost", port = 27017, database = "animecover", collection = "songs",):
    """Import data to MongoDB

    Parameters
    ----------
    data_source : list
        The metainfo of all exported covers

    host : str
        MongoDB host

    port : int
        MongoDB port

    Returns
    -------
    result : InsertManyResult
        The result of insertion
    """

    # 连接 MongoDB
    client = MongoClient(host, port)
    # 使用数据库
    db = client[database]
    #  `collection` 中放入记录
    result = db[collection].insert_many(data_source)
    return result


def arg_parse():
    """Parse command line arguments"""

    parser = argparse.ArgumentParser()
    parser.add_argument("-o", "--output", type = str, default = "database.json", help = "Save all metainfo of exported covers to file")
    parser.add_argument("--host", type = str, default = "localhost", help = "MongoDB host")
    parser.add_argument("-p", "--port", type = int, default = 27017, help = "MongoDB post")
    parser.add_argument("-d", "--database", type = str, default = "animecover", help = "Name of MongoDB database")
    parser.add_argument("-c", "--collection", type = str, default = "songs", help = "Name of collection in database")
    parser.add_argument("-s", "--stage", type = int, default = 1, help = "Stages 1/2")
    args = parser.parse_args()
    return args


def main():
    args = arg_parse()
    if args.stage == 1:
        # stage 1
        # export all covers exist in iTunes library
        # and save metainfo
        export_cover(args.output)
    elif args.stage == 2:
        # stage 2
        # import all metainfo to MongoDB
        with io.open(args.output, "r", encoding = "utf-8") as f:
            data = json.loads(f.read())
            result = import_to_mongodb(data, args.host, args.port, args.database, args.collection)
            print("{} of {} successfully imported".format(len(result.inserted_ids), len(data)))


if __name__ == "__main__":
    main()

Leave a Reply

Your email address will not be published. Required fields are marked *

twelve + fourteen =