From 17f5c61ae1502a42b0d607b8933ce91655114861 Mon Sep 17 00:00:00 2001 From: twodoorcoupe Date: Mon, 8 Jul 2024 12:18:42 +0200 Subject: [PATCH] Add option to filter images by type --- picard/const/defaults.py | 2 ++ picard/coverart/processing/filters.py | 14 +++++++++++++ picard/options.py | 7 ++++++- picard/ui/forms/ui_options_cover.py | 16 +++++++++++++++ picard/ui/options/cover.py | 29 +++++++++++++++++++++++++++ test/test_coverart_processing.py | 24 +++++++++++++++++++++- ui/options_cover.ui | 24 ++++++++++++++++++++++ 7 files changed, 114 insertions(+), 2 deletions(-) diff --git a/picard/const/defaults.py b/picard/const/defaults.py index e42f8d6cb..7ee807307 100644 --- a/picard/const/defaults.py +++ b/picard/const/defaults.py @@ -85,6 +85,8 @@ DEFAULT_QUERY_LIMIT = 50 DEFAULT_DRIVES = get_default_cdrom_drives() +DEFAULT_CA_NEVER_REPLACE_TYPE_INCLUDE = ['front'] +DEFAULT_CA_NEVER_REPLACE_TYPE_EXCLUDE = ['matrix/runout', 'raw/unedited', 'watermark'] DEFAULT_CA_PROVIDERS = [ ('Cover Art Archive', True), ('UrlRelationships', True), diff --git a/picard/coverart/processing/filters.py b/picard/coverart/processing/filters.py index c1a8f238d..ebba55a5a 100644 --- a/picard/coverart/processing/filters.py +++ b/picard/coverart/processing/filters.py @@ -69,6 +69,20 @@ def bigger_previous_image_filter(data, info, album, coverartimage): def image_types_filter(data, info, album, coverartimage): + config = get_config() + if config.setting['dont_replace_cover_of_types'] and config.setting['save_images_to_tags']: + downloaded_types = set(coverartimage.normalized_types()) + never_replace_types = config.setting['dont_replace_included_types'] + always_replace_types = config.setting['dont_replace_excluded_types'] + previous_image_types = album.orig_metadata.images.get_types_dict() + if downloaded_types.intersection(always_replace_types): + return True + for previous_image_type in previous_image_types: + type_already_embedded = downloaded_types.intersection(previous_image_type) + should_not_replace = downloaded_types.intersection(never_replace_types) + if type_already_embedded and should_not_replace: + log.debug("Discarding cover art. An image with the same type is already embedded.") + return False return True diff --git a/picard/options.py b/picard/options.py index b10dcd438..710e95ca0 100644 --- a/picard/options.py +++ b/picard/options.py @@ -34,6 +34,8 @@ from picard.config import ( from picard.const import MUSICBRAINZ_SERVERS from picard.const.defaults import ( DEFAULT_AUTOBACKUP_DIRECTORY, + DEFAULT_CA_NEVER_REPLACE_TYPE_EXCLUDE, + DEFAULT_CA_NEVER_REPLACE_TYPE_INCLUDE, DEFAULT_CA_PROVIDERS, DEFAULT_CAA_IMAGE_SIZE, DEFAULT_CAA_IMAGE_TYPE_EXCLUDE, @@ -166,7 +168,10 @@ TextOption('setting', 'cd_lookup_device', ','.join(DEFAULT_DRIVES)) ListOption('setting', 'ca_providers', DEFAULT_CA_PROVIDERS, title=N_("Cover art providers")) TextOption('setting', 'cover_image_filename', DEFAULT_COVER_IMAGE_FILENAME, title=N_("File name for images")) BoolOption('setting', 'embed_only_one_front_image', True, title=N_("Embed only a single front image")) -BoolOption('setting', 'dont_replace_with_smaller_cover', False, title=N_("Never replace front images with smaller ones")) +BoolOption('setting', 'dont_replace_with_smaller_cover', False, title=N_("Never replace cover images with smaller ones")) +BoolOption('setting', 'dont_replace_cover_of_types', False, title=N_("Never replace cover images of the given types")) +ListOption('setting', 'dont_replace_included_types', DEFAULT_CA_NEVER_REPLACE_TYPE_INCLUDE, title=N_("Never replace cover images of these types")) +ListOption('setting', 'dont_replace_excluded_types', DEFAULT_CA_NEVER_REPLACE_TYPE_EXCLUDE, title=N_("Always replace cover images of these types")) BoolOption('setting', 'image_type_as_filename', False, title=N_("Always use the primary image type as the file name for non-front images")) BoolOption('setting', 'save_images_overwrite', False, title=N_("Overwrite existing image files")) BoolOption('setting', 'save_images_to_files', False, title=N_("Save cover images as separate files")) diff --git a/picard/ui/forms/ui_options_cover.py b/picard/ui/forms/ui_options_cover.py index 9eccc49a7..ce191a53d 100644 --- a/picard/ui/forms/ui_options_cover.py +++ b/picard/ui/forms/ui_options_cover.py @@ -34,6 +34,20 @@ class Ui_CoverOptionsPage(object): self.cb_dont_replace_with_smaller = QtWidgets.QCheckBox(parent=self.save_images_to_tags) self.cb_dont_replace_with_smaller.setObjectName("cb_dont_replace_with_smaller") self.vboxlayout.addWidget(self.cb_dont_replace_with_smaller) + self.never_replace_types_layout = QtWidgets.QHBoxLayout() + self.never_replace_types_layout.setObjectName("never_replace_types_layout") + self.cb_never_replace_types = QtWidgets.QCheckBox(parent=self.save_images_to_tags) + self.cb_never_replace_types.setObjectName("cb_never_replace_types") + self.never_replace_types_layout.addWidget(self.cb_never_replace_types) + self.select_types_button = QtWidgets.QPushButton(parent=self.save_images_to_tags) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.select_types_button.sizePolicy().hasHeightForWidth()) + self.select_types_button.setSizePolicy(sizePolicy) + self.select_types_button.setObjectName("select_types_button") + self.never_replace_types_layout.addWidget(self.select_types_button) + self.vboxlayout.addLayout(self.never_replace_types_layout) self.verticalLayout.addWidget(self.save_images_to_tags) self.save_images_to_files = QtWidgets.QGroupBox(parent=CoverOptionsPage) self.save_images_to_files.setCheckable(True) @@ -104,6 +118,8 @@ class Ui_CoverOptionsPage(object): self.save_images_to_tags.setTitle(_("Embed cover images into tags")) self.cb_embed_front_only.setText(_("Embed only a single front image")) self.cb_dont_replace_with_smaller.setText(_("Never replace cover images with smaller ones")) + self.cb_never_replace_types.setText(_("Never replace cover images matching selected types")) + self.select_types_button.setText(_("Select Types...")) self.save_images_to_files.setTitle(_("Save cover images as separate files")) self.label_use_filename.setText(_("Use the following file name for images:")) self.save_images_overwrite.setText(_("Overwrite the file if it already exists")) diff --git a/picard/ui/options/cover.py b/picard/ui/options/cover.py index 374368bf5..2e630283c 100644 --- a/picard/ui/options/cover.py +++ b/picard/ui/options/cover.py @@ -31,6 +31,10 @@ from picard.config import ( Option, get_config, ) +from picard.const.defaults import ( + DEFAULT_CA_NEVER_REPLACE_TYPE_EXCLUDE, + DEFAULT_CA_NEVER_REPLACE_TYPE_INCLUDE, +) from picard.coverart.providers import cover_art_providers from picard.extension_points.options_pages import register_options_page from picard.i18n import ( @@ -38,6 +42,7 @@ from picard.i18n import ( gettext as _, ) +from picard.ui.caa_types_selector import CAATypesSelectorDialog from picard.ui.forms.ui_options_cover import Ui_CoverOptionsPage from picard.ui.moveable_list_view import MoveableListView from picard.ui.options import OptionsPage @@ -62,12 +67,17 @@ class CoverOptionsPage(OptionsPage): self.ui.save_images_to_files.clicked.connect(self.update_ca_providers_groupbox_state) self.ui.save_images_to_tags.clicked.connect(self.update_ca_providers_groupbox_state) self.ui.save_only_one_front_image.toggled.connect(self.ui.image_type_as_filename.setDisabled) + self.ui.cb_never_replace_types.toggled.connect(self.ui.select_types_button.setEnabled) + self.ui.select_types_button.clicked.connect(self.select_never_replace_image_types) self.move_view = MoveableListView(self.ui.ca_providers_list, self.ui.up_button, self.ui.down_button) self.register_setting('save_images_to_tags', ['save_images_to_tags']) self.register_setting('embed_only_one_front_image', ['cb_embed_front_only']) self.register_setting('dont_replace_with_smaller_cover', ['dont_replace_with_smaller_cover']) + self.register_setting('dont_replace_cover_of_types', ['dont_replace_cover_of_types']) + self.register_setting('dont_replace_included_types', ['dont_replace_included_types']) + self.register_setting('dont_replace_excluded_types', ['dont_replace_excluded_types']) self.register_setting('save_images_to_files', ['save_images_to_files']) self.register_setting('cover_image_filename', ['cover_image_filename']) self.register_setting('save_images_overwrite', ['save_images_overwrite']) @@ -78,6 +88,8 @@ class CoverOptionsPage(OptionsPage): def restore_defaults(self): # Remove previous entries self.ui.ca_providers_list.clear() + self.dont_replace_included_types = DEFAULT_CA_NEVER_REPLACE_TYPE_INCLUDE + self.dont_replace_excluded_types = DEFAULT_CA_NEVER_REPLACE_TYPE_EXCLUDE super().restore_defaults() def _load_cover_art_providers(self): @@ -94,6 +106,10 @@ class CoverOptionsPage(OptionsPage): self.ui.save_images_to_tags.setChecked(config.setting['save_images_to_tags']) self.ui.cb_embed_front_only.setChecked(config.setting['embed_only_one_front_image']) self.ui.cb_dont_replace_with_smaller.setChecked(config.setting['dont_replace_with_smaller_cover']) + self.ui.cb_never_replace_types.setChecked(config.setting['dont_replace_cover_of_types']) + self.ui.select_types_button.setEnabled(config.setting['dont_replace_cover_of_types']) + self.dont_replace_included_types = config.setting['dont_replace_included_types'] + self.dont_replace_excluded_types = config.setting['dont_replace_excluded_types'] self.ui.save_images_to_files.setChecked(config.setting['save_images_to_files']) self.ui.cover_image_filename.setText(config.setting['cover_image_filename']) self.ui.save_images_overwrite.setChecked(config.setting['save_images_overwrite']) @@ -112,6 +128,9 @@ class CoverOptionsPage(OptionsPage): config.setting['save_images_to_tags'] = self.ui.save_images_to_tags.isChecked() config.setting['embed_only_one_front_image'] = self.ui.cb_embed_front_only.isChecked() config.setting['dont_replace_with_smaller_cover'] = self.ui.cb_dont_replace_with_smaller.isChecked() + config.setting['dont_replace_cover_of_types'] = self.ui.cb_never_replace_types.isChecked() + config.setting['dont_replace_included_types'] = self.dont_replace_included_types + config.setting['dont_replace_excluded_types'] = self.dont_replace_excluded_types config.setting['save_images_to_files'] = self.ui.save_images_to_files.isChecked() config.setting['cover_image_filename'] = self.ui.cover_image_filename.text() config.setting['save_images_overwrite'] = self.ui.save_images_overwrite.isChecked() @@ -124,5 +143,15 @@ class CoverOptionsPage(OptionsPage): tags_enabled = self.ui.save_images_to_tags.isChecked() self.ui.ca_providers_groupbox.setEnabled(files_enabled or tags_enabled) + def select_never_replace_image_types(self): + (included_types, excluded_types, ok) = CAATypesSelectorDialog.display( + types_include=self.dont_replace_included_types, + types_exclude=self.dont_replace_excluded_types, + parent=self, + ) + if ok: + self.dont_replace_included_types = included_types + self.dont_replace_excluded_types = excluded_types + register_options_page(CoverOptionsPage) diff --git a/test/test_coverart_processing.py b/test/test_coverart_processing.py index a8cdfd87c..36c0c5d78 100644 --- a/test/test_coverart_processing.py +++ b/test/test_coverart_processing.py @@ -32,6 +32,7 @@ from picard.coverart.image import CoverArtImage from picard.coverart.processing import run_image_processors from picard.coverart.processing.filters import ( bigger_previous_image_filter, + image_types_filter, size_filter, size_metadata_filter, ) @@ -69,6 +70,9 @@ class ImageFiltersTest(PicardTestCase): 'cover_minimum_width': 500, 'cover_minimum_height': 500, 'dont_replace_with_smaller_cover': True, + 'dont_replace_cover_of_types': True, + 'dont_replace_included_types': ['front', 'booklet'], + 'dont_replace_excluded_types': ['back'], 'save_images_to_tags': True, } self.set_config_values(settings) @@ -89,12 +93,16 @@ class ImageFiltersTest(PicardTestCase): self.assertTrue(size_metadata_filter(image_metadata2)) self.assertTrue(size_metadata_filter(image_metadata3)) - def test_filter_by_previous_image_size(self): + def _create_fake_album(self): previous_coverartimage = CoverArtImage(types=['front'], support_types=True) previous_coverartimage.width = 1000 previous_coverartimage.height = 1000 album = Album(None) album.orig_metadata.images = ImageList([previous_coverartimage]) + return album + + def test_filter_by_previous_image_size(self): + album = self._create_fake_album() image1, info1 = create_fake_image(500, 500, 'jpg') image2, info2 = create_fake_image(2000, 2000, 'jpg') coverartimage = CoverArtImage(types=['front'], support_types=True) @@ -103,6 +111,20 @@ class ImageFiltersTest(PicardTestCase): coverartimage = CoverArtImage(types=['back'], support_types=True) self.assertTrue(bigger_previous_image_filter(image1, info1, album, coverartimage)) + def test_filter_by_image_type(self): + album = self._create_fake_album() + image, info = create_fake_image(1000, 1000, 'jpg') + coverartimage1 = CoverArtImage(types=['front'], support_types=True) + coverartimage2 = CoverArtImage(types=['back'], support_types=True) + coverartimage3 = CoverArtImage(types=['front', 'back'], support_types=True) + coverartimage4 = CoverArtImage(types=['spine'], support_types=True) + coverartimage5 = CoverArtImage(types=['booklet', 'spine'], support_types=True) + self.assertFalse(image_types_filter(image, info, album, coverartimage1)) + self.assertTrue(image_types_filter(image, info, album, coverartimage2)) + self.assertTrue(image_types_filter(image, info, album, coverartimage3)) + self.assertTrue(image_types_filter(image, info, album, coverartimage4)) + self.assertTrue(image_types_filter(image, info, album, coverartimage5)) + class ImageProcessorsTest(PicardTestCase): def setUp(self): diff --git a/ui/options_cover.ui b/ui/options_cover.ui index a61ea60b4..67f9ca876 100644 --- a/ui/options_cover.ui +++ b/ui/options_cover.ui @@ -52,6 +52,30 @@ + + + + + + Never replace cover images matching selected types + + + + + + + + 0 + 0 + + + + Select Types... + + + + +