mirror of
https://github.com/fergalmoran/picard.git
synced 2026-03-21 20:55:08 +00:00
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:
@@ -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:'):
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user