mirror of
https://github.com/fergalmoran/picard.git
synced 2026-02-24 08:33:59 +00:00
Merge pull request #914 from rdswift/coverart_selection
PICARD-1273: Add an option to exclude new cover art type "Raw / Unedited"
This commit is contained in:
@@ -26,6 +26,7 @@ from collections import (
|
||||
OrderedDict,
|
||||
namedtuple,
|
||||
)
|
||||
from functools import partial
|
||||
|
||||
from PyQt5 import (
|
||||
QtCore,
|
||||
@@ -72,6 +73,9 @@ _CAA_THUMBNAIL_SIZE_MAP = OrderedDict([
|
||||
|
||||
_CAA_IMAGE_SIZE_DEFAULT = 500
|
||||
|
||||
_CAA_IMAGE_TYPE_DEFAULT_INCLUDE = ['front',]
|
||||
_CAA_IMAGE_TYPE_DEFAULT_EXCLUDE = ['raw/unedited', 'watermark',]
|
||||
|
||||
|
||||
def caa_url_fallback_list(desired_size, thumbnails):
|
||||
"""List of thumbnail urls equal or smaller than size, in size decreasing order
|
||||
@@ -94,34 +98,193 @@ def caa_url_fallback_list(desired_size, thumbnails):
|
||||
return urls
|
||||
|
||||
|
||||
class CAATypesSelectorDialog(QtWidgets.QDialog):
|
||||
_columns = 4
|
||||
class ArrowButton(QtWidgets.QPushButton):
|
||||
"""Standard arrow button for CAA image type selection dialog.
|
||||
|
||||
def __init__(self, parent=None, types=None):
|
||||
if types is None:
|
||||
types = []
|
||||
Keyword Arguments:
|
||||
label {string} -- Label to display on the button
|
||||
command {command} -- Command to execute when the button is clicked (default: {None})
|
||||
parent {[type]} -- Parent of the QPushButton object being created (default: {None})
|
||||
"""
|
||||
|
||||
ARROW_BUTTON_WIDTH = 35
|
||||
ARROW_BUTTON_HEIGHT = 20
|
||||
|
||||
def __init__(self, label, command=None, parent=None):
|
||||
super().__init__(label, parent=parent)
|
||||
if command is not None:
|
||||
self.clicked.connect(command)
|
||||
self.setFixedSize(QtCore.QSize(self.ARROW_BUTTON_WIDTH, self.ARROW_BUTTON_HEIGHT))
|
||||
|
||||
|
||||
class ArrowsColumn(QtWidgets.QWidget):
|
||||
"""Standard arrow buttons column for CAA image type selection dialog.
|
||||
|
||||
Keyword Arguments:
|
||||
selection_list {ListBox} -- ListBox of selected items associated with this arrow column
|
||||
ignore_list {ListBox} -- ListBox of unselected items associated with this arrow column
|
||||
callback {command} -- Command to execute after items are moved between lists (default: {None})
|
||||
reverse {bool} -- Determines whether the arrow directions should be reversed (default: {False})
|
||||
parent {[type]} -- Parent of the QWidget object being created (default: {None})
|
||||
"""
|
||||
|
||||
def __init__(self, selection_list, ignore_list, callback=None, reverse=False, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.selection_list = selection_list
|
||||
self.ignore_list = ignore_list
|
||||
self.callback = callback
|
||||
spacer_item = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
arrows_layout = QtWidgets.QVBoxLayout()
|
||||
arrows_layout.addItem(QtWidgets.QSpacerItem(spacer_item))
|
||||
self.button_add = ArrowButton('>' if reverse else '<', self.move_from_ignore)
|
||||
arrows_layout.addWidget(self.button_add)
|
||||
self.button_add_all = ArrowButton('>>' if reverse else '<<', self.move_all_from_ignore)
|
||||
arrows_layout.addWidget(self.button_add_all)
|
||||
self.button_remove = ArrowButton('<' if reverse else '>', self.move_to_ignore)
|
||||
arrows_layout.addWidget(self.button_remove)
|
||||
self.button_remove_all = ArrowButton('<<' if reverse else '>>', self.move_all_to_ignore)
|
||||
arrows_layout.addWidget(self.button_remove_all)
|
||||
arrows_layout.addItem(QtWidgets.QSpacerItem(spacer_item))
|
||||
self.setLayout(arrows_layout)
|
||||
|
||||
def move_from_ignore(self):
|
||||
self.ignore_list.move_selected_items(self.selection_list, callback=self.callback)
|
||||
|
||||
def move_all_from_ignore(self):
|
||||
self.ignore_list.move_all_items(self.selection_list, callback=self.callback)
|
||||
|
||||
def move_to_ignore(self):
|
||||
self.selection_list.move_selected_items(self.ignore_list, callback=self.callback)
|
||||
|
||||
def move_all_to_ignore(self):
|
||||
self.selection_list.move_all_items(self.ignore_list, callback=self.callback)
|
||||
|
||||
|
||||
class ListBox(QtWidgets.QListWidget):
|
||||
"""Standard list box for CAA image type selection dialog.
|
||||
|
||||
Keyword Arguments:
|
||||
parent {[type]} -- Parent of the QListWidget object being created (default: {None})
|
||||
"""
|
||||
|
||||
LISTBOX_WIDTH = 150
|
||||
LISTBOX_HEIGHT = 250
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.setFixedSize(QtCore.QSize(self.LISTBOX_WIDTH, self.LISTBOX_HEIGHT))
|
||||
self.setSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
|
||||
self.setSortingEnabled(True)
|
||||
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
|
||||
|
||||
def move_item(self, item, target_list):
|
||||
"""Move the specified item to another listbox."""
|
||||
self.takeItem(self.row(item))
|
||||
target_list.addItem(item)
|
||||
|
||||
def move_selected_items(self, target_list, callback=None):
|
||||
"""Move the selected item to another listbox."""
|
||||
for item in self.selectedItems():
|
||||
self.move_item(item, target_list)
|
||||
if callback:
|
||||
callback()
|
||||
|
||||
def move_all_items(self, target_list, callback=None):
|
||||
"""Move all items to another listbox."""
|
||||
while self.count():
|
||||
self.move_item(self.item(0), target_list)
|
||||
if callback:
|
||||
callback()
|
||||
|
||||
def all_items_data(self, role=QtCore.Qt.UserRole):
|
||||
for index in range(self.count()):
|
||||
yield self.item(index).data(role)
|
||||
|
||||
|
||||
class CAATypesSelectorDialog(QtWidgets.QDialog):
|
||||
"""Display dialog box to select the CAA image types to include and exclude from download and use.
|
||||
|
||||
Keyword Arguments:
|
||||
parent {[type]} -- Parent of the QDialog object being created (default: {None})
|
||||
types_include {[string]} -- List of CAA image types to include (default: {None})
|
||||
types_exclude {[string]} -- List of CAA image types to exclude (default: {None})
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None, types_include=None, types_exclude=None):
|
||||
super().__init__(parent)
|
||||
if types_include is None:
|
||||
types_include = []
|
||||
if types_exclude is None:
|
||||
types_exclude = []
|
||||
|
||||
self.setWindowTitle(_("Cover art types"))
|
||||
self._items = {}
|
||||
self.layout = QtWidgets.QVBoxLayout(self)
|
||||
|
||||
# Create list boxes for dialog
|
||||
self.list_include = ListBox()
|
||||
self.list_exclude = ListBox()
|
||||
self.list_ignore = ListBox()
|
||||
|
||||
# Populate list boxes from current settings
|
||||
self.fill_lists(types_include, types_exclude)
|
||||
|
||||
# Set triggers when the lists receive the current focus
|
||||
self.list_include.clicked.connect(partial(self.clear_focus, [self.list_ignore, self.list_exclude,]))
|
||||
self.list_exclude.clicked.connect(partial(self.clear_focus, [self.list_ignore, self.list_include,]))
|
||||
self.list_ignore.clicked.connect(partial(self.clear_focus, [self.list_include, self.list_exclude,]))
|
||||
|
||||
# Add instructions to the dialog box
|
||||
instructions = QtWidgets.QLabel()
|
||||
instructions.setText(_("Please select the contents of the image type 'Include' and 'Exclude' lists."))
|
||||
instructions.setWordWrap(True)
|
||||
instructions.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
|
||||
self.layout.addWidget(instructions)
|
||||
|
||||
grid = QtWidgets.QWidget()
|
||||
gridlayout = QtWidgets.QGridLayout()
|
||||
grid.setLayout(gridlayout)
|
||||
|
||||
for index, caa_type in enumerate(CAA_TYPES):
|
||||
row = index // self._columns
|
||||
column = index % self._columns
|
||||
name = caa_type["name"]
|
||||
text = translate_caa_type(name)
|
||||
item = QtWidgets.QCheckBox(text)
|
||||
item.setChecked(name in types)
|
||||
self._items[item] = caa_type
|
||||
gridlayout.addWidget(item, row, column)
|
||||
self.arrows_include = ArrowsColumn(
|
||||
self.list_include,
|
||||
self.list_ignore,
|
||||
callback=self.set_buttons_enabled_state,
|
||||
)
|
||||
|
||||
self.arrows_exclude = ArrowsColumn(
|
||||
self.list_exclude,
|
||||
self.list_ignore,
|
||||
callback=self.set_buttons_enabled_state,
|
||||
reverse=True
|
||||
)
|
||||
|
||||
def add_widget(row=0, column=0, widget=None):
|
||||
gridlayout.addWidget(widget, row, column)
|
||||
|
||||
add_widget(row=0, column=0, widget=QtWidgets.QLabel(_("Include types list")))
|
||||
add_widget(row=1, column=0, widget=self.list_include)
|
||||
|
||||
add_widget(row=1, column=1, widget=self.arrows_include)
|
||||
add_widget(row=1, column=2, widget=self.list_ignore)
|
||||
add_widget(row=1, column=3, widget=self.arrows_exclude)
|
||||
|
||||
add_widget(row=0, column=4, widget=QtWidgets.QLabel(_("Exclude types list")))
|
||||
add_widget(row=1, column=4, widget=self.list_exclude)
|
||||
|
||||
self.layout.addWidget(grid)
|
||||
|
||||
# Add usage explanation to the dialog box
|
||||
instructions = QtWidgets.QLabel()
|
||||
instructions.setText(_(
|
||||
"CAA images with an image type found in the 'Include' list will be downloaded and used "
|
||||
"UNLESS they also have an image type found in the 'Exclude' list. Images with types "
|
||||
"found in the 'Exclude' list will NEVER be used. Image types not appearing in the 'Include' "
|
||||
"or 'Exclude' lists will not be considered when determining whether or not to download and "
|
||||
"use a CAA image.\n")
|
||||
)
|
||||
instructions.setWordWrap(True)
|
||||
instructions.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
|
||||
self.layout.addWidget(instructions)
|
||||
|
||||
self.buttonbox = QtWidgets.QDialogButtonBox(self)
|
||||
self.buttonbox.setOrientation(QtCore.Qt.Horizontal)
|
||||
self.buttonbox.addButton(
|
||||
@@ -132,8 +295,10 @@ class CAATypesSelectorDialog(QtWidgets.QDialog):
|
||||
StandardButton(StandardButton.HELP), QtWidgets.QDialogButtonBox.HelpRole)
|
||||
|
||||
extrabuttons = [
|
||||
(N_("Chec&k all"), self.checkall),
|
||||
(N_("&Uncheck all"), self.uncheckall),
|
||||
(N_("I&nclude all"), self.move_all_to_include_list),
|
||||
(N_("E&xclude all"), self.move_all_to_exclude_list),
|
||||
(N_("C&lear all"), self.move_all_to_ignore_list),
|
||||
(N_("Restore &Defaults"), self.reset_to_defaults),
|
||||
]
|
||||
for label, callback in extrabuttons:
|
||||
button = QtWidgets.QPushButton(_(label))
|
||||
@@ -148,30 +313,97 @@ class CAATypesSelectorDialog(QtWidgets.QDialog):
|
||||
self.buttonbox.rejected.connect(self.reject)
|
||||
self.buttonbox.helpRequested.connect(self.help)
|
||||
|
||||
self.set_buttons_enabled_state()
|
||||
|
||||
def move_all_to_include_list(self):
|
||||
self.list_ignore.move_all_items(self.list_include)
|
||||
self.list_exclude.move_all_items(self.list_include)
|
||||
self.set_buttons_enabled_state()
|
||||
|
||||
def move_all_to_exclude_list(self):
|
||||
self.list_ignore.move_all_items(self.list_exclude)
|
||||
self.list_include.move_all_items(self.list_exclude)
|
||||
self.set_buttons_enabled_state()
|
||||
|
||||
def move_all_to_ignore_list(self):
|
||||
self.list_include.move_all_items(self.list_ignore)
|
||||
self.list_exclude.move_all_items(self.list_ignore)
|
||||
self.set_buttons_enabled_state()
|
||||
|
||||
def fill_lists(self, includes, excludes):
|
||||
"""Fill dialog listboxes.
|
||||
|
||||
First clears the contents of the three listboxes, and then populates the listboxes
|
||||
from the dictionary of standard CAA types, using the provided 'includes' and
|
||||
'excludes' lists to determine the appropriate list for each type.
|
||||
|
||||
Arguments:
|
||||
includes -- list of standard image types to place in the "Include" listbox
|
||||
excludes -- list of standard image types to place in the "Exclude" listbox
|
||||
"""
|
||||
self.list_include.clear()
|
||||
self.list_exclude.clear()
|
||||
self.list_ignore.clear()
|
||||
for caa_type in CAA_TYPES:
|
||||
name = caa_type['name']
|
||||
title = translate_caa_type(caa_type['title'])
|
||||
item = QtWidgets.QListWidgetItem(title)
|
||||
item.setData(QtCore.Qt.UserRole, name)
|
||||
if name in includes:
|
||||
self.list_include.addItem(item)
|
||||
elif name in excludes:
|
||||
self.list_exclude.addItem(item)
|
||||
else:
|
||||
self.list_ignore.addItem(item)
|
||||
|
||||
def help(self):
|
||||
webbrowser2.goto('doc_cover_art_types')
|
||||
|
||||
def uncheckall(self):
|
||||
self._set_checked_all(False)
|
||||
def get_selected_types_include(self):
|
||||
return list(self.list_include.all_items_data()) or ['front']
|
||||
|
||||
def checkall(self):
|
||||
self._set_checked_all(True)
|
||||
def get_selected_types_exclude(self):
|
||||
return list(self.list_exclude.all_items_data()) or ['none']
|
||||
|
||||
def _set_checked_all(self, value):
|
||||
for item in self._items.keys():
|
||||
item.setChecked(value)
|
||||
def clear_focus(self, lists):
|
||||
for temp_list in lists:
|
||||
temp_list.clearSelection()
|
||||
self.set_buttons_enabled_state()
|
||||
|
||||
def get_selected_types(self):
|
||||
return [typ['name'] for item, typ in self._items.items() if
|
||||
item.isChecked()] or ['front']
|
||||
def reset_to_defaults(self):
|
||||
self.fill_lists(_CAA_IMAGE_TYPE_DEFAULT_INCLUDE, _CAA_IMAGE_TYPE_DEFAULT_EXCLUDE)
|
||||
self.set_buttons_enabled_state()
|
||||
|
||||
def set_buttons_enabled_state(self):
|
||||
has_items_include = self.list_include.count()
|
||||
has_items_exclude = self.list_exclude.count()
|
||||
has_items_ignore = self.list_ignore.count()
|
||||
|
||||
has_selected_include = bool(self.list_include.selectedItems())
|
||||
has_selected_exclude = bool(self.list_exclude.selectedItems())
|
||||
has_selected_ignore = bool(self.list_ignore.selectedItems())
|
||||
|
||||
# "Include" list buttons
|
||||
self.arrows_include.button_add.setEnabled(has_items_ignore and has_selected_ignore)
|
||||
self.arrows_include.button_add_all.setEnabled(has_items_ignore)
|
||||
self.arrows_include.button_remove.setEnabled(has_items_include and has_selected_include)
|
||||
self.arrows_include.button_remove_all.setEnabled(has_items_include)
|
||||
|
||||
# "Exclude" list buttons
|
||||
self.arrows_exclude.button_add.setEnabled(has_items_ignore and has_selected_ignore)
|
||||
self.arrows_exclude.button_add_all.setEnabled(has_items_ignore)
|
||||
self.arrows_exclude.button_remove.setEnabled(has_items_exclude and has_selected_exclude)
|
||||
self.arrows_exclude.button_remove_all.setEnabled(has_items_exclude)
|
||||
|
||||
@staticmethod
|
||||
def run(parent=None, types=None):
|
||||
if types is None:
|
||||
types = []
|
||||
dialog = CAATypesSelectorDialog(parent, types)
|
||||
def run(parent=None, types_include=None, types_exclude=None):
|
||||
if types_include is None:
|
||||
types_include = []
|
||||
if types_exclude is None:
|
||||
types_exclude = []
|
||||
dialog = CAATypesSelectorDialog(parent, types_include, types_exclude)
|
||||
result = dialog.exec_()
|
||||
return (dialog.get_selected_types(), result == QtWidgets.QDialog.Accepted)
|
||||
return (dialog.get_selected_types_include(), dialog.get_selected_types_exclude(), result == QtWidgets.QDialog.Accepted)
|
||||
|
||||
|
||||
class ProviderOptionsCaa(ProviderOptions):
|
||||
@@ -183,10 +415,10 @@ class ProviderOptionsCaa(ProviderOptions):
|
||||
config.BoolOption("setting", "caa_save_single_front_image", False),
|
||||
config.BoolOption("setting", "caa_approved_only", False),
|
||||
config.BoolOption("setting", "caa_image_type_as_filename", False),
|
||||
config.IntOption("setting", "caa_image_size",
|
||||
_CAA_IMAGE_SIZE_DEFAULT),
|
||||
config.ListOption("setting", "caa_image_types", ["front"]),
|
||||
config.IntOption("setting", "caa_image_size", _CAA_IMAGE_SIZE_DEFAULT),
|
||||
config.ListOption("setting", "caa_image_types", _CAA_IMAGE_TYPE_DEFAULT_INCLUDE),
|
||||
config.BoolOption("setting", "caa_restrict_image_types", True),
|
||||
config.ListOption("setting", "caa_image_types_to_omit", _CAA_IMAGE_TYPE_DEFAULT_EXCLUDE),
|
||||
]
|
||||
|
||||
_options_ui = Ui_CaaOptions
|
||||
@@ -231,11 +463,11 @@ class ProviderOptionsCaa(ProviderOptions):
|
||||
self.ui.select_caa_types.setEnabled(enabled)
|
||||
|
||||
def select_caa_types(self):
|
||||
(types, ok) = CAATypesSelectorDialog.run(
|
||||
self, config.setting["caa_image_types"])
|
||||
(types, types_to_omit, ok) = CAATypesSelectorDialog.run(
|
||||
self, config.setting["caa_image_types"], config.setting["caa_image_types_to_omit"])
|
||||
if ok:
|
||||
config.setting["caa_image_types"] = types
|
||||
|
||||
config.setting["caa_image_types_to_omit"] = types_to_omit
|
||||
|
||||
|
||||
class CoverArtProviderCaa(CoverArtProvider):
|
||||
@@ -253,6 +485,7 @@ class CoverArtProviderCaa(CoverArtProvider):
|
||||
def __init__(self, coverart):
|
||||
super().__init__(coverart)
|
||||
self.caa_types = list(map(str.lower, config.setting["caa_image_types"]))
|
||||
self.caa_types_to_omit = list(map(str.lower, config.setting["caa_image_types_to_omit"]))
|
||||
self.len_caa_types = len(self.caa_types)
|
||||
self.restrict_types = config.setting["caa_restrict_image_types"]
|
||||
|
||||
@@ -261,16 +494,14 @@ class CoverArtProviderCaa(CoverArtProvider):
|
||||
# MB web service indicates if CAA has artwork
|
||||
# https://tickets.metabrainz.org/browse/MBS-4536
|
||||
if 'cover-art-archive' not in self.release:
|
||||
log.debug("No Cover Art Archive information for %s"
|
||||
% self.release['id'])
|
||||
log.debug('No Cover Art Archive information for {release_id}'.format(release_id=self.release['id']))
|
||||
return False
|
||||
|
||||
caa_node = self.release['cover-art-archive']
|
||||
caa_has_suitable_artwork = caa_node['artwork']
|
||||
|
||||
if not caa_has_suitable_artwork:
|
||||
log.debug("There are no images in the Cover Art Archive for %s"
|
||||
% self.release['id'])
|
||||
log.debug('There are no images in the Cover Art Archive for {release_id}'.format(release_id=self.release['id']))
|
||||
return False
|
||||
|
||||
if self.restrict_types:
|
||||
@@ -299,11 +530,9 @@ class CoverArtProviderCaa(CoverArtProvider):
|
||||
caa_has_suitable_artwork = front_in_caa or back_in_caa
|
||||
|
||||
if not caa_has_suitable_artwork:
|
||||
log.debug("There are no suitable images in the Cover Art Archive for %s"
|
||||
% self.release['id'])
|
||||
log.debug('There are no suitable images in the Cover Art Archive for {release_id}'.format(release_id=self.release['id']))
|
||||
else:
|
||||
log.debug("There are suitable images in the Cover Art Archive for %s"
|
||||
% self.release['id'])
|
||||
log.debug('There are suitable images in the Cover Art Archive for {release_id}'.format(release_id=self.release['id']))
|
||||
|
||||
return caa_has_suitable_artwork
|
||||
|
||||
@@ -313,7 +542,7 @@ class CoverArtProviderCaa(CoverArtProvider):
|
||||
self.coverart.front_image_found:
|
||||
return False
|
||||
if self.restrict_types and not self.len_caa_types:
|
||||
log.debug("User disabled all Cover Art Archive types")
|
||||
log.debug('User disabled all Cover Art Archive types')
|
||||
return False
|
||||
return self._has_suitable_artwork
|
||||
|
||||
@@ -346,6 +575,8 @@ class CoverArtProviderCaa(CoverArtProvider):
|
||||
except ValueError:
|
||||
self.error("Invalid JSON: %s" % (http.url().toString()))
|
||||
else:
|
||||
if self.restrict_types:
|
||||
log.debug('CAA types: included: %s, excluded: %s' % (self.caa_types, self.caa_types_to_omit,))
|
||||
for image in caa_data["images"]:
|
||||
if config.setting["caa_approved_only"] and not image["approved"]:
|
||||
continue
|
||||
@@ -364,11 +595,18 @@ class CoverArtProviderCaa(CoverArtProvider):
|
||||
# only keep enabled caa types
|
||||
types = set(image["types"]).intersection(
|
||||
set(self.caa_types))
|
||||
if types and self.caa_types_to_omit:
|
||||
types = not set(image["types"]).intersection(
|
||||
set(self.caa_types_to_omit))
|
||||
log.debug('CAA image {status}: {image_name} {image_types}'.format(
|
||||
status=('accepted' if types else 'rejected'),
|
||||
image_name=image['image'],
|
||||
image_types=image['types'],)
|
||||
)
|
||||
else:
|
||||
types = True
|
||||
if types:
|
||||
urls = caa_url_fallback_list(config.setting["caa_image_size"],
|
||||
image["thumbnails"])
|
||||
urls = caa_url_fallback_list(config.setting["caa_image_size"], image["thumbnails"])
|
||||
if not urls or is_pdf:
|
||||
url = image["image"]
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user