#!/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 # 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_or_assign_uuid_id3(track): uuid = track._iface.get_uuid() if uuid is None: id3_tags = track._iface._mutagen_file.tags # mutagen.id3._specs.EncodedTextSpec.write encodes 'owner' id3_tags.add(mutagen.id3.UFID( owner=track._iface._UFID_OWNER_ID, data=generate_uuid(), )) id3_tags.save() id3_tags.load(filename=id3_tags.filename) uuid = track._iface.get_uuid() 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) 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))