tag_interface.py 5.0 KB

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