#!/usr/bin/env python3 # -*- coding: utf-8 -*- import mutagen import mutagen.id3 import mutagen.mp4 import os import subprocess # 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' 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): if os.path.isdir(path): for dirpath, dirnames, filenames in os.walk(path): for filename in filenames: symuid_sync(os.path.join(dirpath, filename)) else: try: f = mutagen.File(filename=path) except Exception: raise Exception(path) if not f: print("{!r}: unsupported filetype, ignored".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') return argparser def main(argv): argparser = _init_argparser() args = argparser.parse_args(argv[1:]) symuid_sync(**vars(args)) return 0 if __name__ == "__main__": import sys sys.exit(main(sys.argv))