symuid-sync 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  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 subprocess
  8. # http://id3.org/id3v2.4.0-frames#4.1.
  9. TRACK_UUID_ID3_OWNER_ID = 'symuid'
  10. # freeform keys start with '----'
  11. # http://mutagen.readthedocs.io/en/latest/api/mp4.html
  12. TRACK_UUID_MP4_TAG = '----:symuid:uuid'
  13. def generate_uuid():
  14. return subprocess.check_output(['uuid', '-v', '4', '-F', 'BIN']).strip()
  15. def get_uuid_id3(id3_tags):
  16. assert isinstance(id3_tags, mutagen.id3.ID3), type(id3_tags)
  17. ufids = id3_tags.getall('UFID')
  18. for ufid in ufids:
  19. if ufid.owner == TRACK_UUID_ID3_OWNER_ID:
  20. return ufid.data
  21. return None
  22. def get_or_assign_uuid_id3(id3_tags):
  23. uuid = get_uuid_id3(id3_tags)
  24. if uuid is None:
  25. # mutagen.id3._specs.EncodedTextSpec.write encodes 'owner'
  26. id3_tags.add(mutagen.id3.UFID(
  27. owner=TRACK_UUID_ID3_OWNER_ID,
  28. data=generate_uuid(),
  29. ))
  30. id3_tags.save()
  31. id3_tags.load(filename=id3_tags.filename)
  32. uuid = get_uuid_id3(id3_tags)
  33. print("{!r}: assigned uuid {!r}".format(id3_tags.filename, uuid))
  34. assert uuid is not None
  35. return uuid
  36. def get_or_assign_uuid_mp4(mp4_file):
  37. if not TRACK_UUID_MP4_TAG in mp4_file:
  38. mp4_file[TRACK_UUID_MP4_TAG] = mutagen.mp4.MP4FreeForm(
  39. data=generate_uuid(),
  40. # https://mutagen.readthedocs.io/en/latest/api/mp4.html#mutagen.mp4.AtomDataType.UUID
  41. dataformat=mutagen.mp4.AtomDataType.UUID,
  42. )
  43. mp4_file.save()
  44. mp4_file.load(filename=mp4_file.filename)
  45. print("{!r}: assigned uuid {!r}".format(
  46. mp4_file.filename, mp4_file[TRACK_UUID_MP4_TAG][0],
  47. ))
  48. return mp4_file[TRACK_UUID_MP4_TAG][0]
  49. def symuid_sync(path):
  50. if os.path.isdir(path):
  51. for dirpath, dirnames, filenames in os.walk(path):
  52. for filename in filenames:
  53. symuid_sync(os.path.join(dirpath, filename))
  54. else:
  55. try:
  56. f = mutagen.File(filename=path)
  57. except Exception:
  58. raise Exception(path)
  59. if not f:
  60. print("{!r}: unsupported filetype, ignored".format(path))
  61. elif isinstance(f, mutagen.mp4.MP4):
  62. get_or_assign_uuid_mp4(f)
  63. elif isinstance(f.tags, mutagen.id3.ID3):
  64. get_or_assign_uuid_id3(f.tags)
  65. else:
  66. raise Exception(f)
  67. def _init_argparser():
  68. import argparse
  69. argparser = argparse.ArgumentParser(description=None)
  70. argparser.add_argument('path')
  71. return argparser
  72. def main(argv):
  73. argparser = _init_argparser()
  74. args = argparser.parse_args(argv[1:])
  75. symuid_sync(**vars(args))
  76. return 0
  77. if __name__ == "__main__":
  78. import sys
  79. sys.exit(main(sys.argv))