Add option to filter images by type

This commit is contained in:
twodoorcoupe
2024-07-08 12:18:42 +02:00
parent c96f0b07e6
commit 17f5c61ae1
7 changed files with 114 additions and 2 deletions

View File

@@ -85,6 +85,8 @@ DEFAULT_QUERY_LIMIT = 50
DEFAULT_DRIVES = get_default_cdrom_drives() 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 = [ DEFAULT_CA_PROVIDERS = [
('Cover Art Archive', True), ('Cover Art Archive', True),
('UrlRelationships', True), ('UrlRelationships', True),

View File

@@ -69,6 +69,20 @@ def bigger_previous_image_filter(data, info, album, coverartimage):
def image_types_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 return True

View File

@@ -34,6 +34,8 @@ from picard.config import (
from picard.const import MUSICBRAINZ_SERVERS from picard.const import MUSICBRAINZ_SERVERS
from picard.const.defaults import ( from picard.const.defaults import (
DEFAULT_AUTOBACKUP_DIRECTORY, DEFAULT_AUTOBACKUP_DIRECTORY,
DEFAULT_CA_NEVER_REPLACE_TYPE_EXCLUDE,
DEFAULT_CA_NEVER_REPLACE_TYPE_INCLUDE,
DEFAULT_CA_PROVIDERS, DEFAULT_CA_PROVIDERS,
DEFAULT_CAA_IMAGE_SIZE, DEFAULT_CAA_IMAGE_SIZE,
DEFAULT_CAA_IMAGE_TYPE_EXCLUDE, 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")) 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")) 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', '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', '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_overwrite', False, title=N_("Overwrite existing image files"))
BoolOption('setting', 'save_images_to_files', False, title=N_("Save cover images as separate files")) BoolOption('setting', 'save_images_to_files', False, title=N_("Save cover images as separate files"))

View File

@@ -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 = QtWidgets.QCheckBox(parent=self.save_images_to_tags)
self.cb_dont_replace_with_smaller.setObjectName("cb_dont_replace_with_smaller") self.cb_dont_replace_with_smaller.setObjectName("cb_dont_replace_with_smaller")
self.vboxlayout.addWidget(self.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.verticalLayout.addWidget(self.save_images_to_tags)
self.save_images_to_files = QtWidgets.QGroupBox(parent=CoverOptionsPage) self.save_images_to_files = QtWidgets.QGroupBox(parent=CoverOptionsPage)
self.save_images_to_files.setCheckable(True) 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.save_images_to_tags.setTitle(_("Embed cover images into tags"))
self.cb_embed_front_only.setText(_("Embed only a single front image")) 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_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.save_images_to_files.setTitle(_("Save cover images as separate files"))
self.label_use_filename.setText(_("Use the following file name for images:")) self.label_use_filename.setText(_("Use the following file name for images:"))
self.save_images_overwrite.setText(_("Overwrite the file if it already exists")) self.save_images_overwrite.setText(_("Overwrite the file if it already exists"))

View File

@@ -31,6 +31,10 @@ from picard.config import (
Option, Option,
get_config, 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.coverart.providers import cover_art_providers
from picard.extension_points.options_pages import register_options_page from picard.extension_points.options_pages import register_options_page
from picard.i18n import ( from picard.i18n import (
@@ -38,6 +42,7 @@ from picard.i18n import (
gettext as _, gettext as _,
) )
from picard.ui.caa_types_selector import CAATypesSelectorDialog
from picard.ui.forms.ui_options_cover import Ui_CoverOptionsPage from picard.ui.forms.ui_options_cover import Ui_CoverOptionsPage
from picard.ui.moveable_list_view import MoveableListView from picard.ui.moveable_list_view import MoveableListView
from picard.ui.options import OptionsPage 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_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_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.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.move_view = MoveableListView(self.ui.ca_providers_list, self.ui.up_button,
self.ui.down_button) self.ui.down_button)
self.register_setting('save_images_to_tags', ['save_images_to_tags']) 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('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_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('save_images_to_files', ['save_images_to_files'])
self.register_setting('cover_image_filename', ['cover_image_filename']) self.register_setting('cover_image_filename', ['cover_image_filename'])
self.register_setting('save_images_overwrite', ['save_images_overwrite']) self.register_setting('save_images_overwrite', ['save_images_overwrite'])
@@ -78,6 +88,8 @@ class CoverOptionsPage(OptionsPage):
def restore_defaults(self): def restore_defaults(self):
# Remove previous entries # Remove previous entries
self.ui.ca_providers_list.clear() 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() super().restore_defaults()
def _load_cover_art_providers(self): 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.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_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_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.save_images_to_files.setChecked(config.setting['save_images_to_files'])
self.ui.cover_image_filename.setText(config.setting['cover_image_filename']) self.ui.cover_image_filename.setText(config.setting['cover_image_filename'])
self.ui.save_images_overwrite.setChecked(config.setting['save_images_overwrite']) 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['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['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_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['save_images_to_files'] = self.ui.save_images_to_files.isChecked()
config.setting['cover_image_filename'] = self.ui.cover_image_filename.text() config.setting['cover_image_filename'] = self.ui.cover_image_filename.text()
config.setting['save_images_overwrite'] = self.ui.save_images_overwrite.isChecked() 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() tags_enabled = self.ui.save_images_to_tags.isChecked()
self.ui.ca_providers_groupbox.setEnabled(files_enabled or tags_enabled) 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) register_options_page(CoverOptionsPage)

View File

@@ -32,6 +32,7 @@ from picard.coverart.image import CoverArtImage
from picard.coverart.processing import run_image_processors from picard.coverart.processing import run_image_processors
from picard.coverart.processing.filters import ( from picard.coverart.processing.filters import (
bigger_previous_image_filter, bigger_previous_image_filter,
image_types_filter,
size_filter, size_filter,
size_metadata_filter, size_metadata_filter,
) )
@@ -69,6 +70,9 @@ class ImageFiltersTest(PicardTestCase):
'cover_minimum_width': 500, 'cover_minimum_width': 500,
'cover_minimum_height': 500, 'cover_minimum_height': 500,
'dont_replace_with_smaller_cover': True, '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, 'save_images_to_tags': True,
} }
self.set_config_values(settings) 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_metadata2))
self.assertTrue(size_metadata_filter(image_metadata3)) 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 = CoverArtImage(types=['front'], support_types=True)
previous_coverartimage.width = 1000 previous_coverartimage.width = 1000
previous_coverartimage.height = 1000 previous_coverartimage.height = 1000
album = Album(None) album = Album(None)
album.orig_metadata.images = ImageList([previous_coverartimage]) 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') image1, info1 = create_fake_image(500, 500, 'jpg')
image2, info2 = create_fake_image(2000, 2000, 'jpg') image2, info2 = create_fake_image(2000, 2000, 'jpg')
coverartimage = CoverArtImage(types=['front'], support_types=True) coverartimage = CoverArtImage(types=['front'], support_types=True)
@@ -103,6 +111,20 @@ class ImageFiltersTest(PicardTestCase):
coverartimage = CoverArtImage(types=['back'], support_types=True) coverartimage = CoverArtImage(types=['back'], support_types=True)
self.assertTrue(bigger_previous_image_filter(image1, info1, album, coverartimage)) 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): class ImageProcessorsTest(PicardTestCase):
def setUp(self): def setUp(self):

View File

@@ -52,6 +52,30 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<layout class="QHBoxLayout" name="never_replace_types_layout">
<item>
<widget class="QCheckBox" name="cb_never_replace_types">
<property name="text">
<string>Never replace cover images matching selected types</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="select_types_button">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Select Types...</string>
</property>
</widget>
</item>
</layout>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>