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:
Laurent Monin
2018-08-27 18:48:18 +02:00
committed by GitHub

View File

@@ -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: