#!/usr/bin/env python3 # -*- coding: utf-8 -*- import mutagen import mutagen.id3 import mutagen.mp4 import os import re import subprocess import sys # 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 symuid_sync(path, path_ignore_regex, show_ignored=False): if path_ignore_regex.search(path): if show_ignored: print("{!r}: path matches ignore pattern".format(path)) elif os.path.isdir(path): for dirpath, dirnames, filenames in os.walk(path): for filename in filenames: symuid_sync( os.path.join(dirpath, filename), path_ignore_regex, show_ignored, ) else: try: f = mutagen.File(filename=path) except Exception: raise Exception(path) if not f: sys.stderr.write( "{!r}: unsupported filetype, skipped\n".format(path), ) elif isinstance(f, mutagen.mp4.MP4): get_or_assign_uuid_mp4(f) elif isinstance(f.tags, mutagen.id3.ID3): get_or_assign_uuid_id3(f.tags) else: raise Exception(f) 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))