#!/usr/bin/env python3 # -*- coding: utf-8 -*- import mutagen import mutagen.id3 import mutagen.mp4 import os import re import subprocess import sys import symuid # http://id3.org/id3v2.4.0-frames#4.1. TRACK_UUID_ID3_OWNER_ID = 'symuid' # freeform keys start with '----' # http://mutagen.readthedocs.io/en/latest/api/mp4.html TRACK_UUID_MP4_TAG = '----:symuid:uuid' PATH_DEFAULT_IGNORE_REGEX = r'\.(itdb|itc2|itl|jpg|midi?|plist|xml|zip)$' def generate_uuid(): return subprocess.check_output(['uuid', '-v', '4', '-F', 'BIN']).strip() def get_uuid_id3(id3_tags): assert isinstance(id3_tags, mutagen.id3.ID3), type(id3_tags) ufids = id3_tags.getall('UFID') for ufid in ufids: if ufid.owner == TRACK_UUID_ID3_OWNER_ID: return ufid.data return None def get_or_assign_uuid_id3(id3_tags): uuid = get_uuid_id3(id3_tags) if uuid is None: # mutagen.id3._specs.EncodedTextSpec.write encodes 'owner' id3_tags.add(mutagen.id3.UFID( owner=TRACK_UUID_ID3_OWNER_ID, data=generate_uuid(), )) id3_tags.save() id3_tags.load(filename=id3_tags.filename) uuid = get_uuid_id3(id3_tags) print("{!r}: assigned uuid {!r}".format(id3_tags.filename, uuid)) assert uuid is not None return uuid def get_or_assign_uuid_mp4(mp4_file): if not TRACK_UUID_MP4_TAG in mp4_file: mp4_file[TRACK_UUID_MP4_TAG] = mutagen.mp4.MP4FreeForm( data=generate_uuid(), # https://mutagen.readthedocs.io/en/latest/api/mp4.html#mutagen.mp4.AtomDataType.UUID dataformat=mutagen.mp4.AtomDataType.UUID, ) mp4_file.save() mp4_file.load(filename=mp4_file.filename) print("{!r}: assigned uuid {!r}".format( mp4_file.filename, mp4_file[TRACK_UUID_MP4_TAG][0], )) return mp4_file[TRACK_UUID_MP4_TAG][0] def log_path(track_path, msg, stream=sys.stdout): stream.write("{!r}: {}\n".format(track_path, msg)) def log_path_error(track_path, msg): log_path(track_path, msg, stream=sys.stderr) def symuid_sync(path, path_ignore_regex, show_ignored=False): for track in symuid.Track.walk( root_path=path, path_ignore_regex=path_ignore_regex, ignored_cb=lambda p: show_ignored and log_path(p, 'ignored'), unsupported_cb=lambda p, e: log_path_error(p, 'unsupported type, skipped'), ): if isinstance(track._iface, symuid.tag_interface.MP4): get_or_assign_uuid_mp4(track._iface._mutagen_file) elif isinstance(track._iface, symuid.tag_interface.ID3): get_or_assign_uuid_id3(track._iface._mutagen_file.tags) else: raise Exception(track) def _init_argparser(): import argparse argparser = argparse.ArgumentParser(description=None) argparser.add_argument('path') argparser.add_argument( '--path-ignore-regex', default=PATH_DEFAULT_IGNORE_REGEX, nargs=1, metavar='pattern', dest='path_ignore_regex', type=lambda pattern: re.compile(pattern), help='(default: %(default)s)', ) argparser.add_argument( '--show-ignored', action='store_true', ) return argparser def main(argv): argparser = _init_argparser() args = argparser.parse_args(argv[1:]) symuid_sync(**vars(args)) return 0 if __name__ == "__main__": sys.exit(main(sys.argv))