symuid-sync 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. import mutagen
  4. import mutagen.id3
  5. import mutagen.mp4
  6. import os
  7. import re
  8. import subprocess
  9. import sys
  10. import symuid
  11. # http://id3.org/id3v2.4.0-frames#4.1.
  12. TRACK_UUID_ID3_OWNER_ID = 'symuid'
  13. # freeform keys start with '----'
  14. # http://mutagen.readthedocs.io/en/latest/api/mp4.html
  15. TRACK_UUID_MP4_TAG = '----:symuid:uuid'
  16. PATH_DEFAULT_IGNORE_REGEX = r'\.(itdb|itc2|itl|jpg|midi?|plist|xml|zip)$'
  17. def generate_uuid():
  18. return subprocess.check_output(['uuid', '-v', '4', '-F', 'BIN']).strip()
  19. def get_uuid_id3(id3_tags):
  20. assert isinstance(id3_tags, mutagen.id3.ID3), type(id3_tags)
  21. ufids = id3_tags.getall('UFID')
  22. for ufid in ufids:
  23. if ufid.owner == TRACK_UUID_ID3_OWNER_ID:
  24. return ufid.data
  25. return None
  26. def get_or_assign_uuid_id3(id3_tags):
  27. uuid = get_uuid_id3(id3_tags)
  28. if uuid is None:
  29. # mutagen.id3._specs.EncodedTextSpec.write encodes 'owner'
  30. id3_tags.add(mutagen.id3.UFID(
  31. owner=TRACK_UUID_ID3_OWNER_ID,
  32. data=generate_uuid(),
  33. ))
  34. id3_tags.save()
  35. id3_tags.load(filename=id3_tags.filename)
  36. uuid = get_uuid_id3(id3_tags)
  37. print("{!r}: assigned uuid {!r}".format(id3_tags.filename, uuid))
  38. assert uuid is not None
  39. return uuid
  40. def get_or_assign_uuid_mp4(mp4_file):
  41. if not TRACK_UUID_MP4_TAG in mp4_file:
  42. mp4_file[TRACK_UUID_MP4_TAG] = mutagen.mp4.MP4FreeForm(
  43. data=generate_uuid(),
  44. # https://mutagen.readthedocs.io/en/latest/api/mp4.html#mutagen.mp4.AtomDataType.UUID
  45. dataformat=mutagen.mp4.AtomDataType.UUID,
  46. )
  47. mp4_file.save()
  48. mp4_file.load(filename=mp4_file.filename)
  49. print("{!r}: assigned uuid {!r}".format(
  50. mp4_file.filename, mp4_file[TRACK_UUID_MP4_TAG][0],
  51. ))
  52. return mp4_file[TRACK_UUID_MP4_TAG][0]
  53. def log_path(track_path, msg, stream=sys.stdout):
  54. stream.write("{!r}: {}\n".format(track_path, msg))
  55. def log_path_error(track_path, msg):
  56. log_path(track_path, msg, stream=sys.stderr)
  57. def symuid_sync(path, path_ignore_regex, show_ignored=False):
  58. for track in symuid.Track.walk(
  59. root_path=path,
  60. path_ignore_regex=path_ignore_regex,
  61. ignored_cb=lambda p: show_ignored and log_path(p, 'ignored'),
  62. unsupported_cb=lambda p, e: log_path_error(p, 'unsupported type, skipped'),
  63. ):
  64. if isinstance(track._iface, symuid.tag_interface.MP4):
  65. get_or_assign_uuid_mp4(track._iface._mutagen_file)
  66. elif isinstance(track._iface, symuid.tag_interface.ID3):
  67. get_or_assign_uuid_id3(track._iface._mutagen_file.tags)
  68. else:
  69. raise Exception(track)
  70. def _init_argparser():
  71. import argparse
  72. argparser = argparse.ArgumentParser(description=None)
  73. argparser.add_argument('path')
  74. argparser.add_argument(
  75. '--path-ignore-regex',
  76. default=PATH_DEFAULT_IGNORE_REGEX,
  77. nargs=1,
  78. metavar='pattern',
  79. dest='path_ignore_regex',
  80. type=lambda pattern: re.compile(pattern),
  81. help='(default: %(default)s)',
  82. )
  83. argparser.add_argument(
  84. '--show-ignored',
  85. action='store_true',
  86. )
  87. return argparser
  88. def main(argv):
  89. argparser = _init_argparser()
  90. args = argparser.parse_args(argv[1:])
  91. symuid_sync(**vars(args))
  92. return 0
  93. if __name__ == "__main__":
  94. sys.exit(main(sys.argv))