diff --git a/picard/formats/mp4.py b/picard/formats/mp4.py index c0d1d8de5..5c9eaf0dc 100644 --- a/picard/formats/mp4.py +++ b/picard/formats/mp4.py @@ -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:'): diff --git a/test/formats/test_mp4.py b/test/formats/test_mp4.py index 885baea3b..11a41eb27 100644 --- a/test/formats/test_mp4.py +++ b/test/formats/test_mp4.py @@ -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):