diff --git a/picard/formats/apev2.py b/picard/formats/apev2.py index 5102227ff..2d1f13fdf 100644 --- a/picard/formats/apev2.py +++ b/picard/formats/apev2.py @@ -191,12 +191,15 @@ class APEv2File(File): tags = mutagen.apev2.APEv2() images_to_save = list(metadata.images.to_be_saved_to_tags()) if config.setting["clear_existing_tags"]: + preserved = [] + if config.setting['preserve_images']: + preserved = list(self._iter_cover_art_tags(tags)) tags.clear() + for name, value in preserved: + tags[name] = value elif images_to_save: - for name, value in tags.items(): - if (value.kind == mutagen.apev2.BINARY - and name.lower().startswith('cover art')): - del tags[name] + for name, value in self._iter_cover_art_tags(tags): + del tags[name] temp = {} for name, value in metadata.items(): if name.startswith("~") or not self.supports_tag(name): @@ -273,6 +276,12 @@ class APEv2File(File): else: return name.title() + @staticmethod + def _iter_cover_art_tags(tags): + for name, value in tags.items(): + if value.kind == mutagen.apev2.BINARY and name.lower().startswith('cover art'): + yield (name, value) + @classmethod def supports_tag(cls, name): return (bool(name) and name not in UNSUPPORTED_TAGS diff --git a/picard/formats/asf.py b/picard/formats/asf.py index 8e551a7a4..6371aaa06 100644 --- a/picard/formats/asf.py +++ b/picard/formats/asf.py @@ -267,7 +267,10 @@ class ASFFile(File): tags = file.tags if config.setting['clear_existing_tags']: + cover = tags['WM/Picture'] if config.setting['preserve_images'] else None tags.clear() + if cover: + tags['WM/Picture'] = cover cover = [] for image in metadata.images.to_be_saved_to_tags(): tag_data = pack_image(image.mimetype, image.data, diff --git a/picard/formats/id3.py b/picard/formats/id3.py index e84bcf425..d8f22d6e7 100644 --- a/picard/formats/id3.py +++ b/picard/formats/id3.py @@ -374,7 +374,10 @@ class ID3File(File): tags = self._get_tags(filename) config = get_config() if config.setting['clear_existing_tags']: + cover = tags.getall('APIC') if config.setting["preserve_images"] else None tags.clear() + if cover: + tags.setall('APIC', cover) images_to_save = list(metadata.images.to_be_saved_to_tags()) if images_to_save: tags.delall('APIC') diff --git a/picard/formats/mp4.py b/picard/formats/mp4.py index 939e6d1aa..382f1dcfe 100644 --- a/picard/formats/mp4.py +++ b/picard/formats/mp4.py @@ -253,7 +253,10 @@ class MP4File(File): tags = file.tags if config.setting["clear_existing_tags"]: + cover = tags['covr'] if config.setting['preserve_images'] else None tags.clear() + if cover: + tags['covr'] = cover for name, values in metadata.rawitems(): if name.startswith('lyrics:'): diff --git a/picard/formats/vorbis.py b/picard/formats/vorbis.py index c6f7d9f21..f6495ff24 100644 --- a/picard/formats/vorbis.py +++ b/picard/formats/vorbis.py @@ -4,7 +4,7 @@ # # Copyright (C) 2006-2008, 2012 Lukáš Lalinský # Copyright (C) 2008 Hendrik van Antwerpen -# Copyright (C) 2008-2010, 2014-2015, 2018-2020 Philipp Wolfer +# Copyright (C) 2008-2010, 2014-2015, 2018-2021 Philipp Wolfer # Copyright (C) 2012-2013 Michael Wiencek # Copyright (C) 2012-2014 Wieland Hoffmann # Copyright (C) 2013 Calvin Walton @@ -231,12 +231,22 @@ class VCommentFile(File): if file.tags is None: file.add_tags() if config.setting["clear_existing_tags"]: - channel_mask = file.tags.get('waveformatextensible_channel_mask', None) + preserve_tags = ['waveformatextensible_channel_mask'] + if not is_flac and config.setting["preserve_images"]: + preserve_tags.append('METADATA_BLOCK_PICTURE') + preserve_tags.append('COVERART') + preserved_values = {} + for name in preserve_tags: + if name in file.tags: + preserved_values[name] = file.tags[name] file.tags.clear() - if channel_mask: - file.tags['waveformatextensible_channel_mask'] = channel_mask + for name, value in preserved_values.items(): + if value: + file.tags[name] = value images_to_save = list(metadata.images.to_be_saved_to_tags()) - if is_flac and (config.setting["clear_existing_tags"] or images_to_save): + if is_flac and ( + (config.setting["clear_existing_tags"] and not config.setting["preserve_images"]) + or images_to_save): file.clear_pictures() tags = {} for name, value in metadata.items(): diff --git a/picard/ui/options/tags.py b/picard/ui/options/tags.py index e37cde065..a48b44f6b 100644 --- a/picard/ui/options/tags.py +++ b/picard/ui/options/tags.py @@ -53,6 +53,7 @@ class TagsOptionsPage(OptionsPage): BoolOption("setting", "dont_write_tags", False), BoolOption("setting", "preserve_timestamps", False), BoolOption("setting", "clear_existing_tags", False), + BoolOption("setting", "preserve_images", False), BoolOption("setting", "remove_id3_from_flac", False), BoolOption("setting", "remove_ape_from_mp3", False), ListOption("setting", "preserved_tags", []), @@ -68,6 +69,7 @@ class TagsOptionsPage(OptionsPage): self.ui.write_tags.setChecked(not config.setting["dont_write_tags"]) self.ui.preserve_timestamps.setChecked(config.setting["preserve_timestamps"]) self.ui.clear_existing_tags.setChecked(config.setting["clear_existing_tags"]) + self.ui.preserve_images.setChecked(config.setting["preserve_images"]) self.ui.remove_ape_from_mp3.setChecked(config.setting["remove_ape_from_mp3"]) self.ui.remove_id3_from_flac.setChecked(config.setting["remove_id3_from_flac"]) self.ui.preserved_tags.update(config.setting["preserved_tags"]) @@ -81,6 +83,7 @@ class TagsOptionsPage(OptionsPage): if clear_existing_tags != config.setting["clear_existing_tags"]: config.setting["clear_existing_tags"] = clear_existing_tags self.tagger.window.metadata_box.update() + config.setting["preserve_images"] = self.ui.preserve_images.isChecked() config.setting["remove_ape_from_mp3"] = self.ui.remove_ape_from_mp3.isChecked() config.setting["remove_id3_from_flac"] = self.ui.remove_id3_from_flac.isChecked() config.setting["preserved_tags"] = list(self.ui.preserved_tags.tags) diff --git a/picard/ui/ui_options_tags.py b/picard/ui/ui_options_tags.py index f11d227c1..cf4ae2299 100644 --- a/picard/ui/ui_options_tags.py +++ b/picard/ui/ui_options_tags.py @@ -10,7 +10,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_TagsOptionsPage(object): def setupUi(self, TagsOptionsPage): TagsOptionsPage.setObjectName("TagsOptionsPage") - TagsOptionsPage.resize(539, 525) + TagsOptionsPage.resize(567, 525) self.vboxlayout = QtWidgets.QVBoxLayout(TagsOptionsPage) self.vboxlayout.setObjectName("vboxlayout") self.write_tags = QtWidgets.QCheckBox(TagsOptionsPage) @@ -28,6 +28,10 @@ class Ui_TagsOptionsPage(object): self.clear_existing_tags = QtWidgets.QCheckBox(self.before_tagging) self.clear_existing_tags.setObjectName("clear_existing_tags") self.vboxlayout1.addWidget(self.clear_existing_tags) + self.preserve_images = QtWidgets.QCheckBox(self.before_tagging) + self.preserve_images.setEnabled(False) + self.preserve_images.setObjectName("preserve_images") + self.vboxlayout1.addWidget(self.preserve_images) self.remove_id3_from_flac = QtWidgets.QCheckBox(self.before_tagging) self.remove_id3_from_flac.setObjectName("remove_id3_from_flac") self.vboxlayout1.addWidget(self.remove_id3_from_flac) @@ -50,12 +54,13 @@ class Ui_TagsOptionsPage(object): self.vboxlayout.addWidget(self.before_tagging) self.retranslateUi(TagsOptionsPage) + self.clear_existing_tags.toggled['bool'].connect(self.preserve_images.setEnabled) QtCore.QMetaObject.connectSlotsByName(TagsOptionsPage) TagsOptionsPage.setTabOrder(self.write_tags, self.preserve_timestamps) TagsOptionsPage.setTabOrder(self.preserve_timestamps, self.clear_existing_tags) - TagsOptionsPage.setTabOrder(self.clear_existing_tags, self.remove_id3_from_flac) + TagsOptionsPage.setTabOrder(self.clear_existing_tags, self.preserve_images) + TagsOptionsPage.setTabOrder(self.preserve_images, self.remove_id3_from_flac) TagsOptionsPage.setTabOrder(self.remove_id3_from_flac, self.remove_ape_from_mp3) - TagsOptionsPage.setTabOrder(self.remove_ape_from_mp3, self.preserved_tags) def retranslateUi(self, TagsOptionsPage): _translate = QtCore.QCoreApplication.translate @@ -63,6 +68,7 @@ class Ui_TagsOptionsPage(object): self.preserve_timestamps.setText(_("Preserve timestamps of tagged files")) self.before_tagging.setTitle(_("Before Tagging")) self.clear_existing_tags.setText(_("Clear existing tags")) + self.preserve_images.setText(_("Keep embedded images when clearing tags")) self.remove_id3_from_flac.setText(_("Remove ID3 tags from FLAC files")) self.remove_ape_from_mp3.setText(_("Remove APEv2 tags from MP3 files")) self.preserved_tags_label.setText(_("Preserve these tags from being cleared or overwritten with MusicBrainz data:")) diff --git a/test/formats/common.py b/test/formats/common.py index da6f5835b..9f5830e91 100644 --- a/test/formats/common.py +++ b/test/formats/common.py @@ -40,6 +40,7 @@ from picard.metadata import Metadata settings = { 'clear_existing_tags': False, + 'preserve_images': False, 'embed_only_one_front_image': False, 'enabled_plugins': '', 'id3v23_join_with': '/', diff --git a/test/formats/coverart.py b/test/formats/coverart.py index a3376f989..54e7b4903 100644 --- a/test/formats/coverart.py +++ b/test/formats/coverart.py @@ -2,7 +2,7 @@ # # Picard, the next-generation MusicBrainz tagger # -# Copyright (C) 2019-2020 Philipp Wolfer +# Copyright (C) 2019-2021 Philipp Wolfer # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -75,6 +75,10 @@ class CommonCoverArtTests: def setUp(self): super().setUp() + self.set_config_values({ + 'clear_existing_tags': False, + 'preserve_images': False, + }) self.jpegdata = load_coverart_file('mb.jpg') self.pngdata = load_coverart_file('mb.png') @@ -118,6 +122,20 @@ class CommonCoverArtTests: loaded_metadata = save_and_load_metadata(self.filename, metadata) self.assertEqual(0, len(loaded_metadata.images)) + @skipUnlessTestfile + def test_cover_art_clear_tags(self): + image = CoverArtImage(data=self.pngdata, types=['front']) + file_save_image(self.filename, image) + metadata = load_metadata(self.filename) + self.assertEqual(image, metadata.images[0]) + config.setting['clear_existing_tags'] = True + config.setting['preserve_images'] = True + metadata = save_and_load_metadata(self.filename, Metadata()) + self.assertEqual(image, metadata.images[0]) + config.setting['preserve_images'] = False + metadata = save_and_load_metadata(self.filename, Metadata()) + self.assertEqual(0, len(metadata.images)) + def _cover_metadata(self): imgdata = self.jpegdata metadata = Metadata() diff --git a/ui/options_tags.ui b/ui/options_tags.ui index 8136d1670..5fd6079c6 100644 --- a/ui/options_tags.ui +++ b/ui/options_tags.ui @@ -6,7 +6,7 @@ 0 0 - 539 + 567 525 @@ -47,6 +47,16 @@ + + + + false + + + Keep embedded images when clearing tags + + + @@ -111,10 +121,27 @@ write_tags preserve_timestamps clear_existing_tags + preserve_images remove_id3_from_flac remove_ape_from_mp3 - preserved_tags - + + + clear_existing_tags + toggled(bool) + preserve_images + setEnabled(bool) + + + 283 + 107 + + + 283 + 132 + + + +