symuid-sync 3.5 KB

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