PICARD-1098: Support custom tags for MP4

Custom tags are saved to "----:com.apple.iTunes:REPLAYGAIN_ALBUM_GAIN" + tag_name. tag_name is treated cases insensitive, but casing is preserved.
This commit is contained in:
Philipp Wolfer
2019-11-03 12:53:39 +01:00
parent 0854b91c90
commit 5318b4c3fe
2 changed files with 49 additions and 30 deletions

View File

@@ -36,6 +36,11 @@ from picard.metadata import Metadata
from picard.util import encode_filename
def _add_text_values_to_metadata(metadata, name, values):
for value in values:
metadata.add(name, value.decode("utf-8", "replace").strip("\x00"))
class MP4File(File):
EXTENSIONS = [".m4a", ".m4b", ".m4p", ".m4v", ".mp4"]
NAME = "MPEG-4 Audio"
@@ -125,7 +130,8 @@ class MP4File(File):
}
__r_freeform_tags = dict([(v, k) for k, v in __freeform_tags.items()])
# Tags to load case insensitive
# Tags to load case insensitive. Case is preserved, but the specified case
# is written if it is unset.
__r_freeform_tags_ci = {
"replaygain_album_gain": "----:com.apple.iTunes:REPLAYGAIN_ALBUM_GAIN",
"replaygain_album_peak": "----:com.apple.iTunes:REPLAYGAIN_ALBUM_PEAK",
@@ -161,15 +167,12 @@ class MP4File(File):
for value in values:
metadata.add(self.__int_tags[name], str(value))
elif name in self.__freeform_tags:
for value in values:
value = value.decode("utf-8", "replace").strip("\x00")
metadata.add(self.__freeform_tags[name], value)
tag_name = self.__freeform_tags[name]
_add_text_values_to_metadata(metadata, tag_name, values)
elif name_lower in self.__freeform_tags_ci:
for value in values:
value = value.decode("utf-8", "replace").strip("\x00")
tag_name = self.__freeform_tags_ci[name_lower]
metadata.add(tag_name, value)
self.__casemap[tag_name] = name
tag_name = self.__freeform_tags_ci[name_lower]
self.__casemap[tag_name] = name
_add_text_values_to_metadata(metadata, tag_name, values)
elif name == "----:com.apple.iTunes:fingerprint":
for value in values:
value = value.decode("utf-8", "replace").strip("\x00")
@@ -197,6 +200,17 @@ class MP4File(File):
(filename, e))
else:
metadata.images.append(coverartimage)
# Read other freeform tags always case insensitive
elif name.startswith('----:com.apple.iTunes:'):
tag_name = name_lower[22:]
self.__casemap[tag_name] = name[22:]
if (name not in self.__r_text_tags
and name not in self.__r_bool_tags
and name not in self.__r_int_tags
and name not in self.__r_freeform_tags
and name_lower not in self.__r_freeform_tags_ci
and name not in self.__other_supported_tags):
_add_text_values_to_metadata(metadata, tag_name, values)
self._info(metadata, file)
return metadata
@@ -236,6 +250,11 @@ class MP4File(File):
tags[name] = values
elif name == "musicip_fingerprint":
tags["----:com.apple.iTunes:fingerprint"] = [b"MusicMagic Fingerprint%s" % v.encode('ascii') for v in values]
elif self.supports_tag(name) and name not in ('tracknumber',
'totaltracks', 'discnumber', 'totaldiscs'):
values = [v.encode("utf-8") for v in values]
name = self.__casemap.get(name, name)
tags['----:com.apple.iTunes:' + name] = values
if "tracknumber" in metadata:
if "totaltracks" in metadata:
@@ -274,14 +293,13 @@ class MP4File(File):
@classmethod
def supports_tag(cls, name):
return (name in cls.__r_text_tags
or name in cls.__r_bool_tags
or name in cls.__r_freeform_tags
or name in cls.__r_freeform_tags_ci
or name in cls.__r_int_tags
or name in cls.__other_supported_tags
or name.startswith('lyrics:')
or name in ('~length', 'musicip_fingerprint'))
unsupported_tags = ['r128_album_gain', 'r128_track_gain']
return ((name
and not name.startswith("~")
and name not in unsupported_tags
and not (name.startswith('comment:') and len(name) > 9)
and not name.startswith('performer:'))
or name in ('~length'))
def _get_tag_name(self, name):
if name.startswith('lyrics:'):

View File

@@ -52,19 +52,20 @@ class CommonMP4Tests:
def test_ci_tags_preserve_case(self):
# Ensure values are not duplicated on repeated save and are saved
# case preserving.
tags = mutagen.mp4.MP4Tags()
tags['----:com.apple.iTunes:Replaygain_Album_Peak'] = [b'-6.48 dB']
save_raw(self.filename, tags)
loaded_metadata = load_metadata(self.filename)
loaded_metadata['replaygain_album_peak'] = '1.0'
save_metadata(self.filename, loaded_metadata)
raw_metadata = load_raw(self.filename)
self.assertIn('----:com.apple.iTunes:Replaygain_Album_Peak', raw_metadata)
self.assertEqual(
raw_metadata['----:com.apple.iTunes:Replaygain_Album_Peak'][0].decode('utf-8'),
loaded_metadata['replaygain_album_peak'])
self.assertEqual(1, len(raw_metadata['----:com.apple.iTunes:Replaygain_Album_Peak']))
self.assertNotIn('----:com.apple.iTunes:REPLAYGAIN_ALBUM_PEAK', raw_metadata)
for name in ('Replaygain_Album_Peak', 'Custom'):
tags = mutagen.mp4.MP4Tags()
tags['----:com.apple.iTunes:' + name] = [b'foo']
save_raw(self.filename, tags)
loaded_metadata = load_metadata(self.filename)
loaded_metadata[name.lower()] = 'bar'
save_metadata(self.filename, loaded_metadata)
raw_metadata = load_raw(self.filename)
self.assertIn('----:com.apple.iTunes:' + name, raw_metadata)
self.assertEqual(
raw_metadata['----:com.apple.iTunes:' + name][0].decode('utf-8'),
loaded_metadata[name.lower()])
self.assertEqual(1, len(raw_metadata['----:com.apple.iTunes:' + name]))
self.assertNotIn('----:com.apple.iTunes:' + name.upper(), raw_metadata)
class M4ATest(CommonMP4Tests.MP4TestCase):