From 6721fb378e448172fae858fe2777f52aeca28399 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 15 Sep 2023 08:03:19 +0200 Subject: [PATCH 1/3] deezerart: Use astrcmp instead of difflib This provides both better comparison and also makes the plugin compatible with Picard packaged for Windows / macOS (where difflib is not included). --- plugins/deezerart/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/deezerart/__init__.py b/plugins/deezerart/__init__.py index 64728dd3..63a015fa 100644 --- a/plugins/deezerart/__init__.py +++ b/plugins/deezerart/__init__.py @@ -6,7 +6,6 @@ PLUGIN_LICENSE = "GPL-3.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-3.0.html" -from difflib import SequenceMatcher from typing import Any, List, Optional from urllib.parse import urlsplit @@ -14,6 +13,7 @@ from picard import config from picard.coverart import providers from picard.coverart.image import CoverArtImage +from picard.util.astrcmp import astrcmp from PyQt5 import QtNetwork as QtNet from .deezer import Client, SearchOptions, obj @@ -21,12 +21,13 @@ __version__ = PLUGIN_VERSION +MIN_SIMILARITY_THRESHOLD = 0.65 + def is_similar(str1: str, str2: str) -> bool: if str1 in str2: return True - # Python doc considers a ratio equal to 0.6 a good match. - return SequenceMatcher(None, str1, str2).quick_ratio() >= 0.65 + return astrcmp(str1, str2) >= MIN_SIMILARITY_THRESHOLD def is_deezer_url(url: str) -> bool: From 8830729c7f30c88c4042819f919fb80c3e288e68 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 15 Sep 2023 08:05:59 +0200 Subject: [PATCH 2/3] deezerart: Extended debug logging for similarity mismatches --- plugins/deezerart/__init__.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/plugins/deezerart/__init__.py b/plugins/deezerart/__init__.py index 63a015fa..5dc24752 100644 --- a/plugins/deezerart/__init__.py +++ b/plugins/deezerart/__init__.py @@ -82,8 +82,8 @@ def queue_images(self): def error(self, msg): super().error(self._log_prefix + msg) - def log_debug(self, msg: Any): - picard.log.debug('%s%s', self._log_prefix, msg) + def log_debug(self, msg: Any, *args): + picard.log.debug(self._log_prefix + msg, *args) def _url_callback(self, url: str): if is_deezer_url(url): @@ -123,7 +123,11 @@ def _queue_from_search(self, results: List[obj.APIObject], error: Optional[QtNet for result in results: if not isinstance(result, obj.Track): continue - if not is_similar(artist, result.artist.name) or not is_similar(album, result.album.title): + if not is_similar(artist, result.artist.name): + self.log_debug('artist similarity below threshold: %r ~ %r', artist, result.artist.title) + continue + if not is_similar(album, result.album.title): + self.log_debug('album similarity below threshold: %r ~ %r', album, result.album.title) continue cover_url = result.album.cover_url(obj.CoverSize(config.setting['deezerart_size'])) self.queue_put(CoverArtImage(cover_url)) From bfb0b839285b29a4c6b7a590fb470889c592b13d Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 15 Sep 2023 08:34:26 +0200 Subject: [PATCH 3/3] deezerart: make min. similarity threshold configurable --- plugins/deezerart/__init__.py | 20 +++++++----- plugins/deezerart/options.py | 35 +++++++++++++++------ plugins/deezerart/options.ui | 58 ++++++++++++++++++++++++++--------- 3 files changed, 82 insertions(+), 31 deletions(-) diff --git a/plugins/deezerart/__init__.py b/plugins/deezerart/__init__.py index 5dc24752..22bb0fbf 100644 --- a/plugins/deezerart/__init__.py +++ b/plugins/deezerart/__init__.py @@ -1,7 +1,7 @@ PLUGIN_NAME = "Deezer cover art" PLUGIN_AUTHOR = "Fabio Forni " PLUGIN_DESCRIPTION = "Fetch cover arts from Deezer" -PLUGIN_VERSION = '1.1.1' +PLUGIN_VERSION = '1.2' PLUGIN_API_VERSIONS = ['2.5'] PLUGIN_LICENSE = "GPL-3.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-3.0.html" @@ -21,13 +21,13 @@ __version__ = PLUGIN_VERSION -MIN_SIMILARITY_THRESHOLD = 0.65 +DEFAULT_SIMILARITY_THRESHOLD = 0.6 -def is_similar(str1: str, str2: str) -> bool: +def is_similar(str1: str, str2: str, min_similarity: float = DEFAULT_SIMILARITY_THRESHOLD) -> bool: if str1 in str2: return True - return astrcmp(str1, str2) >= MIN_SIMILARITY_THRESHOLD + return astrcmp(str1, str2) >= min_similarity def is_deezer_url(url: str) -> bool: @@ -37,16 +37,21 @@ def is_deezer_url(url: str) -> bool: class OptionsPage(providers.ProviderOptions): NAME = 'Deezer' TITLE = 'Deezer' - options = [config.TextOption('setting', 'deezerart_size', obj.CoverSize.BIG.value)] + options = [ + config.TextOption('setting', 'deezerart_size', obj.CoverSize.BIG.value), + config.FloatOption('setting', 'deezerart_min_similarity', DEFAULT_SIMILARITY_THRESHOLD), + ] _options_ui = Ui_Form def load(self): for s in obj.CoverSize: self.ui.size.addItem(str(s.name).title(), userData=s.value) self.ui.size.setCurrentIndex(self.ui.size.findData(config.setting['deezerart_size'])) + self.ui.min_similarity.setValue(int(config.setting["deezerart_min_similarity"] * 100)) def save(self): config.setting['deezerart_size'] = self.ui.size.currentData() + config.setting['deezerart_min_similarity'] = float(self.ui.min_similarity.value()) / 100.0 class Provider(providers.CoverArtProvider): @@ -120,13 +125,14 @@ def _queue_from_search(self, results: List[obj.APIObject], error: Optional[QtNet return artist = self._artist() album = self.metadata['album'] + min_similarity = config.setting['deezerart_min_similarity'] for result in results: if not isinstance(result, obj.Track): continue - if not is_similar(artist, result.artist.name): + if not is_similar(artist, result.artist.name, min_similarity): self.log_debug('artist similarity below threshold: %r ~ %r', artist, result.artist.title) continue - if not is_similar(album, result.album.title): + if not is_similar(album, result.album.title, min_similarity): self.log_debug('album similarity below threshold: %r ~ %r', album, result.album.title) continue cover_url = result.album.cover_url(obj.CoverSize(config.setting['deezerart_size'])) diff --git a/plugins/deezerart/options.py b/plugins/deezerart/options.py index e610c396..4936bf06 100644 --- a/plugins/deezerart/options.py +++ b/plugins/deezerart/options.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'plugins/deezerart/options.ui' # -# Created by: PyQt5 UI code generator 5.15.4 +# Created by: PyQt5 UI code generator 5.15.9 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. @@ -15,19 +15,33 @@ class Ui_Form(object): def setupUi(self, Form): Form.setObjectName("Form") Form.resize(420, 320) - self.verticalLayout = QtWidgets.QVBoxLayout(Form) - self.verticalLayout.setObjectName("verticalLayout") - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setObjectName("gridLayout") self.sizeLabel = QtWidgets.QLabel(Form) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.sizeLabel.sizePolicy().hasHeightForWidth()) + self.sizeLabel.setSizePolicy(sizePolicy) self.sizeLabel.setObjectName("sizeLabel") - self.horizontalLayout.addWidget(self.sizeLabel) + self.gridLayout.addWidget(self.sizeLabel, 0, 0, 1, 1) self.size = QtWidgets.QComboBox(Form) self.size.setObjectName("size") - self.horizontalLayout.addWidget(self.size) - self.verticalLayout.addLayout(self.horizontalLayout) + self.gridLayout.addWidget(self.size, 0, 2, 1, 1) + self.min_similarity_label = QtWidgets.QLabel(Form) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.min_similarity_label.sizePolicy().hasHeightForWidth()) + self.min_similarity_label.setSizePolicy(sizePolicy) + self.min_similarity_label.setObjectName("min_similarity_label") + self.gridLayout.addWidget(self.min_similarity_label, 1, 0, 1, 2) + self.min_similarity = QtWidgets.QSpinBox(Form) + self.min_similarity.setMaximum(100) + self.min_similarity.setObjectName("min_similarity") + self.gridLayout.addWidget(self.min_similarity, 1, 2, 1, 1) spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout.addItem(spacerItem) + self.gridLayout.addItem(spacerItem, 2, 1, 1, 1) self.retranslateUi(Form) QtCore.QMetaObject.connectSlotsByName(Form) @@ -36,3 +50,6 @@ def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate Form.setWindowTitle(_translate("Form", "Form")) self.sizeLabel.setText(_translate("Form", "Cover size:")) + self.min_similarity_label.setText(_translate("Form", "Minimal similarity for matches:")) + self.min_similarity.setSuffix(_translate("Form", " %")) + self.min_similarity.setPrefix(_translate("Form", " ")) diff --git a/plugins/deezerart/options.ui b/plugins/deezerart/options.ui index d889194f..9d4ce4de 100644 --- a/plugins/deezerart/options.ui +++ b/plugins/deezerart/options.ui @@ -13,22 +13,50 @@ Form - - - - - - - Cover size: - - - - - - - + + + + + + 0 + 0 + + + + Cover size: + + + + + + + + + + + 0 + 0 + + + + Minimal similarity for matches: + + + + + + + % + + + + + + 100 + + - + Qt::Vertical