tag_interface.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. # -*- coding: utf-8 -*-
  2. import mutagen.id3
  3. import mutagen.mp4
  4. import re
  5. class _mutagen:
  6. @property
  7. def track_path(self):
  8. return self._mutagen_file.filename
  9. def save(self):
  10. self._mutagen_file.save()
  11. class ID3(_mutagen):
  12. # http://id3.org/id3v2.4.0-frames#4.1.
  13. _UFID_OWNER_ID = 'symuid'
  14. def __init__(self, mutagen_file):
  15. assert mutagen_file.tags, mutagen_file
  16. assert isinstance(mutagen_file.tags, mutagen.id3.ID3), \
  17. mutagen_file.tags
  18. self._mutagen_file = mutagen_file
  19. def get_free_ints(self, tag_label_prefix):
  20. for t in self._mutagen_file.tags.getall('TXXX:' + tag_label_prefix):
  21. assert len(t.text) == 1, t
  22. yield (t.desc, int(t.text[0]))
  23. def get_free_int(self, tag_label):
  24. tags = [t for t in self.get_free_ints(tag_label)]
  25. if len(tags) == 0:
  26. raise KeyError(tag_label)
  27. else:
  28. assert len(tags) == 1, tags
  29. assert tags[0][0] == tag_label, tag
  30. return tags[0][1]
  31. def set_free_int(self, tag_label, data):
  32. # mutagen.id3._specs.EncodedTextSpec.write encodes 'desc' and 'text'
  33. tag = mutagen.id3.TXXX(
  34. encoding=mutagen.id3.Encoding.LATIN1,
  35. desc=tag_label,
  36. text=[str(data)],
  37. )
  38. # TODO overwrite instead of add() ?
  39. self._mutagen_file.tags.add(tag)
  40. return tag
  41. def get_track_uuid(self):
  42. for ufid in self._mutagen_file.tags.getall('UFID'):
  43. if ufid.owner == self._UFID_OWNER_ID:
  44. return ufid.data
  45. return None
  46. def set_track_uuid(self, uuid):
  47. # mutagen.id3._specs.EncodedTextSpec.write encodes 'owner'
  48. tag = mutagen.id3.UFID(owner=self._UFID_OWNER_ID, data=uuid)
  49. self._mutagen_file.tags.add(tag)
  50. return tag
  51. class MP4(_mutagen):
  52. _UUID_TAG_KEY = 'symuid:uuid'
  53. def __init__(self, mutagen_file):
  54. assert mutagen_file.tags, mutagen_file
  55. assert isinstance(mutagen_file.tags, mutagen.mp4.MP4Tags), \
  56. mutagen_file.tags
  57. self._mutagen_file = mutagen_file
  58. @staticmethod
  59. def _freeform_to_int(freeform):
  60. # "a signed big-endian integer with length one of { 1,2,3,4,8 } bytes"
  61. assert freeform.dataformat == mutagen.mp4.AtomDataType.INTEGER, freeform
  62. return int.from_bytes(freeform, byteorder='big', signed=True)
  63. def get_free_ints(self, tag_label_prefix):
  64. label_pattern = re.compile(r'^----:{}(:|$)'.format(
  65. re.escape(tag_label_prefix),
  66. ))
  67. for label, values in self._mutagen_file.tags.items():
  68. # TODO overwrite instead of add() ?
  69. if label_pattern.match(label):
  70. assert len(values) == 1, (label, values)
  71. value = MP4._freeform_to_int(values[0])
  72. yield (re.sub(r'^----:', '', label), value)
  73. def _get_free(self, tag_label):
  74. # freeform keys start with '----'
  75. # http://mutagen.readthedocs.io/en/latest/api/mp4.html
  76. tags = self._mutagen_file.tags['----:' + tag_label]
  77. assert len(tags) == 1, tags
  78. return tags[0]
  79. def get_free_int(self, tag_label):
  80. return MP4._freeform_to_int(self._get_free(tag_label))
  81. def _get_free_uuid(self, tag_label):
  82. tag = self._get_free(tag_label)
  83. assert tag.dataformat == mutagen.mp4.AtomDataType.UUID, tag.dataformat
  84. return tag
  85. def set_free_int(self, tag_label, data):
  86. assert isinstance(data, int)
  87. tag = mutagen.mp4.MP4FreeForm(
  88. # "a signed big-endian integer with length one of { 1,2,3,4,8 } bytes"
  89. dataformat=mutagen.mp4.AtomDataType.INTEGER,
  90. # TODO set byte length properly
  91. data=data.to_bytes(1, byteorder='big', signed=True),
  92. )
  93. self._mutagen_file.tags['----:' + tag_label] = tag
  94. return tag
  95. def _set_free_uuid(self, tag_label, data):
  96. assert isinstance(data, bytes)
  97. tag = mutagen.mp4.MP4FreeForm(
  98. data=data,
  99. # https://mutagen.readthedocs.io/en/latest/api/mp4.html#mutagen.mp4.AtomDataType.UUID
  100. dataformat=mutagen.mp4.AtomDataType.UUID,
  101. )
  102. self._mutagen_file.tags['----:' + tag_label] = [tag]
  103. return tag
  104. def get_track_uuid(self):
  105. try:
  106. return self._get_free_uuid(self._UUID_TAG_KEY)
  107. except KeyError:
  108. return None
  109. def set_track_uuid(self, uuid):
  110. return self._set_free_uuid(self._UUID_TAG_KEY, uuid)