From 6ae7c58fc850673e7c7ae5ba58f8736cbcadafb1 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 4 Sep 2018 15:10:03 +0200 Subject: [PATCH 001/123] lastfm: Basic changes for Picard 2.0 --- plugins/lastfm/__init__.py | 34 +++--- plugins/lastfm/options_lastfm.ui | 162 +++++++++++++++++----------- plugins/lastfm/ui_options_lastfm.py | 132 ++++++++++++----------- 3 files changed, 188 insertions(+), 140 deletions(-) diff --git a/plugins/lastfm/__init__.py b/plugins/lastfm/__init__.py index ee6d4a3a..f7dc6cf3 100644 --- a/plugins/lastfm/__init__.py +++ b/plugins/lastfm/__init__.py @@ -1,18 +1,18 @@ # -*- coding: utf-8 -*- PLUGIN_NAME = 'Last.fm' -PLUGIN_AUTHOR = 'Lukáš Lalinský' +PLUGIN_AUTHOR = 'Lukáš Lalinský, Philipp Wolfer' PLUGIN_DESCRIPTION = 'Use tags from Last.fm as genre.' -PLUGIN_VERSION = "0.5" +PLUGIN_VERSION = "0.6" PLUGIN_API_VERSIONS = ["2.0"] -from PyQt4 import QtCore +import traceback +from functools import partial +from PyQt5 import QtCore from picard.metadata import register_track_metadata_processor from picard.ui.options import register_options_page, OptionsPage from picard.config import BoolOption, IntOption, TextOption from picard.plugins.lastfm.ui_options_lastfm import Ui_LastfmOptionsPage -from picard.util import partial -import traceback LASTFM_HOST = "ws.audioscrobbler.com" LASTFM_PORT = 80 @@ -26,7 +26,7 @@ # Cache for Tags to avoid re-requesting tags within same Picard session _cache = {} # Keeps track of requests for tags made to webservice API but not yet returned (to avoid re-requesting the same URIs) -_pending_xmlws_requests = {} +_pending_requests = {} # TODO: move this to an options page TRANSLATE_TAGS = { @@ -75,9 +75,9 @@ def _tags_downloaded(album, metadata, min_usage, ignore, next, current, data, re _tags_finalize(album, metadata, current + tags, next) # Process any pending requests for the same URL - if url in _pending_xmlws_requests: - pending = _pending_xmlws_requests[url] - del _pending_xmlws_requests[url] + if url in _pending_requests: + pending = _pending_requests[url] + del _pending_requests[url] for delayed_call in pending: delayed_call() @@ -91,18 +91,18 @@ def _tags_downloaded(album, metadata, min_usage, ignore, next, current, data, re def get_tags(album, metadata, path, min_usage, ignore, next, current): """Get tags from an URL.""" - url = str(QtCore.QUrl.fromPercentEncoding(path)) + url = str(QtCore.QUrl.fromPercentEncoding(path.encode('utf-8'))) if url in _cache: _tags_finalize(album, metadata, current + _cache[url], next) else: # If we have already sent a request for this URL, delay this call until later - if url in _pending_xmlws_requests: - _pending_xmlws_requests[url].append(partial(get_tags, album, metadata, path, min_usage, ignore, next, current)) + if url in _pending_requests: + _pending_requests[url].append(partial(get_tags, album, metadata, path, min_usage, ignore, next, current)) else: - _pending_xmlws_requests[url] = [] + _pending_requests[url] = [] album._requests += 1 - album.tagger.xmlws.get(LASTFM_HOST, LASTFM_PORT, path, + album.tagger.webservice.get(LASTFM_HOST, LASTFM_PORT, path, partial(_tags_downloaded, album, metadata, min_usage, ignore, next, current), priority=True, important=True) @@ -110,7 +110,7 @@ def get_tags(album, metadata, path, min_usage, ignore, next, current): def encode_str(s): # Yes, that's right, Last.fm prefers double URL-encoding s = QtCore.QUrl.toPercentEncoding(s) - s = QtCore.QUrl.toPercentEncoding(unicode(s)) + s = QtCore.QUrl.toPercentEncoding(string_(s)) return s @@ -176,8 +176,8 @@ def save(self): self.config.setting["lastfm_use_track_tags"] = self.ui.use_track_tags.isChecked() self.config.setting["lastfm_use_artist_tags"] = self.ui.use_artist_tags.isChecked() self.config.setting["lastfm_min_tag_usage"] = self.ui.min_tag_usage.value() - self.config.setting["lastfm_ignore_tags"] = unicode(self.ui.ignore_tags.text()) - self.config.setting["lastfm_join_tags"] = unicode(self.ui.join_tags.currentText()) + self.config.setting["lastfm_ignore_tags"] = str(self.ui.ignore_tags.text()) + self.config.setting["lastfm_join_tags"] = str(self.ui.join_tags.currentText()) register_track_metadata_processor(process_track) diff --git a/plugins/lastfm/options_lastfm.ui b/plugins/lastfm/options_lastfm.ui index 91a0aed1..02cbf46d 100644 --- a/plugins/lastfm/options_lastfm.ui +++ b/plugins/lastfm/options_lastfm.ui @@ -1,7 +1,8 @@ - + + LastfmOptionsPage - - + + 0 0 @@ -9,35 +10,53 @@ 317 - - + + + 6 + + 9 - - 6 + + 9 + + + 9 + + + 9 - - + + Last.fm - - + + + 2 + + 9 - - 2 + + 9 + + + 9 + + + 9 - - + + Use track tags - - + + Use artist tags @@ -46,75 +65,89 @@ - - + + Tags - - + + + 2 + + 9 - - 2 + + 9 + + + 9 + + + 9 - - + + Ignore tags: - + - - + + + 6 + + 0 - - 6 + + 0 + + + 0 + + + 0 - - - - 5 - 5 + + + 4 0 - + Join multiple tags with: - - - - 5 - 0 + + + 1 0 - + true - + - + / - + , @@ -123,37 +156,44 @@ - - + + + 6 + + 0 - - 6 + + 0 + + + 0 + + + 0 - - - - 7 - 5 + + + 0 0 - + Minimal tag usage: - + min_tag_usage - - + + % - + 100 @@ -165,10 +205,10 @@ - + Qt::Vertical - + 263 21 diff --git a/plugins/lastfm/ui_options_lastfm.py b/plugins/lastfm/ui_options_lastfm.py index 8bf3a5e6..203418c0 100644 --- a/plugins/lastfm/ui_options_lastfm.py +++ b/plugins/lastfm/ui_options_lastfm.py @@ -1,93 +1,91 @@ # -*- coding: utf-8 -*- -# Automatically generated - don't edit. -# Use `python setup.py build_ui` to update it. +# Form implementation generated from reading ui file 'options_lastfm.ui' +# +# Created by: PyQt5 UI code generator 5.11.2 +# +# WARNING! All changes made in this file will be lost! -from PyQt4 import QtCore, QtGui - -try: - _fromUtf8 = QtCore.QString.fromUtf8 -except AttributeError: - _fromUtf8 = lambda s: s +from PyQt5 import QtCore, QtGui, QtWidgets class Ui_LastfmOptionsPage(object): def setupUi(self, LastfmOptionsPage): - LastfmOptionsPage.setObjectName(_fromUtf8("LastfmOptionsPage")) + LastfmOptionsPage.setObjectName("LastfmOptionsPage") LastfmOptionsPage.resize(305, 317) - self.vboxlayout = QtGui.QVBoxLayout(LastfmOptionsPage) - self.vboxlayout.setMargin(9) + self.vboxlayout = QtWidgets.QVBoxLayout(LastfmOptionsPage) + self.vboxlayout.setContentsMargins(9, 9, 9, 9) self.vboxlayout.setSpacing(6) - self.vboxlayout.setObjectName(_fromUtf8("vboxlayout")) - self.rename_files = QtGui.QGroupBox(LastfmOptionsPage) - self.rename_files.setObjectName(_fromUtf8("rename_files")) - self.vboxlayout1 = QtGui.QVBoxLayout(self.rename_files) - self.vboxlayout1.setMargin(9) + self.vboxlayout.setObjectName("vboxlayout") + self.rename_files = QtWidgets.QGroupBox(LastfmOptionsPage) + self.rename_files.setObjectName("rename_files") + self.vboxlayout1 = QtWidgets.QVBoxLayout(self.rename_files) + self.vboxlayout1.setContentsMargins(9, 9, 9, 9) self.vboxlayout1.setSpacing(2) - self.vboxlayout1.setObjectName(_fromUtf8("vboxlayout1")) - self.use_track_tags = QtGui.QCheckBox(self.rename_files) - self.use_track_tags.setObjectName(_fromUtf8("use_track_tags")) + self.vboxlayout1.setObjectName("vboxlayout1") + self.use_track_tags = QtWidgets.QCheckBox(self.rename_files) + self.use_track_tags.setObjectName("use_track_tags") self.vboxlayout1.addWidget(self.use_track_tags) - self.use_artist_tags = QtGui.QCheckBox(self.rename_files) - self.use_artist_tags.setObjectName(_fromUtf8("use_artist_tags")) + self.use_artist_tags = QtWidgets.QCheckBox(self.rename_files) + self.use_artist_tags.setObjectName("use_artist_tags") self.vboxlayout1.addWidget(self.use_artist_tags) self.vboxlayout.addWidget(self.rename_files) - self.rename_files_2 = QtGui.QGroupBox(LastfmOptionsPage) - self.rename_files_2.setObjectName(_fromUtf8("rename_files_2")) - self.vboxlayout2 = QtGui.QVBoxLayout(self.rename_files_2) - self.vboxlayout2.setMargin(9) + self.rename_files_2 = QtWidgets.QGroupBox(LastfmOptionsPage) + self.rename_files_2.setObjectName("rename_files_2") + self.vboxlayout2 = QtWidgets.QVBoxLayout(self.rename_files_2) + self.vboxlayout2.setContentsMargins(9, 9, 9, 9) self.vboxlayout2.setSpacing(2) - self.vboxlayout2.setObjectName(_fromUtf8("vboxlayout2")) - self.ignore_tags_2 = QtGui.QLabel(self.rename_files_2) - self.ignore_tags_2.setObjectName(_fromUtf8("ignore_tags_2")) + self.vboxlayout2.setObjectName("vboxlayout2") + self.ignore_tags_2 = QtWidgets.QLabel(self.rename_files_2) + self.ignore_tags_2.setObjectName("ignore_tags_2") self.vboxlayout2.addWidget(self.ignore_tags_2) - self.ignore_tags = QtGui.QLineEdit(self.rename_files_2) - self.ignore_tags.setObjectName(_fromUtf8("ignore_tags")) + self.ignore_tags = QtWidgets.QLineEdit(self.rename_files_2) + self.ignore_tags.setObjectName("ignore_tags") self.vboxlayout2.addWidget(self.ignore_tags) - self.hboxlayout = QtGui.QHBoxLayout() - self.hboxlayout.setMargin(0) + self.hboxlayout = QtWidgets.QHBoxLayout() + self.hboxlayout.setContentsMargins(0, 0, 0, 0) self.hboxlayout.setSpacing(6) - self.hboxlayout.setObjectName(_fromUtf8("hboxlayout")) - self.ignore_tags_4 = QtGui.QLabel(self.rename_files_2) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Policy(5), QtGui.QSizePolicy.Policy(5)) + self.hboxlayout.setObjectName("hboxlayout") + self.ignore_tags_4 = QtWidgets.QLabel(self.rename_files_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(4) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.ignore_tags_4.sizePolicy().hasHeightForWidth()) self.ignore_tags_4.setSizePolicy(sizePolicy) - self.ignore_tags_4.setObjectName(_fromUtf8("ignore_tags_4")) + self.ignore_tags_4.setObjectName("ignore_tags_4") self.hboxlayout.addWidget(self.ignore_tags_4) - self.join_tags = QtGui.QComboBox(self.rename_files_2) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Policy(5), QtGui.QSizePolicy.Policy(0)) + self.join_tags = QtWidgets.QComboBox(self.rename_files_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(1) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.join_tags.sizePolicy().hasHeightForWidth()) self.join_tags.setSizePolicy(sizePolicy) self.join_tags.setEditable(True) - self.join_tags.setObjectName(_fromUtf8("join_tags")) - self.join_tags.addItem(_fromUtf8("")) - self.join_tags.setItemText(0, _fromUtf8("")) - self.join_tags.addItem(_fromUtf8("")) - self.join_tags.addItem(_fromUtf8("")) + self.join_tags.setObjectName("join_tags") + self.join_tags.addItem("") + self.join_tags.setItemText(0, "") + self.join_tags.addItem("") + self.join_tags.addItem("") self.hboxlayout.addWidget(self.join_tags) self.vboxlayout2.addLayout(self.hboxlayout) - self.hboxlayout1 = QtGui.QHBoxLayout() - self.hboxlayout1.setMargin(0) + self.hboxlayout1 = QtWidgets.QHBoxLayout() + self.hboxlayout1.setContentsMargins(0, 0, 0, 0) self.hboxlayout1.setSpacing(6) - self.hboxlayout1.setObjectName(_fromUtf8("hboxlayout1")) - self.label_4 = QtGui.QLabel(self.rename_files_2) - sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Policy(7), QtGui.QSizePolicy.Policy(5)) + self.hboxlayout1.setObjectName("hboxlayout1") + self.label_4 = QtWidgets.QLabel(self.rename_files_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.label_4.sizePolicy().hasHeightForWidth()) self.label_4.setSizePolicy(sizePolicy) - self.label_4.setObjectName(_fromUtf8("label_4")) + self.label_4.setObjectName("label_4") self.hboxlayout1.addWidget(self.label_4) - self.min_tag_usage = QtGui.QSpinBox(self.rename_files_2) + self.min_tag_usage = QtWidgets.QSpinBox(self.rename_files_2) self.min_tag_usage.setMaximum(100) - self.min_tag_usage.setObjectName(_fromUtf8("min_tag_usage")) + self.min_tag_usage.setObjectName("min_tag_usage") self.hboxlayout1.addWidget(self.min_tag_usage) self.vboxlayout2.addLayout(self.hboxlayout1) self.vboxlayout.addWidget(self.rename_files_2) - spacerItem = QtGui.QSpacerItem(263, 21, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + spacerItem = QtWidgets.QSpacerItem(263, 21, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.vboxlayout.addItem(spacerItem) self.label_4.setBuddy(self.min_tag_usage) @@ -96,14 +94,24 @@ def setupUi(self, LastfmOptionsPage): LastfmOptionsPage.setTabOrder(self.use_track_tags, self.ignore_tags) def retranslateUi(self, LastfmOptionsPage): - self.rename_files.setTitle(_("Last.fm")) - self.use_track_tags.setText(_("Use track tags")) - self.use_artist_tags.setText(_("Use artist tags")) - self.rename_files_2.setTitle(_("Tags")) - self.ignore_tags_2.setText(_("Ignore tags:")) - self.ignore_tags_4.setText(_("Join multiple tags with:")) - self.join_tags.setItemText(1, _(" / ")) - self.join_tags.setItemText(2, _(", ")) - self.label_4.setText(_("Minimal tag usage:")) - self.min_tag_usage.setSuffix(_(" %")) + _translate = QtCore.QCoreApplication.translate + self.rename_files.setTitle(_translate("LastfmOptionsPage", "Last.fm")) + self.use_track_tags.setText(_translate("LastfmOptionsPage", "Use track tags")) + self.use_artist_tags.setText(_translate("LastfmOptionsPage", "Use artist tags")) + self.rename_files_2.setTitle(_translate("LastfmOptionsPage", "Tags")) + self.ignore_tags_2.setText(_translate("LastfmOptionsPage", "Ignore tags:")) + self.ignore_tags_4.setText(_translate("LastfmOptionsPage", "Join multiple tags with:")) + self.join_tags.setItemText(1, _translate("LastfmOptionsPage", " / ")) + self.join_tags.setItemText(2, _translate("LastfmOptionsPage", ", ")) + self.label_4.setText(_translate("LastfmOptionsPage", "Minimal tag usage:")) + self.min_tag_usage.setSuffix(_translate("LastfmOptionsPage", " %")) + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + LastfmOptionsPage = QtWidgets.QWidget() + ui = Ui_LastfmOptionsPage() + ui.setupUi(LastfmOptionsPage) + LastfmOptionsPage.show() + sys.exit(app.exec_()) From 91efd4da938ff5dd3efe07650cc543489e578fae Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 4 Sep 2018 22:06:24 +0200 Subject: [PATCH 002/123] lastfm: Port to Last.fm API 2.0 --- plugins/lastfm/__init__.py | 56 ++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 20 deletions(-) diff --git a/plugins/lastfm/__init__.py b/plugins/lastfm/__init__.py index f7dc6cf3..679fde18 100644 --- a/plugins/lastfm/__init__.py +++ b/plugins/lastfm/__init__.py @@ -9,20 +9,24 @@ import traceback from functools import partial from PyQt5 import QtCore -from picard.metadata import register_track_metadata_processor -from picard.ui.options import register_options_page, OptionsPage from picard.config import BoolOption, IntOption, TextOption +from picard.metadata import register_track_metadata_processor from picard.plugins.lastfm.ui_options_lastfm import Ui_LastfmOptionsPage +from picard.ui.options import register_options_page, OptionsPage +from picard.util import build_qurl -LASTFM_HOST = "ws.audioscrobbler.com" +LASTFM_HOST = 'ws.audioscrobbler.com' LASTFM_PORT = 80 +LASTFM_PATH = '/2.0/' +LASTFM_API_KEY = '0a210a4a6741f2ec8f27a791b9d5d971' -# From http://www.last.fm/api/tos, 2011-07-30 +# From https://www.last.fm/api/tos, 2018-09-04 # 4.4 (...) You will not make more than 5 requests per originating IP address per second, averaged over a # 5 minute period, without prior written consent. (...) from picard.webservice import ratecontrol ratecontrol.set_minimum_delay((LASTFM_HOST, LASTFM_PORT), 200) + # Cache for Tags to avoid re-requesting tags within same Picard session _cache = {} # Keeps track of requests for tags made to webservice API but not yet returned (to avoid re-requesting the same URIs) @@ -52,7 +56,7 @@ def _tags_finalize(album, metadata, tags, next): def _tags_downloaded(album, metadata, min_usage, ignore, next, current, data, reply, error): try: try: - intags = data.toptags[0].tag + intags = data.lfm[0].toptags[0].tag except AttributeError: intags = [] tags = [] @@ -70,7 +74,7 @@ def _tags_downloaded(album, metadata, min_usage, ignore, next, current, data, re pass if name.lower() not in ignore: tags.append(name.title()) - url = str(reply.url().path()) + url = reply.url().toString() _cache[url] = tags _tags_finalize(album, metadata, current + tags, next) @@ -89,41 +93,53 @@ def _tags_downloaded(album, metadata, min_usage, ignore, next, current, data, re album._finalize_loading(None) -def get_tags(album, metadata, path, min_usage, ignore, next, current): +def get_tags(album, metadata, queryargs, min_usage, ignore, next, current): """Get tags from an URL.""" - url = str(QtCore.QUrl.fromPercentEncoding(path.encode('utf-8'))) + url = build_qurl(LASTFM_HOST, LASTFM_PORT, LASTFM_PATH, queryargs).toString() if url in _cache: _tags_finalize(album, metadata, current + _cache[url], next) else: - # If we have already sent a request for this URL, delay this call until later if url in _pending_requests: - _pending_requests[url].append(partial(get_tags, album, metadata, path, min_usage, ignore, next, current)) + _pending_requests[url].append(partial(get_tags, album, metadata, + queryargs, min_usage, ignore, next, current)) else: _pending_requests[url] = [] album._requests += 1 - album.tagger.webservice.get(LASTFM_HOST, LASTFM_PORT, path, - partial(_tags_downloaded, album, metadata, min_usage, ignore, next, current), - priority=True, important=True) + album.tagger.webservice.get(LASTFM_HOST, LASTFM_PORT, LASTFM_PATH, + partial(_tags_downloaded, album, metadata, min_usage, ignore, next, current), + queryargs=queryargs, parse_response_type='xml', + priority=True, important=True) def encode_str(s): - # Yes, that's right, Last.fm prefers double URL-encoding s = QtCore.QUrl.toPercentEncoding(s) - s = QtCore.QUrl.toPercentEncoding(string_(s)) - return s + return bytes(s).decode() + + +def get_queryargs(queryargs): + queryargs = {k: encode_str(v) for (k, v) in queryargs.items()} + queryargs['api_key'] = LASTFM_API_KEY + return queryargs def get_track_tags(album, metadata, artist, track, min_usage, ignore, next, current): """Get track top tags.""" - path = "/1.0/track/%s/%s/toptags.xml" % (encode_str(artist), encode_str(track)) - get_tags(album, metadata, path, min_usage, ignore, next, current) + queryargs = get_queryargs({ + 'method': 'Track.getTopTags', + 'artist': artist, + 'track': track, + }) + get_tags(album, metadata, queryargs, min_usage, ignore, next, current) def get_artist_tags(album, metadata, artist, min_usage, ignore, next, current): """Get artist top tags.""" - path = "/1.0/artist/%s/toptags.xml" % (encode_str(artist),) - get_tags(album, metadata, path, min_usage, ignore, next, current) + queryargs = get_queryargs({ + 'method': 'Artist.getTopTags', + 'artist': artist, + }) + get_tags(album, metadata, queryargs, min_usage, ignore, next, current) def process_track(album, metadata, release, track): From 286d9a657bb6230de900bab7f904327017d59e3b Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 4 Sep 2018 22:31:58 +0200 Subject: [PATCH 003/123] norelease: Port to API 2.0 --- plugins/no_release/no_release.py | 35 ++++++++++++++++---------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/plugins/no_release/no_release.py b/plugins/no_release/no_release.py index 3af0ba8a..538b00cc 100644 --- a/plugins/no_release/no_release.py +++ b/plugins/no_release/no_release.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- PLUGIN_NAME = 'No release' -PLUGIN_AUTHOR = 'Johannes Weißl' +PLUGIN_AUTHOR = 'Johannes Weißl, Philipp Wolfer' PLUGIN_DESCRIPTION = '''Do not store specific release information in releases of unknown origin.''' -PLUGIN_VERSION = '0.1' -PLUGIN_API_VERSIONS = ['0.15'] +PLUGIN_VERSION = '0.2' +PLUGIN_API_VERSIONS = ['2.0'] -from PyQt4 import QtCore, QtGui +from PyQt5 import QtCore, QtGui, QtWidgets from picard.album import Album from picard.metadata import register_album_metadata_processor, register_track_metadata_processor @@ -20,43 +20,42 @@ class Ui_NoReleaseOptionsPage(object): def setupUi(self, NoReleaseOptionsPage): NoReleaseOptionsPage.setObjectName('NoReleaseOptionsPage') NoReleaseOptionsPage.resize(394, 300) - self.verticalLayout = QtGui.QVBoxLayout(NoReleaseOptionsPage) + self.verticalLayout = QtWidgets.QVBoxLayout(NoReleaseOptionsPage) self.verticalLayout.setObjectName('verticalLayout') - self.groupBox = QtGui.QGroupBox(NoReleaseOptionsPage) + self.groupBox = QtWidgets.QGroupBox(NoReleaseOptionsPage) self.groupBox.setObjectName('groupBox') - self.vboxlayout = QtGui.QVBoxLayout(self.groupBox) + self.vboxlayout = QtWidgets.QVBoxLayout(self.groupBox) self.vboxlayout.setObjectName('vboxlayout') - self.norelease_enable = QtGui.QCheckBox(self.groupBox) + self.norelease_enable = QtWidgets.QCheckBox(self.groupBox) self.norelease_enable.setObjectName('norelease_enable') self.vboxlayout.addWidget(self.norelease_enable) - self.label = QtGui.QLabel(self.groupBox) + self.label = QtWidgets.QLabel(self.groupBox) self.label.setObjectName('label') self.vboxlayout.addWidget(self.label) - self.horizontalLayout = QtGui.QHBoxLayout() + self.horizontalLayout = QtWidgets.QHBoxLayout() self.horizontalLayout.setObjectName('horizontalLayout') - self.norelease_strip_tags = QtGui.QLineEdit(self.groupBox) + self.norelease_strip_tags = QtWidgets.QLineEdit(self.groupBox) self.norelease_strip_tags.setObjectName('norelease_strip_tags') self.horizontalLayout.addWidget(self.norelease_strip_tags) self.vboxlayout.addLayout(self.horizontalLayout) self.verticalLayout.addWidget(self.groupBox) - spacerItem = QtGui.QSpacerItem(368, 187, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + spacerItem = QtWidgets.QSpacerItem(368, 187, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.verticalLayout.addItem(spacerItem) self.retranslateUi(NoReleaseOptionsPage) QtCore.QMetaObject.connectSlotsByName(NoReleaseOptionsPage) def retranslateUi(self, NoReleaseOptionsPage): - self.groupBox.setTitle(QtGui.QApplication.translate('NoReleaseOptionsPage', 'No release', None, QtGui.QApplication.UnicodeUTF8)) - self.norelease_enable.setText(QtGui.QApplication.translate('NoReleaseOptionsPage', _('Enable plugin for all releases by default'), None, QtGui.QApplication.UnicodeUTF8)) - self.label.setText(QtGui.QApplication.translate('NoReleaseOptionsPage', _('Tags to strip (comma-separated)'), None, QtGui.QApplication.UnicodeUTF8)) + self.groupBox.setTitle(QtWidgets.QApplication.translate('NoReleaseOptionsPage', 'No release')) + self.norelease_enable.setText(QtWidgets.QApplication.translate('NoReleaseOptionsPage', _('Enable plugin for all releases by default'))) + self.label.setText(QtWidgets.QApplication.translate('NoReleaseOptionsPage', _('Tags to strip (comma-separated)'))) def strip_release_specific_metadata(tagger, metadata): strip_tags = tagger.config.setting['norelease_strip_tags'] strip_tags = [tag.strip() for tag in strip_tags.split(',')] for tag in strip_tags: - if tag in metadata: - del metadata[tag] + metadata.delete(tag) class NoReleaseAction(BaseAction): @@ -93,7 +92,7 @@ def load(self): self.ui.norelease_enable.setChecked(self.config.setting['norelease_enable']) def save(self): - self.config.setting['norelease_strip_tags'] = unicode(self.ui.norelease_strip_tags.text()) + self.config.setting['norelease_strip_tags'] = str(self.ui.norelease_strip_tags.text()) self.config.setting['norelease_enable'] = self.ui.norelease_enable.isChecked() From 02c3481664f892ebf9f9f66c776dd2f4ef0708d2 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Wed, 5 Sep 2018 17:36:49 +0200 Subject: [PATCH 004/123] lastfm: Style and readability fixes --- plugins/lastfm/__init__.py | 96 +++++++++++++++++++++----------------- 1 file changed, 54 insertions(+), 42 deletions(-) diff --git a/plugins/lastfm/__init__.py b/plugins/lastfm/__init__.py index 679fde18..03591a55 100644 --- a/plugins/lastfm/__init__.py +++ b/plugins/lastfm/__init__.py @@ -6,7 +6,6 @@ PLUGIN_VERSION = "0.6" PLUGIN_API_VERSIONS = ["2.0"] -import traceback from functools import partial from PyQt5 import QtCore from picard.config import BoolOption, IntOption, TextOption @@ -14,6 +13,7 @@ from picard.plugins.lastfm.ui_options_lastfm import Ui_LastfmOptionsPage from picard.ui.options import register_options_page, OptionsPage from picard.util import build_qurl +from picard.webservice import ratecontrol LASTFM_HOST = 'ws.audioscrobbler.com' LASTFM_PORT = 80 @@ -21,15 +21,15 @@ LASTFM_API_KEY = '0a210a4a6741f2ec8f27a791b9d5d971' # From https://www.last.fm/api/tos, 2018-09-04 -# 4.4 (...) You will not make more than 5 requests per originating IP address per second, averaged over a -# 5 minute period, without prior written consent. (...) -from picard.webservice import ratecontrol +# 4.4 […] You will not make more than 5 requests per originating IP address per +# second, averaged over a 5 minute period, without prior written consent. […] ratecontrol.set_minimum_delay((LASTFM_HOST, LASTFM_PORT), 200) - # Cache for Tags to avoid re-requesting tags within same Picard session _cache = {} -# Keeps track of requests for tags made to webservice API but not yet returned (to avoid re-requesting the same URIs) + +# Keeps track of requests for tags made to webservice API but not yet returned +# (to avoid re-requesting the same URIs) _pending_requests = {} # TODO: move this to an options page @@ -41,9 +41,9 @@ TITLE_CASE = True -def _tags_finalize(album, metadata, tags, next): - if next: - next(tags) +def _tags_finalize(album, metadata, tags, next_): + if next_: + next_(tags) else: tags = list(set(tags)) if tags: @@ -53,7 +53,8 @@ def _tags_finalize(album, metadata, tags, next): metadata["genre"] = tags -def _tags_downloaded(album, metadata, min_usage, ignore, next, current, data, reply, error): +def _tags_downloaded(album, metadata, min_usage, ignore, next_, current, data, + reply, error): try: try: intags = data.lfm[0].toptags[0].tag @@ -76,7 +77,7 @@ def _tags_downloaded(album, metadata, min_usage, ignore, next, current, data, re tags.append(name.title()) url = reply.url().toString() _cache[url] = tags - _tags_finalize(album, metadata, current + tags, next) + _tags_finalize(album, metadata, current + tags, next_) # Process any pending requests for the same URL if url in _pending_requests: @@ -85,29 +86,33 @@ def _tags_downloaded(album, metadata, min_usage, ignore, next, current, data, re for delayed_call in pending: delayed_call() - except: - album.tagger.log.error("Problem processing downloaded tags in last.fm plugin: %s", traceback.format_exc()) + except Exception as err: + album.tagger.log.error(err, exc_info=True) raise finally: album._requests -= 1 album._finalize_loading(None) -def get_tags(album, metadata, queryargs, min_usage, ignore, next, current): +def get_tags(album, metadata, queryargs, min_usage, ignore, next_, current): """Get tags from an URL.""" - url = build_qurl(LASTFM_HOST, LASTFM_PORT, LASTFM_PATH, queryargs).toString() + url = build_qurl( + LASTFM_HOST, LASTFM_PORT, LASTFM_PATH, queryargs).toString() if url in _cache: - _tags_finalize(album, metadata, current + _cache[url], next) + _tags_finalize(album, metadata, current + _cache[url], next_) else: - # If we have already sent a request for this URL, delay this call until later + # If we have already sent a request for this URL, delay this call if url in _pending_requests: - _pending_requests[url].append(partial(get_tags, album, metadata, - queryargs, min_usage, ignore, next, current)) + _pending_requests[url].append( + partial(get_tags, album, metadata, queryargs, min_usage, + ignore, next_, current)) else: _pending_requests[url] = [] album._requests += 1 - album.tagger.webservice.get(LASTFM_HOST, LASTFM_PORT, LASTFM_PATH, - partial(_tags_downloaded, album, metadata, min_usage, ignore, next, current), + album.tagger.webservice.get( + LASTFM_HOST, LASTFM_PORT, LASTFM_PATH, + partial(_tags_downloaded, album, metadata, min_usage, ignore, + next_, current), queryargs=queryargs, parse_response_type='xml', priority=True, important=True) @@ -123,41 +128,46 @@ def get_queryargs(queryargs): return queryargs -def get_track_tags(album, metadata, artist, track, min_usage, ignore, next, current): +def get_track_tags(album, metadata, artist, track, min_usage, + ignore, next_, current): """Get track top tags.""" queryargs = get_queryargs({ 'method': 'Track.getTopTags', 'artist': artist, 'track': track, }) - get_tags(album, metadata, queryargs, min_usage, ignore, next, current) + get_tags(album, metadata, queryargs, min_usage, ignore, next_, current) -def get_artist_tags(album, metadata, artist, min_usage, ignore, next, current): +def get_artist_tags(album, metadata, artist, min_usage, + ignore, next_, current): """Get artist top tags.""" queryargs = get_queryargs({ 'method': 'Artist.getTopTags', 'artist': artist, }) - get_tags(album, metadata, queryargs, min_usage, ignore, next, current) + get_tags(album, metadata, queryargs, min_usage, ignore, next_, current) def process_track(album, metadata, release, track): - tagger = album.tagger - use_track_tags = tagger.config.setting["lastfm_use_track_tags"] - use_artist_tags = tagger.config.setting["lastfm_use_artist_tags"] - min_tag_usage = tagger.config.setting["lastfm_min_tag_usage"] - ignore_tags = tagger.config.setting["lastfm_ignore_tags"].lower().split(",") + setting = album.tagger.config.setting + use_track_tags = setting["lastfm_use_track_tags"] + use_artist_tags = setting["lastfm_use_artist_tags"] + min_tag_usage = setting["lastfm_min_tag_usage"] + ignore_tags = setting["lastfm_ignore_tags"].lower().split(",") if use_track_tags or use_artist_tags: artist = metadata["artist"] title = metadata["title"] if artist: if use_artist_tags: - get_artist_tags_func = partial(get_artist_tags, album, metadata, artist, min_tag_usage, ignore_tags, None) + get_artist_tags_func = partial(get_artist_tags, album, + metadata, artist, min_tag_usage, + ignore_tags, None) else: get_artist_tags_func = None if title and use_track_tags: - get_track_tags(album, metadata, artist, title, min_tag_usage, ignore_tags, get_artist_tags_func, []) + get_track_tags(album, metadata, artist, title, min_tag_usage, + ignore_tags, get_artist_tags_func, []) elif get_artist_tags_func: get_artist_tags_func([]) @@ -182,18 +192,20 @@ def __init__(self, parent=None): self.ui.setupUi(self) def load(self): - self.ui.use_track_tags.setChecked(self.config.setting["lastfm_use_track_tags"]) - self.ui.use_artist_tags.setChecked(self.config.setting["lastfm_use_artist_tags"]) - self.ui.min_tag_usage.setValue(self.config.setting["lastfm_min_tag_usage"]) - self.ui.ignore_tags.setText(self.config.setting["lastfm_ignore_tags"]) - self.ui.join_tags.setEditText(self.config.setting["lastfm_join_tags"]) + setting = self.config.setting + self.ui.use_track_tags.setChecked(setting["lastfm_use_track_tags"]) + self.ui.use_artist_tags.setChecked(setting["lastfm_use_artist_tags"]) + self.ui.min_tag_usage.setValue(setting["lastfm_min_tag_usage"]) + self.ui.ignore_tags.setText(setting["lastfm_ignore_tags"]) + self.ui.join_tags.setEditText(setting["lastfm_join_tags"]) def save(self): - self.config.setting["lastfm_use_track_tags"] = self.ui.use_track_tags.isChecked() - self.config.setting["lastfm_use_artist_tags"] = self.ui.use_artist_tags.isChecked() - self.config.setting["lastfm_min_tag_usage"] = self.ui.min_tag_usage.value() - self.config.setting["lastfm_ignore_tags"] = str(self.ui.ignore_tags.text()) - self.config.setting["lastfm_join_tags"] = str(self.ui.join_tags.currentText()) + setting = self.config.setting + setting["lastfm_use_track_tags"] = self.ui.use_track_tags.isChecked() + setting["lastfm_use_artist_tags"] = self.ui.use_artist_tags.isChecked() + setting["lastfm_min_tag_usage"] = self.ui.min_tag_usage.value() + setting["lastfm_ignore_tags"] = str(self.ui.ignore_tags.text()) + setting["lastfm_join_tags"] = str(self.ui.join_tags.currentText()) register_track_metadata_processor(process_track) From 81bc1d213c8b8ff9e9a545839f5de69311de8d3e Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Wed, 5 Sep 2018 18:06:47 +0200 Subject: [PATCH 005/123] lastfm: allow regular expressions for ignored tags --- plugins/lastfm/__init__.py | 39 +++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/plugins/lastfm/__init__.py b/plugins/lastfm/__init__.py index 03591a55..66e0a1a0 100644 --- a/plugins/lastfm/__init__.py +++ b/plugins/lastfm/__init__.py @@ -6,8 +6,10 @@ PLUGIN_VERSION = "0.6" PLUGIN_API_VERSIONS = ["2.0"] +import re from functools import partial from PyQt5 import QtCore +from picard import log from picard.config import BoolOption, IntOption, TextOption from picard.metadata import register_track_metadata_processor from picard.plugins.lastfm.ui_options_lastfm import Ui_LastfmOptionsPage @@ -41,6 +43,32 @@ TITLE_CASE = True +def parse_ignored_tags(ignore_tags_setting): + ignore_tags = [] + for tag in ignore_tags_setting.lower().split(','): + tag = tag.strip() + if tag.startswith('/') and tag.endswith('/'): + try: + tag = re.compile(tag[1:-1]) + except re.error: + log.error( + 'Error parsing ignored tag "%s"', tag, exc_info=True) + ignore_tags.append(tag) + return ignore_tags + + +def matches_ignored(ignore_tags, tag): + tag = tag.lower().strip() + for pattern in ignore_tags: + if isinstance(pattern, re.Pattern): + match = pattern.match(tag) + else: + match = pattern == tag + if match: + return True + return False + + def _tags_finalize(album, metadata, tags, next_): if next_: next_(tags) @@ -73,7 +101,7 @@ def _tags_downloaded(album, metadata, min_usage, ignore, next_, current, data, name = TRANSLATE_TAGS[name] except KeyError: pass - if name.lower() not in ignore: + if not matches_ignored(ignore, name): tags.append(name.title()) url = reply.url().toString() _cache[url] = tags @@ -86,8 +114,8 @@ def _tags_downloaded(album, metadata, min_usage, ignore, next_, current, data, for delayed_call in pending: delayed_call() - except Exception as err: - album.tagger.log.error(err, exc_info=True) + except Exception: + log.error('Problem processing download tags', exc_info=True) raise finally: album._requests -= 1 @@ -154,7 +182,7 @@ def process_track(album, metadata, release, track): use_track_tags = setting["lastfm_use_track_tags"] use_artist_tags = setting["lastfm_use_artist_tags"] min_tag_usage = setting["lastfm_min_tag_usage"] - ignore_tags = setting["lastfm_ignore_tags"].lower().split(",") + ignore_tags = parse_ignored_tags(setting["lastfm_ignore_tags"]) if use_track_tags or use_artist_tags: artist = metadata["artist"] title = metadata["title"] @@ -182,7 +210,8 @@ class LastfmOptionsPage(OptionsPage): BoolOption("setting", "lastfm_use_track_tags", False), BoolOption("setting", "lastfm_use_artist_tags", False), IntOption("setting", "lastfm_min_tag_usage", 15), - TextOption("setting", "lastfm_ignore_tags", "seen live,favorites"), + TextOption("setting", "lastfm_ignore_tags", + "seen live, favorites, /\\d+ of \\d+ stars/"), TextOption("setting", "lastfm_join_tags", ""), ] From dbf9ebf4455ef241ca054f037eca2feb7f5fd67a Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 6 Sep 2018 09:12:07 +0200 Subject: [PATCH 006/123] lastfm: Explicitly import picard.log --- plugins/lastfm/__init__.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/plugins/lastfm/__init__.py b/plugins/lastfm/__init__.py index 66e0a1a0..2e279328 100644 --- a/plugins/lastfm/__init__.py +++ b/plugins/lastfm/__init__.py @@ -9,7 +9,7 @@ import re from functools import partial from PyQt5 import QtCore -from picard import log +from picard import config, log from picard.config import BoolOption, IntOption, TextOption from picard.metadata import register_track_metadata_processor from picard.plugins.lastfm.ui_options_lastfm import Ui_LastfmOptionsPage @@ -75,7 +75,7 @@ def _tags_finalize(album, metadata, tags, next_): else: tags = list(set(tags)) if tags: - join_tags = album.tagger.config.setting["lastfm_join_tags"] + join_tags = config.setting["lastfm_join_tags"] if join_tags: tags = join_tags.join(tags) metadata["genre"] = tags @@ -178,11 +178,10 @@ def get_artist_tags(album, metadata, artist, min_usage, def process_track(album, metadata, release, track): - setting = album.tagger.config.setting - use_track_tags = setting["lastfm_use_track_tags"] - use_artist_tags = setting["lastfm_use_artist_tags"] - min_tag_usage = setting["lastfm_min_tag_usage"] - ignore_tags = parse_ignored_tags(setting["lastfm_ignore_tags"]) + use_track_tags = config.setting["lastfm_use_track_tags"] + use_artist_tags = config.setting["lastfm_use_artist_tags"] + min_tag_usage = config.setting["lastfm_min_tag_usage"] + ignore_tags = parse_ignored_tags(config.setting["lastfm_ignore_tags"]) if use_track_tags or use_artist_tags: artist = metadata["artist"] title = metadata["title"] @@ -221,7 +220,7 @@ def __init__(self, parent=None): self.ui.setupUi(self) def load(self): - setting = self.config.setting + setting = config.setting self.ui.use_track_tags.setChecked(setting["lastfm_use_track_tags"]) self.ui.use_artist_tags.setChecked(setting["lastfm_use_artist_tags"]) self.ui.min_tag_usage.setValue(setting["lastfm_min_tag_usage"]) @@ -229,7 +228,7 @@ def load(self): self.ui.join_tags.setEditText(setting["lastfm_join_tags"]) def save(self): - setting = self.config.setting + setting = config.setting setting["lastfm_use_track_tags"] = self.ui.use_track_tags.isChecked() setting["lastfm_use_artist_tags"] = self.ui.use_artist_tags.isChecked() setting["lastfm_min_tag_usage"] = self.ui.min_tag_usage.value() From 4ebf287951fde458b3ea069bf5bdfd051fbf5856 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 6 Sep 2018 09:54:22 +0200 Subject: [PATCH 007/123] norelease: Explicitly import picard.config --- plugins/no_release/no_release.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/plugins/no_release/no_release.py b/plugins/no_release/no_release.py index 538b00cc..929239c8 100644 --- a/plugins/no_release/no_release.py +++ b/plugins/no_release/no_release.py @@ -8,6 +8,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets +from picard import config from picard.album import Album from picard.metadata import register_album_metadata_processor, register_track_metadata_processor from picard.ui.options import register_options_page, OptionsPage @@ -51,8 +52,8 @@ def retranslateUi(self, NoReleaseOptionsPage): self.label.setText(QtWidgets.QApplication.translate('NoReleaseOptionsPage', _('Tags to strip (comma-separated)'))) -def strip_release_specific_metadata(tagger, metadata): - strip_tags = tagger.config.setting['norelease_strip_tags'] +def strip_release_specific_metadata(metadata): + strip_tags = config.setting['norelease_strip_tags'] strip_tags = [tag.strip() for tag in strip_tags.split(',')] for tag in strip_tags: metadata.delete(tag) @@ -64,9 +65,9 @@ class NoReleaseAction(BaseAction): def callback(self, objs): for album in objs: if isinstance(album, Album): - strip_release_specific_metadata(self.tagger, album.metadata) + strip_release_specific_metadata(album.metadata) for track in album.tracks: - strip_release_specific_metadata(self.tagger, track.metadata) + strip_release_specific_metadata(track.metadata) for file in track.linked_files: track.update_file_metadata(file) album.update() @@ -88,22 +89,23 @@ def __init__(self, parent=None): self.ui.setupUi(self) def load(self): - self.ui.norelease_strip_tags.setText(self.config.setting['norelease_strip_tags']) - self.ui.norelease_enable.setChecked(self.config.setting['norelease_enable']) + self.ui.norelease_strip_tags.setText(config.setting['norelease_strip_tags']) + self.ui.norelease_enable.setChecked(config.setting['norelease_enable']) def save(self): - self.config.setting['norelease_strip_tags'] = str(self.ui.norelease_strip_tags.text()) - self.config.setting['norelease_enable'] = self.ui.norelease_enable.isChecked() + config.setting['norelease_strip_tags'] = str(self.ui.norelease_strip_tags.text()) + config.setting['norelease_enable'] = self.ui.norelease_enable.isChecked() def NoReleaseAlbumProcessor(tagger, metadata, release): - if tagger.config.setting['norelease_enable']: - strip_release_specific_metadata(tagger, metadata) + if config.setting['norelease_enable']: + strip_release_specific_metadata(metadata) def NoReleaseTrackProcessor(tagger, metadata, track, release): - if tagger.config.setting['norelease_enable']: - strip_release_specific_metadata(tagger, metadata) + if config.setting['norelease_enable']: + strip_release_specific_metadata(metadata) + register_album_metadata_processor(NoReleaseAlbumProcessor) register_track_metadata_processor(NoReleaseTrackProcessor) From 288eb8836c8f8012373df8511cda7fa255de7c75 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 6 Sep 2018 23:09:06 +0200 Subject: [PATCH 008/123] fanarttv: Fixed parameter encoding --- plugins/fanarttv/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/fanarttv/__init__.py b/plugins/fanarttv/__init__.py index 96f3815f..f08129bb 100644 --- a/plugins/fanarttv/__init__.py +++ b/plugins/fanarttv/__init__.py @@ -20,7 +20,7 @@ PLUGIN_NAME = 'fanart.tv cover art' PLUGIN_AUTHOR = 'Philipp Wolfer, Sambhav Kothari' PLUGIN_DESCRIPTION = 'Use cover art from fanart.tv. To use this plugin you have to register a personal API key on https://fanart.tv/get-an-api-key/' -PLUGIN_VERSION = "1.2" +PLUGIN_VERSION = "1.3" PLUGIN_API_VERSIONS = ["2.0"] PLUGIN_LICENSE = "GPL-2.0" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" @@ -75,11 +75,11 @@ def enabled(self): def queue_images(self): release_group_id = self.metadata["musicbrainz_releasegroupid"] - path = "/v3/music/albums/%s" % \ - (release_group_id, ) - queryargs = {"api_key": QUrl.toPercentEncoding(FANART_APIKEY), - "client_key": QUrl.toPercentEncoding(self._client_key), - } + path = "/v3/music/albums/%s" % (release_group_id, ) + queryargs = { + "api_key": bytes(QUrl.toPercentEncoding(FANART_APIKEY)).decode(), + "client_key": bytes(QUrl.toPercentEncoding(self._client_key)).decode(), + } log.debug("CoverArtProviderFanartTv.queue_downloads: %s" % path) self.album.tagger.webservice.download( FANART_HOST, @@ -162,7 +162,7 @@ def load(self): self.ui.fanarttv_cdart_use_if_no_albumcover.setChecked(True) def save(self): - config.setting["fanarttv_client_key"] = string_(self.ui.fanarttv_client_key.text()) + config.setting["fanarttv_client_key"] = self.ui.fanarttv_client_key.text() if self.ui.fanarttv_cdart_use_always.isChecked(): config.setting["fanarttv_use_cdart"] = OPTION_CDART_ALWAYS elif self.ui.fanarttv_cdart_use_never.isChecked(): From 9614d9f6290110524a46db93c4b29efabdac542a Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 7 Sep 2018 11:50:13 +0200 Subject: [PATCH 009/123] fanarttv: Don't use explicit load_json() --- plugins/fanarttv/__init__.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/plugins/fanarttv/__init__.py b/plugins/fanarttv/__init__.py index f08129bb..67633e33 100644 --- a/plugins/fanarttv/__init__.py +++ b/plugins/fanarttv/__init__.py @@ -20,12 +20,11 @@ PLUGIN_NAME = 'fanart.tv cover art' PLUGIN_AUTHOR = 'Philipp Wolfer, Sambhav Kothari' PLUGIN_DESCRIPTION = 'Use cover art from fanart.tv. To use this plugin you have to register a personal API key on https://fanart.tv/get-an-api-key/' -PLUGIN_VERSION = "1.3" +PLUGIN_VERSION = "1.4" PLUGIN_API_VERSIONS = ["2.0"] PLUGIN_LICENSE = "GPL-2.0" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" -import traceback from functools import partial from PyQt5.QtCore import QUrl from PyQt5.QtNetwork import QNetworkReply @@ -33,7 +32,6 @@ from picard.coverart.providers import CoverArtProvider, register_cover_art_provider from picard.coverart.image import CoverArtImage from picard.ui.options import register_options_page, OptionsPage -from picard.util import load_json from picard.config import TextOption from picard.plugins.fanarttv.ui_options_fanarttv import Ui_FanartTvOptionsPage @@ -81,13 +79,14 @@ def queue_images(self): "client_key": bytes(QUrl.toPercentEncoding(self._client_key)).decode(), } log.debug("CoverArtProviderFanartTv.queue_downloads: %s" % path) - self.album.tagger.webservice.download( + self.album.tagger.webservice.get( FANART_HOST, FANART_PORT, path, partial(self._json_downloaded, release_group_id), priority=True, important=False, + parse_response_type='json', queryargs=queryargs) self.album._requests += 1 return CoverArtProvider.WAIT @@ -107,8 +106,7 @@ def _json_downloaded(self, release_group_id, data, reply, error): error_level("Problem requesting metadata in fanart.tv plugin: %s", error) else: try: - response = load_json(data) - release = response["albums"][release_group_id] + release = data["albums"][release_group_id] if "albumcover" in release: covers = release["albumcover"] @@ -124,8 +122,8 @@ def _json_downloaded(self, release_group_id, data, reply, error): if not "albumcover" in release: types.append("front") self._select_and_add_cover_art(covers, types) - except: - log.error("Problem processing downloaded metadata in fanart.tv plugin: %s", traceback.format_exc()) + except (AttributeError, KeyError, TypeError): + log.error("Problem processing downloaded metadata in fanart.tv plugin: %s", exc_info=True) self.next_in_queue() From 3c24df9a15f2b4f7e628b9249f20846b8f7eef38 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 6 Sep 2018 14:01:03 +0200 Subject: [PATCH 010/123] bpm: propagate tag changes --- plugins/bpm/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/bpm/__init__.py b/plugins/bpm/__init__.py index c4529182..799d479f 100644 --- a/plugins/bpm/__init__.py +++ b/plugins/bpm/__init__.py @@ -12,7 +12,7 @@ PLUGIN_DESCRIPTION = """Calculate BPM for selected files and albums. Linux only version with dependancy on Aubio and Numpy""" PLUGIN_LICENSE = "GPL-2.0" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" -PLUGIN_VERSION = "1.1" +PLUGIN_VERSION = "1.2" PLUGIN_API_VERSIONS = ["2.0"] # PLUGIN_INCOMPATIBLE_PLATFORMS = [ # 'win32', 'cygwyn', 'darwin', 'os2', 'os2emx', 'riscos', 'atheos'] @@ -96,7 +96,8 @@ def _calculate_bpm(self, file): ) calculated_bpm = get_file_bpm(self.tagger, file.filename) # self.tagger.log.debug('%s' % (calculated_bpm)) - file.metadata["bpm"] = string_(round(calculated_bpm, 1)) + file.metadata["bpm"] = str(round(calculated_bpm, 1)) + file.update() def _calculate_bpm_callback(self, file, result=None, error=None): if not error: From b9d1138a6dfeb37f1e7043027ea75a4d998b5d83 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Wed, 5 Sep 2018 00:12:25 +0200 Subject: [PATCH 011/123] Delete lastfmplus plugin --- plugins/lastfmplus/__init__.py | 748 ------------------------ plugins/lastfmplus/ui_options_lastfm.py | 748 ------------------------ 2 files changed, 1496 deletions(-) delete mode 100644 plugins/lastfmplus/__init__.py delete mode 100644 plugins/lastfmplus/ui_options_lastfm.py diff --git a/plugins/lastfmplus/__init__.py b/plugins/lastfmplus/__init__.py deleted file mode 100644 index 4e94c244..00000000 --- a/plugins/lastfmplus/__init__.py +++ /dev/null @@ -1,748 +0,0 @@ -# -*- coding: utf-8 -*- - -PLUGIN_NAME = 'Last.fm.Plus' -PLUGIN_AUTHOR = 'RifRaf, Lukáš Lalinský, voiceinsideyou' -PLUGIN_DESCRIPTION = '''Uses folksonomy tags from Last.fm to
-* Sort music into major and minor genres based on configurable genre "whitelists"
-* Add "mood", "occasion" and other custom categories
-* Add "original release year" and "decade" tags, as well as populate blank dates.''' -PLUGIN_VERSION = "0.15" -PLUGIN_API_VERSIONS = ["2.0"] - -from PyQt4 import QtGui, QtCore -from picard.metadata import register_track_metadata_processor -from picard.ui.options import register_options_page, OptionsPage -from picard.config import BoolOption, IntOption, TextOption -from picard.plugins.lastfmplus.ui_options_lastfm import Ui_LastfmOptionsPage -from picard.util import partial -import traceback -import re - -LASTFM_HOST = "ws.audioscrobbler.com" -LASTFM_PORT = 80 - -# From http://www.last.fm/api/tos, 2011-07-30 -# 4.4 (...) You will not make more than 5 requests per originating IP address per second, averaged over a -# 5 minute period, without prior written consent. (...) -from picard.webservice import ratecontrol -ratecontrol.set_minimum_delay((LASTFM_HOST, LASTFM_PORT), 200) - -# Cache for Tags to avoid re-requesting tags within same Picard session -_cache = {} -# Keeps track of requests for tags made to webservice API but not yet returned (to avoid re-requesting the same URIs) -_pending_xmlws_requests = {} - -# Cache to Find the Genres and other Tags -ALBUM_GENRE = {} -ALBUM_SUBGENRE = {} -ALBUM_COUNTRY = {} -ALBUM_CITY = {} -ALBUM_DECADE = {} -ALBUM_YEAR = {} -ALBUM_OCCASION = {} -ALBUM_CATEGORY = {} -ALBUM_MOOD = {} - -#noinspection PyDictCreation -GENRE_FILTER = {} -GENRE_FILTER["_loaded_"] = False -GENRE_FILTER["major"] = ["audiobooks, blues, classic rock, classical, country, dance, electronica, folk, hip-hop, indie, jazz, kids, metal, pop, punk, reggae, rock, soul, trance"] -GENRE_FILTER["minor"] = ["2 tone, a cappella, abstract hip-hop, acid, acid jazz, acid rock, acoustic, acoustic guitar, acoustic rock, adult alternative, adult contemporary, alternative, alternative country, alternative folk, alternative metal, alternative pop, alternative rock, ambient, anti-folk, art rock, atmospheric, aussie hip-hop, avant-garde, ballads, baroque, beach, beats, bebop, big band, blaxploitation, blue-eyed soul, bluegrass, blues rock, boogie rock, boogie woogie, bossa nova, breakbeat, breaks, brit pop, brit rock, british invasion, broadway, bubblegum pop, cabaret, calypso, cha cha, choral, christian rock, classic country, classical guitar, club, college rock, composers, contemporary country, contemporary folk, country folk, country pop, country rock, crossover, dance pop, dancehall, dark ambient, darkwave, delta blues, dirty south, disco, doo wop, doom metal, downtempo, dream pop, drum and bass, dub, dub reggae, dubstep, east coast rap, easy listening, electric blues, electro, electro pop, elevator music, emo, emocore, ethnic, eurodance, europop, experimental, fingerstyle, folk jazz, folk pop, folk punk, folk rock, folksongs, free jazz, french rap, funk, funk metal, funk rock, fusion, g-funk, gaelic, gangsta rap, garage, garage rock, glam rock, goa trance, gospel, gothic, gothic metal, gothic rock, gregorian, groove, grunge, guitar, happy hardcore, hard rock, hardcore, hardcore punk, hardcore rap, hardstyle, heavy metal, honky tonk, horror punk, house, humour, hymn, idm, indie folk, indie pop, indie rock, industrial, industrial metal, industrial rock, instrumental, instrumental hip-hop, instrumental rock, j-pop, j-rock, jangle pop, jazz fusion, jazz vocal, jungle, latin, latin jazz, latin pop, lounge, lovers rock, lullaby, madchester, mambo, medieval, melodic rock, minimal, modern country, modern rock, mood music, motown, neo-soul, new age, new romantic, new wave, noise, northern soul, nu metal, old school rap, opera, orchestral, philly soul, piano, political reggae, polka, pop life, pop punk, pop rock, pop soul, post punk, post rock, power pop, progressive, progressive rock, psychedelic, psychedelic folk, psychedelic punk, psychedelic rock, psychobilly, psytrance, punk rock, quiet storm, r&b, ragga, rap, rap metal, reggae pop, reggae rock, rock and roll, rock opera, rockabilly, rocksteady, roots, roots reggae, rumba, salsa, samba, screamo, shock rock, shoegaze, ska, ska punk, smooth jazz, soft rock, southern rock, space rock, spoken word, standards, stoner rock, surf rock, swamp rock, swing, symphonic metal, symphonic rock, synth pop, tango, techno, teen pop, thrash metal, traditional country, traditional folk, tribal, trip-hop, turntablism, underground, underground hip-hop, underground rap, urban, vocal trance, waltz, west coast rap, western swing, world, world fusion"] -GENRE_FILTER["country"] = ["african, american, arabic, australian, austrian, belgian, brazilian, british, canadian, caribbean, celtic, chinese, cuban, danish, dutch, eastern europe, egyptian, estonian, european, finnish, french, german, greek, hawaiian, ibiza, icelandic, indian, iranian, irish, island, israeli, italian, jamaican, japanese, korean, mexican, middle eastern, new zealand, norwegian, oriental, polish, portuguese, russian, scandinavian, scottish, southern, spanish, swedish, swiss, thai, third world, turkish, welsh, western"] -GENRE_FILTER["city"] = ["acapulco, adelaide, amsterdam, athens, atlanta, atlantic city, auckland, austin, bakersfield, bali, baltimore, bangalore, bangkok, barcelona, barrie, beijing, belfast, berlin, birmingham, bogota, bombay, boston, brasilia, brisbane, bristol, brooklyn, brussels, bucharest, budapest, buenos aires, buffalo, calcutta, calgary, california, cancun, caracas, charlotte, chicago, cincinnati, cleveland, copenhagen, dallas, delhi, denver, detroit, dublin, east coast, edmonton, frankfurt, geneva, glasgow, grand rapids, guadalajara, halifax, hamburg, hamilton, helsinki, hong kong, houston, illinois, indianapolis, istanbul, jacksonville, kansas city, kiev, las vegas, leeds, lisbon, liverpool, london, los angeles, louisville, madrid, manchester, manila, marseille, mazatlan, melbourne, memphis, mexico city, miami, michigan, milan, minneapolis, minnesota, mississippi, monterrey, montreal, munich, myrtle beach, nashville, new jersey, new orleans, new york, new york city, niagara falls, omaha, orlando, oslo, ottawa, palm springs, paris, pennsylvania, perth, philadelphia, phoenix, phuket, pittsburgh, portland, puebla, raleigh, reno, richmond, rio de janeiro, rome, sacramento, salt lake city, san antonio, san diego, san francisco, san jose, santiago, sao paulo, seattle, seoul, shanghai, sheffield, spokane, stockholm, sydney, taipei, tampa, texas, tijuana, tokyo, toledo, toronto, tucson, tulsa, vancouver, victoria, vienna, warsaw, wellington, westcoast, windsor, winnipeg, zurich"] -GENRE_FILTER["mood"] = ["angry, bewildered, bouncy, calm, cheerful, chill, cold, complacent, crazy, crushed, cynical, depressed, dramatic, dreamy, drunk, eclectic, emotional, energetic, envious, feel good, flirty, funky, groovy, happy, haunting, healing, high, hopeful, hot, humorous, inspiring, intense, irritated, laidback, lonely, lovesongs, meditation, melancholic, melancholy, mellow, moody, morose, passionate, peace, peaceful, playful, pleased, positive, quirky, reflective, rejected, relaxed, retro, sad, sentimental, sexy, silly, smooth, soulful, spiritual, suicidal, surprised, sympathetic, trippy, upbeat, uplifting, weird, wild, yearning"] -GENRE_FILTER["decade"] = ["1800s, 1810s, 1820s, 1830s, 1980s, 1850s, 1860s, 1870s, 1880s, 1890s, 1900s, 1910s, 1920s, 1930s, 1940s, 1950s, 1960s, 1970s, 1980s, 1990s, 2000s"] -GENRE_FILTER["year"] = ["1801, 1802, 1803, 1804, 1805, 1806, 1807, 1808, 1809, 1810, 1811, 1812, 1813, 1814, 1815, 1816, 1817, 1818, 1819, 1820, 1821, 1822, 1823, 1824, 1825, 1826, 1827, 1828, 1829, 1830, 1831, 1832, 1833, 1834, 1835, 1836, 1837, 1838, 1839, 1840, 1841, 1842, 1843, 1844, 1845, 1846, 1847, 1848, 1849, 1850, 1851, 1852, 1853, 1854, 1855, 1856, 1857, 1858, 1859, 1860, 1861, 1862, 1863, 1864, 1865, 1866, 1867, 1868, 1869, 1870, 1871, 1872, 1873, 1874, 1875, 1876, 1877, 1878, 1879, 1880, 1881, 1882, 1883, 1884, 1885, 1886, 1887, 1888, 1889, 1890, 1891, 1892, 1893, 1894, 1895, 1896, 1897, 1898, 1899, 1900, 1901, 1902, 1903, 1904, 1905, 1906, 1907, 1908, 1909, 1910, 1911, 1912, 1913, 1914, 1915, 1916, 1917, 1918, 1919, 1920, 1921, 1922, 1923, 1924, 1925, 1926, 1927, 1928, 1929, 1930, 1931, 1932, 1933, 1934, 1935, 1936, 1937, 1938, 1939, 1940, 1941, 1942, 1943, 1944, 1945, 1946, 1947, 1948, 1949, 1950, 1951, 1952, 1953, 1954, 1955, 1956, 1957, 1958, 1959, 1960, 1961, 1962, 1963, 1964, 1965, 1966, 1967, 1968, 1969, 1970, 1971, 1972, 1973, 1974, 1975, 1976, 1977, 1978, 1979, 1980, 1981, 1982, 1983, 1984, 1985, 1986, 1987, 1988, 1989, 1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020"] -GENRE_FILTER["occasion"] = ["background, birthday, breakup, carnival, chillout, christmas, death, dinner, drinking, driving, graduation, halloween, hanging out, heartache, holiday, late night, love, new year, party, protest, rain, rave, romantic, sleep, spring, summer, sunny, twilight, valentine, wake up, wedding, winter, work"] -GENRE_FILTER["category"] = ["animal songs, attitude, autumn, b-side, ballad, banjo, bass, beautiful, body parts, bootlegs, brass, cafe del mar, chamber music, clarinet, classic, classic tunes, compilations, covers, cowbell, deceased, demos, divas, dj, drugs, drums, duets, field recordings, female, female vocalists, film score, flute, food, genius, girl group, great lyrics, guitar solo, guitarist, handclaps, harmonica, historical, horns, hypnotic, influential, insane, jam, keyboard, legends, life, linedance, live, loved, lyricism, male, male vocalists, masterpiece, melodic, memories, musicals, nostalgia, novelty, number songs, old school, oldie, oldies, one hit wonders, orchestra, organ, parody, poetry, political, promos, radio programs, rastafarian, remix, samples, satire, saxophone, showtunes, sing-alongs, singer-songwriter, slide guitar, solo instrumentals, songs with names, soundtracks, speeches, stories, strings, stylish, synth, title is a full sentence, top 40, traditional, trumpet, unique, unplugged, violin, virtuoso, vocalization, vocals"] -GENRE_FILTER["translate"] = { - "drum 'n' bass": "drum and bass", - "drum n bass": "drum and bass" -} - - -def matches_list(s, lst): - if s in lst: - return True - for item in lst: - if '*' in item: - if re.match(re.escape(item).replace(r'\*', '.*?'), s): - return True - return False - -# Function to sort/compare a 2 Element of Tupel - - -def cmp1(a, b): - return cmp(a[1], b[1]) * -1 -# Special Compare/Sort-Function to sort downloaded Tags - - -def cmptaginfo(a, b): - return cmp(a[1][0], b[1][0]) * -1 - - -def _lazy_load_filters(cfg): - if not GENRE_FILTER["_loaded_"]: - GENRE_FILTER["major"] = cfg["lastfm_genre_major"].split(',') - GENRE_FILTER["minor"] = cfg["lastfm_genre_minor"].split(',') - GENRE_FILTER["decade"] = cfg["lastfm_genre_decade"].split(',') - GENRE_FILTER["year"] = cfg["lastfm_genre_year"].split(',') - GENRE_FILTER["country"] = cfg["lastfm_genre_country"].split(',') - GENRE_FILTER["city"] = cfg["lastfm_genre_city"].split(',') - GENRE_FILTER["mood"] = cfg["lastfm_genre_mood"].split(',') - GENRE_FILTER["occasion"] = cfg["lastfm_genre_occasion"].split(',') - GENRE_FILTER["category"] = cfg["lastfm_genre_category"].split(',') - GENRE_FILTER["translate"] = dict([item.split(',') for item in cfg["lastfm_genre_translations"].split("\n")]) - GENRE_FILTER["_loaded_"] = True - - -def apply_translations_and_sally(tag_to_count, sally, factor): - ret = {} - for name, count in tag_to_count.iteritems(): - # apply translations - try: - name = GENRE_FILTER["translate"][name.lower()] - except KeyError: - pass - - # make sure it's lowercase - lower = name.lower() - - if lower not in ret or ret[lower][0] < (count * factor): - ret[lower] = [count * factor, sally] - return ret.items() - - -def _tags_finalize(album, metadata, tags, next): - """Processes the tag metadata to decide which tags to use and sets metadata""" - - if next: - next(tags) - else: - cfg = album.tagger.config.setting - - # last tag-weight for inter-tag comparsion - lastw = {"n": False, "s": False} - # List: (use sally-tags, use track-tags, use artist-tags, use - # drop-info,use minweight,searchlist, max_elems - info = {"major" : [True, True, True, True, True, GENRE_FILTER["major"], cfg["lastfm_max_group_tags"]], - "minor" : [True, True, True, True, True, GENRE_FILTER["minor"], cfg["lastfm_max_minor_tags"]], - "country" : [True, False, True, False, False, GENRE_FILTER["country"], 1], - "city" : [True, False, True, False, False, GENRE_FILTER["city"], 1], - "decade" : [True, True, False, False, False, GENRE_FILTER["decade"], 1], - "year" : [True, True, False, False, False, GENRE_FILTER["year"], 1], - "year2" : [True, True, False, False, False, GENRE_FILTER["year"], 1], - "year3" : [True, True, False, False, False, GENRE_FILTER["year"], 1], - "mood" : [True, True, True, False, False, GENRE_FILTER["mood"], cfg["lastfm_max_mood_tags"]], - "occasion": [True, True, True, False, False, GENRE_FILTER["occasion"], cfg["lastfm_max_occasion_tags"]], - "category": [True, True, True, False, False, GENRE_FILTER["category"], cfg["lastfm_max_category_tags"]] - } - hold = {"all/tags": []} - - # Init the Album-Informations - albid = album.id - if cfg["write_id3v23"]: - year_tag = '~id3:TORY' - else: - year_tag = '~id3:TDOR' - glb = {"major" : {'metatag' : 'grouping', 'data' : ALBUM_GENRE}, - "country" : {'metatag' : 'comment:Songs-DB_Custom3', 'data' : ALBUM_COUNTRY}, - "city" : {'metatag' : 'comment:Songs-DB_Custom3', 'data' : ALBUM_CITY}, - "year" : {'metatag' : year_tag, 'data' : ALBUM_YEAR}, - "year2" : {'metatag' : 'originalyear', 'data' : ALBUM_YEAR}, - "year3" : {'metatag' : 'date', 'data' : ALBUM_YEAR} } - for elem in glb.keys(): - if not albid in glb[elem]['data']: - glb[elem]['data'][albid] = {'count': 1, 'genres': {}} - else: - glb[elem]['data'][albid]['count'] += 1 - - if tags: - # search for tags - tags.sort(cmp=cmptaginfo) - for lowered, [weight, stype] in tags: - name = lowered.title() - # if is tag which should only used for extension (if too few - # tags found) - s = stype == 1 - arttag = stype > 0 # if is artist tag - if not name in hold["all/tags"]: - hold["all/tags"].append(name) - - # Decide if tag should be searched in major and minor fields - drop = not (s and (not lastw['s'] or (lastw['s'] - weight) < cfg["lastfm_max_artisttag_drop"])) and not ( - not s and (not lastw['n'] or (lastw['n'] - weight) < cfg["lastfm_max_tracktag_drop"])) - if not drop: - if s: - lastw['s'] = weight - else: - lastw['n'] = weight - - below = (s and weight < cfg["lastfm_min_artisttag_weight"]) or ( - not s and weight < cfg["lastfm_min_tracktag_weight"]) - - for group, ielem in info.items(): - if matches_list(lowered, ielem[5]): - if below and ielem[4]: - # If Should use min-weigh information - break - if drop and ielem[3]: - # If Should use the drop-information - break - if s and not ielem[0]: - # If Sally-Tag and should not be used - break - if arttag and not ielem[2]: - # If Artist-Tag and should not be used - break - if not arttag and not ielem[1]: - # If Track-Tag and should not be used - break - - # prefer Not-Sally-Tags (so, artist OR track-tags) - if not s and group + "/sally" in hold and name in hold[group + "/sally"]: - hold[group + "/sally"].remove(name) - hold[group + "/tags"].remove(name) - # Insert Tag - if not group + "/tags" in hold: - hold[group + "/tags"] = [] - if not name in hold[group + "/tags"]: - if s: - if not group + "/sally" in hold: - hold[group + "/sally"] = [] - hold[group + "/sally"].append(name) - # collect global genre information for special - # tag-filters - if not arttag and group in glb: - if not name in glb[group]['data'][albid]['genres']: - glb[group]['data'][albid][ - 'genres'][name] = weight - else: - glb[group]['data'][albid][ - 'genres'][name] += weight - # append tag - hold[group + "/tags"].append(name) - # Break becase every Tag should be faced only by one - # GENRE_FILTER - break - - # cut to wanted size - for group, ielem in info.items(): - while group + "/tags" in hold and len(hold[group + "/tags"]) > ielem[6]: - # Remove first all Sally-Tags - if group + "/sally" in hold and len(hold[group + "/sally"]) > 0: - deltag = hold[group + "/sally"].pop() - hold[group + "/tags"].remove(deltag) - else: - hold[group + "/tags"].pop() - - # join the information - join_tags = cfg["lastfm_join_tags_sign"] - - def join_tags_or_not(list): - if join_tags: - return join_tags.join(list) - return list - if 1: - used = [] - - # write the major-tags - if "major/tags" in hold and len(hold["major/tags"]) > 0: - metadata["grouping"] = join_tags_or_not(hold["major/tags"]) - used.extend(hold["major/tags"]) - - # write the decade-tags - if "decade/tags" in hold and len(hold["decade/tags"]) > 0 and cfg["lastfm_use_decade_tag"]: - metadata["comment:Songs-DB_Custom1"] = join_tags_or_not( - [item.lower() for item in hold["decade/tags"]]) - used.extend(hold["decade/tags"]) - - # write country tag - if "country/tags" in hold and len(hold["country/tags"]) > 0 and "city/tags" in hold and len(hold["city/tags"]) > 0 and cfg["lastfm_use_country_tag"] and cfg["lastfm_use_city_tag"]: - metadata["comment:Songs-DB_Custom3"] = join_tags_or_not( - hold["country/tags"] + hold["city/tags"]) - used.extend(hold["country/tags"]) - used.extend(hold["city/tags"]) - elif "country/tags" in hold and len(hold["country/tags"]) > 0 and cfg["lastfm_use_country_tag"]: - metadata["comment:Songs-DB_Custom3"] = join_tags_or_not( - hold["country/tags"]) - used.extend(hold["country/tags"]) - elif "city/tags" in hold and len(hold["city/tags"]) > 0 and cfg["lastfm_use_city_tag"]: - metadata["comment:Songs-DB_Custom3"] = join_tags_or_not( - hold["city/tags"]) - used.extend(hold["city/tags"]) - - # write the mood-tags - if "mood/tags" in hold and len(hold["mood/tags"]) > 0: - metadata["mood"] = join_tags_or_not(hold["mood/tags"]) - used.extend(hold["mood/tags"]) - - # write the occasion-tags - if "occasion/tags" in hold and len(hold["occasion/tags"]) > 0: - metadata["comment:Songs-DB_Occasion"] = join_tags_or_not( - hold["occasion/tags"]) - used.extend(hold["occasion/tags"]) - - # write the category-tags - if "category/tags" in hold and len(hold["category/tags"]) > 0: - metadata["comment:Songs-DB_Custom2"] = join_tags_or_not( - hold["category/tags"]) - used.extend(hold["category/tags"]) - - # include major tags as minor tags also copy major to minor if - # no minor genre - if cfg["lastfm_app_major2minor_tag"] and "major/tags" in hold and "minor/tags" in hold and len(hold["minor/tags"]) > 0: - used.extend(hold["major/tags"]) - used.extend(hold["minor/tags"]) - if len(used) > 0: - metadata["genre"] = join_tags_or_not( - hold["major/tags"] + hold["minor/tags"]) - elif cfg["lastfm_app_major2minor_tag"] and "major/tags" in hold and "minor/tags" not in hold: - used.extend(hold["major/tags"]) - if len(used) > 0: - metadata["genre"] = join_tags_or_not( - hold["major/tags"]) - elif "minor/tags" in hold and len(hold["minor/tags"]) > 0: - metadata["genre"] = join_tags_or_not( - hold["minor/tags"]) - used.extend(hold["minor/tags"]) - else: - if "minor/tags" not in hold and "major/tags" in hold: - metadata["genre"] = metadata["grouping"] - - # replace blank original year with release date - if cfg["lastfm_use_year_tag"]: - if "year/tags" not in hold and len(metadata["date"]) > 0: - metadata["originalyear"] = metadata["date"][:4] - if cfg["write_id3v23"]: - metadata["~id3:TORY"] = metadata["date"][:4] - #album.tagger.log.info('TORY: %r', metadata["~id3:TORY"]) - else: - metadata["~id3:TDOR"] = metadata["date"][:4] - #album.tagger.log.info('TDOR: %r', metadata["~id3:TDOR"]) - if metadata["originalyear"] > metadata["date"][:4]: - metadata["originalyear"] = metadata["date"][:4] - if metadata["~id3:TDOR"] > metadata["date"][:4] and not cfg["write_id3v23"]: - metadata["~id3:TDOR"] = metadata["date"][:4] - if metadata["~id3:TORY"] > metadata["date"][:4] and cfg["write_id3v23"]: - metadata["~id3:TORY"] = metadata["date"][:4] - # Replace blank decades - if "decade/tags" not in hold and len(metadata["originalyear"])>0 and int(metadata["originalyear"])>1999 and cfg["lastfm_use_decade_tag"]: - metadata["comment:Songs-DB_Custom1"] = "20%s0s" % str(metadata["originalyear"])[2] - elif "decade/tags" not in hold and len(metadata["originalyear"])>0 and int(metadata["originalyear"])<2000 and int(metadata["originalyear"])>1899 and cfg["lastfm_use_decade_tag"]: - metadata["comment:Songs-DB_Custom1"] = "19%s0s" % str(metadata["originalyear"])[2] - elif "decade/tags" not in hold and len(metadata["originalyear"])>0 and int(metadata["originalyear"])<1900 and int(metadata["originalyear"])>1799 and cfg["lastfm_use_decade_tag"]: - metadata["comment:Songs-DB_Custom1"] = "18%s0s" % str(metadata["originalyear"])[2] - - -def _tags_downloaded(album, metadata, sally, factor, next, current, data, reply, error): - try: - - try: - intags = data.toptags[0].tag - except AttributeError: - intags = [] - - # Extract just names and counts from response; apply no parsing at this stage - tag_to_count = {} - for tag in intags: - # name of the tag - name = tag.name[0].text.strip() - - # count of the tag - try: - count = int(tag.count[0].text.strip()) - except ValueError: - count = 0 - - tag_to_count[name] = count - - url = str(reply.url().path()) - _cache[url] = tag_to_count - - tags = apply_translations_and_sally(tag_to_count, sally, factor) - - _tags_finalize(album, metadata, current + tags, next) - - # Process any pending requests for the same URL - if url in _pending_xmlws_requests: - pending = _pending_xmlws_requests[url] - del _pending_xmlws_requests[url] - for delayed_call in pending: - delayed_call() - - except: - album.tagger.log.error("Problem processing downloaded tags in last.fm plus plugin: %s", traceback.format_exc()) - raise - finally: - album._requests -= 1 - album._finalize_loading(None) - - -def get_tags(album, metadata, path, sally, factor, next, current): - """Get tags from an URL.""" - - # Ensure config is loaded (or reloaded if has been changed) - _lazy_load_filters(album.tagger.config.setting) - - url = str(QtCore.QUrl.fromPercentEncoding(path)) - if url in _cache: - tags = apply_translations_and_sally(_cache[url], sally, factor) - _tags_finalize(album, metadata, current + tags, next) - else: - - # If we have already sent a request for this URL, delay this call until later - if url in _pending_xmlws_requests: - _pending_xmlws_requests[url].append(partial(get_tags, album, metadata, path, sally, factor, next, current)) - else: - _pending_xmlws_requests[url] = [] - album._requests += 1 - album.tagger.xmlws.get(LASTFM_HOST, LASTFM_PORT, path, - partial(_tags_downloaded, album, metadata, sally, factor, next, current), - priority=True, important=True) - - -def encode_str(s): - # Yes, that's right, Last.fm prefers double URL-encoding - s = QtCore.QUrl.toPercentEncoding(s) - s = QtCore.QUrl.toPercentEncoding(unicode(s)) - return s - - -def get_track_tags(album, metadata, artist, track, next, current): - path = "/1.0/track/%s/%s/toptags.xml" % (encode_str(artist), encode_str(track)) - sally = 0 - factor = 1.0 - return get_tags(album, metadata, path, sally, factor, next, current) - - -def get_artist_tags(album, metadata, artist, next, current): - path = "/1.0/artist/%s/toptags.xml" % encode_str(artist) - sally = 2 - if album.tagger.config.setting["lastfm_artist_tag_us_ex"]: - sally = 1 - factor = album.tagger.config.setting["lastfm_artist_tags_weight"] / 100.0 - return get_tags(album, metadata, path, sally, factor, next, current) - - -def process_track(album, metadata, release, track): - tagger = album.tagger - use_track_tags = tagger.config.setting["lastfm_use_track_tags"] - use_artist_tags = tagger.config.setting["lastfm_artist_tag_us_ex"] or tagger.config.setting["lastfm_artist_tag_us_yes"] - - if use_track_tags or use_artist_tags: - artist = metadata["artist"] - title = metadata["title"] - if artist: - if use_artist_tags: - get_artist_tags_func = partial(get_artist_tags, album, metadata, artist, None) - else: - get_artist_tags_func = None - if title and use_track_tags: - get_track_tags(album, metadata, artist, title, get_artist_tags_func, []) - elif get_artist_tags_func: - get_artist_tags_func([]) - - -class LastfmOptionsPage(OptionsPage): - NAME = "lastfmplus" - TITLE = "Last.fm.Plus" - PARENT = "plugins" - - options = [ - IntOption("setting", "lastfm_max_minor_tags", 4), - IntOption("setting", "lastfm_max_group_tags", 1), - IntOption("setting", "lastfm_max_mood_tags", 4), - IntOption("setting", "lastfm_max_occasion_tags", 4), - IntOption("setting", "lastfm_max_category_tags", 4), - BoolOption("setting", "lastfm_use_country_tag", True), - BoolOption("setting", "lastfm_use_city_tag", True), - BoolOption("setting", "lastfm_use_decade_tag", True), - BoolOption("setting", "lastfm_use_year_tag", True), - TextOption("setting", "lastfm_join_tags_sign", "; "), - BoolOption("setting", "lastfm_app_major2minor_tag", True), - BoolOption("setting", "lastfm_use_track_tags", True), - IntOption("setting", "lastfm_min_tracktag_weight", 5), - IntOption("setting", "lastfm_max_tracktag_drop", 90), - BoolOption("setting", "lastfm_artist_tag_us_no", False), - BoolOption("setting", "lastfm_artist_tag_us_ex", True), - BoolOption("setting", "lastfm_artist_tag_us_yes", False), - IntOption("setting", "lastfm_artist_tags_weight", 95), - IntOption("setting", "lastfm_min_artisttag_weight", 10), - IntOption("setting", "lastfm_max_artisttag_drop", 80), - TextOption("setting", "lastfm_genre_major", ",".join(GENRE_FILTER["major"]).lower()), - TextOption("setting", "lastfm_genre_minor", ",".join(GENRE_FILTER["minor"]).lower()), - TextOption("setting", "lastfm_genre_decade",", ".join(GENRE_FILTER["decade"]).lower()), - TextOption("setting", "lastfm_genre_year",", ".join(GENRE_FILTER["year"]).lower()), - TextOption("setting", "lastfm_genre_occasion",", ".join(GENRE_FILTER["occasion"]).lower()), - TextOption("setting", "lastfm_genre_category",", ".join(GENRE_FILTER["category"]).lower()), - TextOption("setting", "lastfm_genre_country",", ".join(GENRE_FILTER["country"]).lower()), - TextOption("setting", "lastfm_genre_city",", ".join(GENRE_FILTER["city"]).lower()), - TextOption("setting", "lastfm_genre_mood", ",".join(GENRE_FILTER["mood"]).lower()), - TextOption("setting", "lastfm_genre_translations", "\n".join(["%s,%s" % (k,v) for k, v in GENRE_FILTER["translate"].items()]).lower()) - ] - - def __init__(self, parent=None): - super(LastfmOptionsPage, self).__init__(parent) - self.ui = Ui_LastfmOptionsPage() - self.ui.setupUi(self) - # TODO Not yet implemented properly - # self.connect(self.ui.check_translation_list, QtCore.SIGNAL("clicked()"), self.check_translations) - self.connect(self.ui.check_word_lists, - QtCore.SIGNAL("clicked()"), self.check_words) - self.connect(self.ui.load_default_lists, - QtCore.SIGNAL("clicked()"), self.load_defaults) - self.connect(self.ui.filter_report, - QtCore.SIGNAL("clicked()"), self.create_report) - - # function to check all translations and make sure a corresponding word - # exists in word lists, notify in message translations pointing nowhere. - def check_translations(self): - cfg = self.config.setting - translations = (cfg["lastfm_genre_translations"].replace("\n", "|")) - tr2 = list(item for item in translations.split('|')) - wordlists = (cfg["lastfm_genre_major"] + cfg["lastfm_genre_minor"] + cfg["lastfm_genre_country"] + cfg["lastfm_genre_occasion"] - + cfg["lastfm_genre_mood"] + cfg["lastfm_genre_decade"] + cfg["lastfm_genre_year"] + cfg["lastfm_genre_category"]) - # TODO need to check to see if translations are in wordlists - QtGui.QMessageBox.information( - self, self.tr("QMessageBox.showInformation()"), ",".join(tr2)) - - # function to check that word lists contain no duplicate entries, notify - # in message duplicates and which lists they appear in - def check_words(self): - cfg = self.config.setting - # Create a set for each option cfg option - - word_sets = { - "Major": set(str(self.ui.genre_major.text()).split(",")), - "Minor": set(str(self.ui.genre_minor.text()).split(",")), - "Countries": set(str(self.ui.genre_country.text()).split(",")), - "Cities": set(str(self.ui.genre_city.text()).split(",")), - "Moods": set(str(self.ui.genre_mood.text()).split(",")), - "Occasions": set(str(self.ui.genre_occasion.text()).split(",")), - "Decades": set(str(self.ui.genre_decade.text()).split(",")), - "Years": set(str(self.ui.genre_year.text()).split(",")), - "Categories": set(str(self.ui.genre_category.text()).split(",")) - } - - text = [] - duplicates = {} - - for name, words in word_sets.iteritems(): - for word in words: - word = word.strip().title() - duplicates.setdefault(word, []).append(name) - - for word, names in duplicates.iteritems(): - if len(names) > 1: - names = "%s and %s" % (", ".join(names[:-1]), names.pop()) - text.append('"%s" in %s lists.' % (word, names)) - - if not text: - text = "No issues found." - else: - text = "\n\n".join(text) - - # Display results in information box - QtGui.QMessageBox.information(self, self.tr("QMessageBox.showInformation()"), text) - - # load/reload defaults - def load_defaults(self): - self.ui.genre_major.setText(", ".join(GENRE_FILTER["major"])) - self.ui.genre_minor.setText(", ".join(GENRE_FILTER["minor"])) - self.ui.genre_decade.setText(", ".join(GENRE_FILTER["decade"])) - self.ui.genre_country.setText(", ".join(GENRE_FILTER["country"])) - self.ui.genre_city.setText(", ".join(GENRE_FILTER["city"])) - self.ui.genre_year.setText(", ".join(GENRE_FILTER["year"])) - self.ui.genre_occasion.setText(", ".join(GENRE_FILTER["occasion"])) - self.ui.genre_category.setText(", ".join(GENRE_FILTER["category"])) - self.ui.genre_mood.setText(", ".join(GENRE_FILTER["mood"])) - self.ui.genre_translations.setText("00s, 2000s\n10s, 1910s\n1920's, 1920s\n1930's, 1930s\n1940's, 1940s\n1950's, 1950s\n1960's, 1960s\n1970's, 1970s\n1980's, 1980s\n1990's, 1990s\n2-tone, 2 tone\n20's, 1920s\n2000's, 2000s\n2000s, 2000s\n20s, 1920s\n20th century classical, classical\n30's, 1930s\n30s, 1930s\n3rd wave ska revival, ska\n40's, 1940s\n40s, 1940s\n50's, 1950s\n50s, 1950s\n60's, 1960s\n60s, 1960s\n70's, 1970s\n70s, 1970s\n80's, 1980s\n80s, 1980s\n90's, 1990s\n90s, 1990s\na capella, a cappella\nabstract-hip-hop, hip-hop\nacapella, a cappella\nacid-rock, acid rock\nafrica, african\naggresive, angry\naggressive, angry\nalone, lonely\nalready-dead, deceased\nalt rock, alternative rock\nalt-country, alternative country\nalternative punk, punk\nalternative dance, dance\nalternative hip-hop, hip-hop\nalternative pop-rock, pop rock\nalternative punk, punk\nalternative rap, rap\nambient-techno, ambient\namericain, american\namericana, american\nanimal-songs, animal songs\nanimals, animal songs\nanti-war, protest\narena rock, rock\natmospheric-drum-and-bass, drum and bass\nau, australian\naussie hip hop, aussie hip-hop\naussie hiphop, aussie hip-hop\naussie rock, australian\naussie, australian\naussie-rock, rock\naustralia, australian\naustralian aboriginal, world\naustralian country, country\naustralian hip hop, aussie hip-hop\naustralian hip-hop, aussie hip-hop\naustralian rap, aussie hip-hop\naustralian rock, rock\naustralian-music, australian\naustralianica, australian\naustralicana, australian\naustria, austrian\navantgarde, avant-garde\nbakersfield-sound, bakersfield\nbaroque pop, baroque\nbeach music, beach\nbeat, beats\nbelgian music, belgian\nbelgian-music, belgian\nbelgium, belgian\nbhangra, indian\nbig beat, beats\nbigbeat, beats\nbittersweet, cynical\nblack metal, doom metal\nblue, sad\nblues guitar, blues\nblues-rock, blues rock\nbluesrock, blues rock\nbollywood, indian\nboogie, boogie woogie\nboogiewoogieflu, boogie woogie\nbrazil, brazilian\nbreakbeats, breakbeat\nbreaks artists, breakbeat\nbrit, british\nbrit-pop, brit pop\nbrit-rock, brit rock\nbritish blues, blues\nbritish punk, punk\nbritish rap, rap\nbritish rock, brit rock\nbritish-folk, folk\nbritpop, brit pop\nbritrock, brit rock\nbroken beat, breakbeat\nbrutal-death-metal, doom metal\nbubblegum, bubblegum pop\nbuddha bar, chillout\ncalming, relaxed\ncanada, canadian\ncha-cha, cha cha\ncha-cha-cha, cha cha\nchicago blues, blues\nchildren, kids\nchildrens music, kids\nchildrens, kids\nchill out, chillout\nchill-out, chillout\nchilled, chill\nchillhouse, chill\nchillin, hanging out\nchristian, gospel\nchina, chinese\nclasica, classical\nclassic blues, blues\nclassic jazz, jazz\nclassic metal, metal\nclassic pop, pop\nclassic punk, punk\nclassic roots reggae, roots reggae\nclassic soul, soul\nclassic-hip-hop, hip-hop\nclassical crossover, classical\nclassical music, classical\nclassics, classic tunes\nclassique, classical\nclub-dance, dance\nclub-house, house\nclub-music, club\ncollegiate acappella, a cappella\ncomedy rock, humour\ncomedy, humour\ncomposer, composers\nconscious reggae, reggae\ncontemporary classical, classical\ncontemporary gospel, gospel\ncontemporary jazz, jazz\ncontemporary reggae, reggae\ncool-covers, covers\ncountry folk, country\ncountry soul, country\ncountry-divas, country\ncountry-female, country\ncountry-legends, country\ncountry-pop, country pop\ncountry-rock, country rock\ncover, covers\ncover-song, covers\ncover-songs, covers\ncowboy, country\ncowhat-fav, country\ncowhat-hero, country\ncuba, cuban\ncyberpunk, punk\nd'n'b, drum and bass\ndance party, party\ndance-punk, punk\ndance-rock, rock\ndancefloor, dance\ndancehall-reggae, dancehall\ndancing, dance\ndark-psy, psytrance\ndark-psytrance, psytrance\ndarkpsy, dark ambient\ndeath metal, doom metal\ndeathcore, thrash metal\ndeep house, house\ndeep-soul, soul\ndeepsoul, soul\ndepressing, depressed\ndepressive, depressed \ndeutsch, german\ndisco-funk, disco\ndisco-house, disco\ndiva, divas\ndj mix, dj\ndnb, drum and bass\ndope, drugs\ndownbeat, downtempo\ndream dance, trance\ndream trance, trance\ndrill 'n' bass, drum and bass\ndrill and bass, drum and bass\ndrill n bass, drum and bass\ndrill-n-bass, drum and bass\ndrillandbass, drum and bass\ndrinking songs, drinking\ndriving-music, driving\ndrum 'n' bass, drum and bass\ndrum n bass, drum and bass\ndrum'n'bass, drum and bass\ndrum, drums\ndrum-n-bass, drum and bass\ndrumandbass, drum and bass\ndub-u, dub\ndub-u-dub, dub\ndub-wise, dub\nduet, duets\nduo, duets\ndutch artists, dutch\ndutch rock, rock\ndutch-bands, dutch\ndutch-sound, dutch\nearly reggae, reggae\neasy, easy listening\negypt, egyptian\neighties, 1980s\nelectro dub, electro\nelectro funk, electro\nelectro house, house\nelectro rock, electro\nelectro-pop, electro\nelectroclash, electro\nelectrofunk, electro\nelectrohouse, house\nelectronic, electronica\nelectronic-rock, rock\nelectronicadance, dance\nelectropop, electro pop\nelectropunk, punk\nelegant, stylish\nelektro, electro\nelevator, elevator music\nemotive, emotional\nenergy, energetic\nengland, british\nenglish, british\nenraged, angry\nepic-trance, trance\nethnic fusion, ethnic\neuro-dance, eurodance\neuro-pop, europop\neuro-trance, trance\neurotrance, trance\neurovision, eurodance\nexperimental-rock, experimental\nfair dinkum australian mate, australian\nfeel good music, feel good\nfeelgood, feel good\nfemale artists, female\nfemale country, country\nfemale fronted, female\nfemale singers, female\nfemale vocalist, female vocalists\nfemale-vocal, female vocalists\nfemale-vocals, female vocalists\nfemale-voices, female vocalists\nfield recording, field recordings\nfilm, film score\nfilm-score, film score\nfingerstyle guitar, fingerstyle\nfinland, finnish\nfinnish-metal, metal\nflamenco rumba, rumba\nfolk-jazz, folk jazz\nfolk-pop, folk pop\nfolk-rock, folk rock\nfolkrock, folk rock\nfrancais, french\nfrance, french\nfreestyle, electronica\nfull on, energetic\nfull-on, energetic\nfull-on-psychedelic-trance, psytrance\nfull-on-trance, trance\nfullon, intense \nfuneral, death\nfunky breaks, breaks\nfunky house, house\nfunny, humorous\ngabber, hardcore\ngeneral pop, pop\ngeneral rock, rock\ngentle, smooth\ngermany, german\ngirl-band, girl group\ngirl-group, girl group\ngirl-groups, girl group\ngirl-power, girl group\ngirls, girl group\nglam metal, glam rock\nglam, glam rock\ngloomy, depressed\ngoa classic, goa trance\ngoa, goa trance\ngoa-psy-trance, psytrance\ngoatrance, trance\ngolden oldies, oldies\ngoth rock, gothic rock\ngoth, gothic\ngothic doom metal, gothic metal\ngreat-lyricists, great lyrics\ngreat-lyrics, great lyrics\ngrime, dubstep\ngregorian chant, gregorian\ngrock 'n' roll, rock and roll\ngroovin, groovy\ngrunge rock, grunge\nguitar god, guitar\nguitar gods, guitar\nguitar hero, guitar\nguitar rock, rock\nguitar-solo, guitar solo\nguitar-virtuoso, guitarist\nhair metal, glam rock\nhanging-out, hanging out\nhappiness, happy\nhappy thoughts, happy\nhard dance, dance\nhard house, house\nhard-trance, trance\nhardcore-techno, techno\nhawaii, hawaiian\nheartbreak, heartache\nheavy rock, hard rock\nhilarious, humorous\nhip hop, hip-hop\nhip-hop and rap, hip-hop\nhip-hoprap, hip-hop\nhiphop, hip-hop\nhippie, stoner rock\nhope, hopeful\nhorrorcore, thrash metal\nhorrorpunk, horror punk\nhumor, humour\nindia, indian\nindie electronic, electronica\nindietronica, electronica\ninspirational, inspiring\ninstrumental pop, instrumental \niran, iranian\nireland, irish\nisrael, israeli\nitaly, italian\njam band, jam\njamaica, jamaican\njamaican ska, ska\njamaician, jamaican\njamaican-artists, jamaican\njammer, jam\njazz blues, jazz\njazz funk, jazz\njazz hop, jazz\njazz piano, jazz\njpop, j-pop\njrock, j-rock\njazz rock, jazz\njazzy, jazz\njump blues, blues\nkiwi, new zealand\nlaid back, easy listening\nlatin rock, latin\nlatino, latin\nle rap france, french rap\nlegend, legends\nlegendary, legends\nlekker ska, ska\nlions-reggae-dancehall, dancehall\nlistless, irritated\nlively, energetic\nlove metal, metal\nlove song, romantic\nlove-songs, lovesongs\nlovely, beautiful\nmade-in-usa, american\nmakes me happy, happy\nmale country, country\nmale groups, male\nmale rock, male\nmale solo artists, male\nmale vocalist, male vocalists\nmale-vocal, male vocalists\nmale-vocals, male vocalists\nmarijuana, drugs\nmelancholic days, melancholy\nmelodic death metal, doom metal\nmelodic hardcore, hardcore\nmelodic metal, metal\nmelodic metalcore, metal\nmelodic punk, punk\nmelodic trance, trance\nmetalcore, thrash metal\nmetro downtempo, downtempo\nmetro reggae, reggae\nmiddle east, middle eastern\nminimal techno, techno\nmood, moody\nmorning, wake up\nmoses reggae, reggae\nmovie, soundtracks\nmovie-score, soundtracks\nmovie-score-composers, composers\nmovie-soundtrack, soundtracks\nmusical, musicals\nmusical-theatre, musicals\nneder rock, rock \nnederland, dutch\nnederlands, dutch\nnederlandse-muziek, dutch\nnederlandstalig, dutch\nnederpop, pop\nnederrock, rock\nnederska, ska\nnedertop, dutch\nneo prog, progressive\nneo progressive rock, progressive rock\nneo progressive, progressive\nneo psychedelia, psychedelic\nneo soul, soul\nnerd rock, rock\nnetherlands, dutch\nneurofunk, funk\nnew rave, rave\nnew school breaks, breaks \nnew school hardcore, hardcore\nnew traditionalist country, traditional country\nnice elevator music, elevator music\nnight, late night\nnight-music, late night\nnoise pop, pop\nnoise rock, rock\nnorway, norwegian\nnostalgic, nostalgia\nnu breaks, breaks\nnu jazz, jazz\nnu skool breaks, breaks \nnu-metal, nu metal\nnumber-songs, number songs\nnumbers, number songs\nnumetal, metal\nnz, new zealand\nold country, country\nold school hardcore, hardcore \nold school hip-hop, hip-hop\nold school reggae, reggae\nold school soul, soul\nold-favorites, oldie\nold-skool, old school\nold-timey, oldie\noldschool, old school\none hit wonder, one hit wonders\noptimistic, positive\noutlaw country, country\noz hip hop, aussie hip-hop\noz rock, rock\noz, australian\nozzie, australian\npancaribbean, caribbean\nparodies, parody\nparty-groovin, party\nparty-music, party\nparty-time, party\npiano rock, piano\npolitical punk, punk\npolitical rap, rap\npool party, party\npop country, country pop\npop music, pop\npop rap, rap\npop-rap, rap\npop-rock, pop rock\npop-soul, pop soul\npoprock, pop rock\nportugal, portuguese\npositive-vibrations, positive\npost grunge, grunge\npost hardcore, hardcore\npost-grunge, grunge\npost-hardcore, hardcore\npost-punk, post punk\npost-rock, post rock\npostrock, post rock\npower ballad, ballad\npower ballads, ballad\npower metal, metal\nprog rock, progressive rock\nprogressive breaks, breaks\nprogressive house, house\nprogressive metal, nu metal\nprogressive psytrance, psytrance \nprogressive trance, psytrance\nproto-punk, punk\npsy, psytrance\npsy-trance, psytrance\npsybient, ambient\npsych folk, psychedelic folk\npsych, psytrance\npsychadelic, psychedelic\npsychedelia, psychedelic\npsychedelic pop, psychedelic\npsychedelic trance, psytrance\npsychill, psytrance\npsycho, insane\npsytrance artists, psytrance\npub rock, rock \npunk blues, punk\npunk caberet, punk\npunk favorites, punk \npunk pop, punk\npunk revival, punk\npunkabilly, punk\npunkrock, punk rock\nqueer, quirky\nquiet, relaxed\nr and b, r&b\nr'n'b, r&b\nr-n-b, r&b\nraggae, reggae\nrap and hip-hop, rap\nrap hip-hop, rap\nrap rock, rap\nrapcore, rap metal\nrasta, rastafarian\nrastafari, rastafarian\nreal hip-hop, hip-hop\nreegae, reggae\nreggae and dub, reggae\nreggae broeder, reggae\nreggae dub ska, reggae\nreggae roots, roots reggae\nreggae-pop, reggae pop\nreggea, reggae\nrelax, relaxed\nrelaxing, relaxed\nrhythm and blues, r&b\nrnb, r&b\nroad-trip, driving\nrock ballad, ballad\nrock ballads, ballad\nrock n roll, rock and roll\nrock pop, pop rock\nrock roll, rock and roll\nrock'n'roll, rock and roll\nrock-n-roll, rock and roll\nrocknroll, rock and roll\nrockpop, pop rock\nromance, romantic\nromantic-tension, romantic\nroots and culture, roots\nroots rock, rock\nrootsreggae, roots reggae \nrussian alternative, russian\nsad-songs, sad\nsample, samples\nsaturday night, party\nsax, saxophone\nscotland, scottish\nseden, swedish\nsensual, passionate\nsing along, sing-alongs\nsing alongs, sing-alongs\nsing-along, sing-alongs\nsinger-songwriters, singer-songwriter\nsingersongwriter, singer-songwriter\nsixties, 1960s\nska revival, ska \nska-punk, ska punk\nskacore, ska\nskate punk, punk\nskinhead reggae, reggae\nsleepy, sleep\nslow jams, slow jam\nsmooth soul, soul\nsoft, smooth\nsolo country acts, country\nsolo instrumental, solo instrumentals\nsoothing, smooth\nsoulful drum and bass, drum and bass\nsoundtrack, soundtracks\nsouth africa, african\nsouth african, african\nsouthern rap, rap\nsouthern soul, soul\nspain, spanish\nspeed metal, metal\nspeed, drugs\nspirituals, spiritual\nspliff, drugs\nstoner, stoner rock\nstreet punk, punk\nsuicide, death\nsuicide, suicidal\nsummertime, summer\nsun-is-shining, sunny\nsunshine pop, pop\nsuper pop, pop\nsurf, surf rock\nswamp blues, swamp rock\nswamp, swamp rock\nsweden, swedish\nswedish metal, metal\nsymphonic power metal, symphonic metal\nsynthpop, synth pop\ntexas blues, blues\ntexas country, country\nthird wave ska revival, ska\nthird wave ska, ska\ntraditional-ska, ska\ntrancytune, trance\ntranquility, peaceful\ntribal house, tribal\ntribal rock, tribal\ntrip hop, trip-hop\ntriphop, trip-hop\ntwo tone, 2 tone\ntwo-tone, 2 tone\nuk hip-hop, hip-hop\nuk, british\nunited kingdom, british\nunited states, american\nuntimely-death, deceased\nuplifting trance, trance\nus, american\nusa, american\nvocal house, house\nvocal jazz, jazz vocal\nvocal pop, pop\nvocal, vocals\nwales, welsh\nweed, drugs\nwest-coast, westcoast\nworld music, world\nxmas, christmas\n") - - - def import_newlist(self): - fileName = QtGui.QFileDialog.getOpenFileName(self, - self.tr("QFileDialog.getOpenFileName()"), - self.ui.fileName.text(), - self.tr("All Files (*);;Text Files (*.txt)")) - if not fileName.isEmpty(): - self.ui.fileName.setText(fileName) - columns = [] - lists = {} - with open(fileName) as f: - for line in f: - data = line.rstrip('\r\n').split(",") - if not columns: # first line - columns = tuple(data) - for column in columns: - lists[column] = [] - else: # data lines - for column, value in zip(columns, data): - if value: - lists[column].append(value) - - self.ui.genre_major.setText(', '.join(lists['Major'])) - self.ui.genre_minor.setText(', '.join(lists['Minor'])) - self.ui.genre_country.setText(', '.join(lists['Country'])) - self.ui.genre_city.setText(', '.join(lists['City'])) - self.ui.genre_decade.setText(', '.join(lists['Decade'])) - self.ui.genre_mood.setText(', '.join(lists['Mood'])) - self.ui.genre_occasion.setText(', '.join(lists['Occasion'])) - - # Function to create simple report window. Could do a count of values in - # each section and the amount of translations. Total tags being scanned - # for. - def create_report(self): - cfg = self.config.setting - options = [ - ('lastfm_genre_major', 'Major Genre Terms'), - ('lastfm_genre_minor', 'Minor Genre Terms'), - ('lastfm_genre_country', 'Country Terms'), - ('lastfm_genre_city', 'City Terms'), - ('lastfm_genre_mood', 'Mood Terms'), - ('lastfm_genre_occasion', 'Occasions Terms'), - ('lastfm_genre_decade', 'Decade Terms'), - ('lastfm_genre_year', 'Year Terms'), - ('lastfm_genre_category', 'Category Terms'), - ('lastfm_genre_translations', 'Translation Terms'), - ] - text = [] - for name, label in options: - nterms = cfg[name].count(',') + 1 - if nterms: - text.append(" • %d %s" % (nterms, label)) - if not text: - text = "No terms found" - else: - text = "You have a total of:
" + "
".join(text) + "" - # Display results in information box - QtGui.QMessageBox.information(self, self.tr("QMessageBox.showInformation()"), text) - - def load(self): - # general - cfg = self.config.setting - self.ui.max_minor_tags.setValue(cfg["lastfm_max_minor_tags"]) - self.ui.max_group_tags.setValue(cfg["lastfm_max_group_tags"]) - self.ui.max_mood_tags.setValue(cfg["lastfm_max_mood_tags"]) - self.ui.max_occasion_tags.setValue(cfg["lastfm_max_occasion_tags"]) - self.ui.max_category_tags.setValue(cfg["lastfm_max_category_tags"]) - self.ui.use_country_tag.setChecked(cfg["lastfm_use_country_tag"]) - self.ui.use_city_tag.setChecked(cfg["lastfm_use_city_tag"]) - self.ui.use_decade_tag.setChecked(cfg["lastfm_use_decade_tag"]) - self.ui.use_year_tag.setChecked(cfg["lastfm_use_year_tag"]) - self.ui.join_tags_sign.setText(cfg["lastfm_join_tags_sign"]) - self.ui.app_major2minor_tag.setChecked(cfg["lastfm_app_major2minor_tag"]) - self.ui.use_track_tags.setChecked(cfg["lastfm_use_track_tags"]) - self.ui.min_tracktag_weight.setValue(cfg["lastfm_min_tracktag_weight"]) - self.ui.max_tracktag_drop.setValue(cfg["lastfm_max_tracktag_drop"]) - self.ui.artist_tag_us_no.setChecked(cfg["lastfm_artist_tag_us_no"]) - self.ui.artist_tag_us_ex.setChecked(cfg["lastfm_artist_tag_us_ex"]) - self.ui.artist_tag_us_yes.setChecked(cfg["lastfm_artist_tag_us_yes"]) - self.ui.artist_tags_weight.setValue(cfg["lastfm_artist_tags_weight"]) - self.ui.min_artisttag_weight.setValue(cfg["lastfm_min_artisttag_weight"]) - self.ui.max_artisttag_drop.setValue(cfg["lastfm_max_artisttag_drop"]) - self.ui.genre_major.setText( cfg["lastfm_genre_major"].replace(",", ", ") ) - self.ui.genre_minor.setText( cfg["lastfm_genre_minor"].replace(",", ", ") ) - self.ui.genre_decade.setText( cfg["lastfm_genre_decade"].replace(",", ", ") ) - self.ui.genre_country.setText(cfg["lastfm_genre_country"].replace(",", ", ") ) - self.ui.genre_city.setText(cfg["lastfm_genre_city"].replace(",", ", ") ) - self.ui.genre_year.setText( cfg["lastfm_genre_year"].replace(",", ", ") ) - self.ui.genre_occasion.setText(cfg["lastfm_genre_occasion"].replace(",", ", ") ) - self.ui.genre_category.setText(cfg["lastfm_genre_category"].replace(",", ", ") ) - self.ui.genre_year.setText(cfg["lastfm_genre_year"].replace(",", ", ") ) - self.ui.genre_mood.setText( cfg["lastfm_genre_mood"].replace(",", ", ") ) - self.ui.genre_translations.setText(cfg["lastfm_genre_translations"].replace(",", ", ") ) - - def save(self): - self.config.setting["lastfm_max_minor_tags"] = self.ui.max_minor_tags.value() - self.config.setting["lastfm_max_group_tags"] = self.ui.max_group_tags.value() - self.config.setting["lastfm_max_mood_tags"] = self.ui.max_mood_tags.value() - self.config.setting["lastfm_max_occasion_tags"] = self.ui.max_occasion_tags.value() - self.config.setting["lastfm_max_category_tags"] = self.ui.max_category_tags.value() - self.config.setting["lastfm_use_country_tag"] = self.ui.use_country_tag.isChecked() - self.config.setting["lastfm_use_city_tag"] = self.ui.use_city_tag.isChecked() - self.config.setting["lastfm_use_decade_tag"] = self.ui.use_decade_tag.isChecked() - self.config.setting["lastfm_use_year_tag"] = self.ui.use_year_tag.isChecked() - self.config.setting["lastfm_join_tags_sign"] = self.ui.join_tags_sign.text() - self.config.setting["lastfm_app_major2minor_tag"] = self.ui.app_major2minor_tag.isChecked() - self.config.setting["lastfm_use_track_tags"] = self.ui.use_track_tags.isChecked() - self.config.setting["lastfm_min_tracktag_weight"] = self.ui.min_tracktag_weight.value() - self.config.setting["lastfm_max_tracktag_drop"] = self.ui.max_tracktag_drop.value() - self.config.setting["lastfm_artist_tag_us_no"] = self.ui.artist_tag_us_no.isChecked() - self.config.setting["lastfm_artist_tag_us_ex"] = self.ui.artist_tag_us_ex.isChecked() - self.config.setting["lastfm_artist_tag_us_yes"] = self.ui.artist_tag_us_yes.isChecked() - self.config.setting["lastfm_artist_tags_weight"] = self.ui.artist_tags_weight.value() - self.config.setting["lastfm_min_artisttag_weight"] = self.ui.min_artisttag_weight.value() - self.config.setting["lastfm_max_artisttag_drop"] = self.ui.max_artisttag_drop.value() - - # parse littlebit the text-inputs - tmp0 = {} - tmp1 = [tmp0.setdefault(i.strip(), i.strip()) - for i in unicode(self.ui.genre_major.text()).lower().split(",") if i not in tmp0] - tmp1.sort() - self.config.setting["lastfm_genre_major"] = ",".join(tmp1) - tmp0 = {} - tmp1 = [tmp0.setdefault(i.strip(), i.strip()) - for i in unicode(self.ui.genre_minor.text()).lower().split(",") if i not in tmp0] - tmp1.sort() - self.config.setting["lastfm_genre_minor"] = ",".join(tmp1) - tmp0 = {} - tmp1 = [tmp0.setdefault(i.strip(), i.strip()) - for i in unicode(self.ui.genre_decade.text()).lower().split(",") if i not in tmp0] - tmp1.sort() - self.config.setting["lastfm_genre_decade"] = ",".join(tmp1) - tmp0 = {} - tmp1 = [tmp0.setdefault(i.strip(), i.strip()) - for i in unicode(self.ui.genre_year.text()).lower().split(",") if i not in tmp0] - tmp1.sort() - self.config.setting["lastfm_genre_year"] = ",".join(tmp1) - tmp0 = {} - tmp1 = [tmp0.setdefault(i.strip(), i.strip()) - for i in unicode(self.ui.genre_country.text()).lower().split(",") if i not in tmp0] - tmp1.sort() - self.config.setting["lastfm_genre_country"] = ",".join(tmp1) - tmp0 = {} - tmp1 = [tmp0.setdefault(i.strip(), i.strip()) - for i in unicode(self.ui.genre_city.text()).lower().split(",") if i not in tmp0] - tmp1.sort() - self.config.setting["lastfm_genre_city"] = ",".join(tmp1) - tmp0 = {} - tmp1 = [tmp0.setdefault(i.strip(), i.strip()) - for i in unicode(self.ui.genre_occasion.text()).lower().split(",") if i not in tmp0] - tmp1.sort() - self.config.setting["lastfm_genre_occasion"] = ",".join(tmp1) - tmp0 = {} - tmp1 = [tmp0.setdefault(i.strip(), i.strip()) - for i in unicode(self.ui.genre_category.text()).lower().split(",") if i not in tmp0] - tmp1.sort() - self.config.setting["lastfm_genre_category"] = ",".join(tmp1) - tmp0 = {} - tmp1 = [tmp0.setdefault(i.strip(), i.strip()) - for i in unicode(self.ui.genre_mood.text()).lower().split(",") if i not in tmp0] - tmp1.sort() - self.config.setting["lastfm_genre_mood"] = ",".join(tmp1) - - trans = {} - tmp0 = unicode( - self.ui.genre_translations.toPlainText()).lower().split("\n") - for tmp1 in tmp0: - tmp2 = tmp1.split(',') - if len(tmp2) == 2: - tmp2[0] = tmp2[0].strip() - tmp2[1] = tmp2[1].strip() - if len(tmp2[0]) < 1 or len(tmp2[1]) < 1: - continue - if tmp2[0] in trans and trans[tmp2[0]] != tmp2[1]: - del trans[tmp2[0]] - elif not tmp2[0] in trans: - trans[tmp2[0]] = tmp2[1] - - tmp3 = trans.items() - tmp3.sort() - self.config.setting["lastfm_genre_translations"] = "\n".join(["%s,%s" % (k,v) for k, v in tmp3]) - GENRE_FILTER["_loaded_"] = False - - -register_track_metadata_processor(process_track) -register_options_page(LastfmOptionsPage) diff --git a/plugins/lastfmplus/ui_options_lastfm.py b/plugins/lastfmplus/ui_options_lastfm.py deleted file mode 100644 index f2743a54..00000000 --- a/plugins/lastfmplus/ui_options_lastfm.py +++ /dev/null @@ -1,748 +0,0 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'options_lastfmplus.ui' -# -# Created: Thu Jul 23 10:55:17 2009 -# by: PyQt4 UI code generator 4.4.4 -# -# WARNING! All changes made in this file will be lost! - -from PyQt4 import QtCore, QtGui - - -class Ui_LastfmOptionsPage(object): - - def setupUi(self, LastfmOptionsPage): - LastfmOptionsPage.setObjectName("LastfmOptionsPage") - LastfmOptionsPage.resize(414, 493) - self.horizontalLayout = QtGui.QHBoxLayout(LastfmOptionsPage) - self.horizontalLayout.setObjectName("horizontalLayout") - self.tabWidget = QtGui.QTabWidget(LastfmOptionsPage) - self.tabWidget.setMinimumSize(QtCore.QSize(330, 475)) - self.tabWidget.setElideMode(QtCore.Qt.ElideNone) - self.tabWidget.setUsesScrollButtons(False) - self.tabWidget.setObjectName("tabWidget") - self.tab_4 = QtGui.QWidget() - self.tab_4.setObjectName("tab_4") - self.gridLayout_3 = QtGui.QGridLayout(self.tab_4) - self.gridLayout_3.setObjectName("gridLayout_3") - self.groupBox_5 = QtGui.QGroupBox(self.tab_4) - self.groupBox_5.setMinimumSize(QtCore.QSize(0, 0)) - self.groupBox_5.setBaseSize(QtCore.QSize(0, 0)) - self.groupBox_5.setObjectName("groupBox_5") - self.gridLayout_2 = QtGui.QGridLayout(self.groupBox_5) - self.gridLayout_2.setObjectName("gridLayout_2") - self.label_10 = QtGui.QLabel(self.groupBox_5) - self.label_10.setLayoutDirection(QtCore.Qt.RightToLeft) - self.label_10.setObjectName("label_10") - self.gridLayout_2.addWidget(self.label_10, 0, 0, 1, 1) - self.max_group_tags = QtGui.QSpinBox(self.groupBox_5) - self.max_group_tags.setObjectName("max_group_tags") - self.gridLayout_2.addWidget(self.max_group_tags, 0, 1, 1, 1) - spacerItem = QtGui.QSpacerItem(20, 95, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) - self.gridLayout_2.addItem(spacerItem, 0, 2, 4, 1) - self.label_12 = QtGui.QLabel(self.groupBox_5) - self.label_12.setLayoutDirection(QtCore.Qt.RightToLeft) - self.label_12.setObjectName("label_12") - self.gridLayout_2.addWidget(self.label_12, 0, 3, 1, 1) - self.max_mood_tags = QtGui.QSpinBox(self.groupBox_5) - self.max_mood_tags.setObjectName("max_mood_tags") - self.gridLayout_2.addWidget(self.max_mood_tags, 0, 4, 1, 1) - self.label_11 = QtGui.QLabel(self.groupBox_5) - self.label_11.setLayoutDirection(QtCore.Qt.RightToLeft) - self.label_11.setObjectName("label_11") - self.gridLayout_2.addWidget(self.label_11, 1, 0, 1, 1) - self.max_minor_tags = QtGui.QSpinBox(self.groupBox_5) - self.max_minor_tags.setObjectName("max_minor_tags") - self.gridLayout_2.addWidget(self.max_minor_tags, 1, 1, 1, 1) - self.label_14 = QtGui.QLabel(self.groupBox_5) - self.label_14.setLayoutDirection(QtCore.Qt.RightToLeft) - self.label_14.setObjectName("label_14") - self.gridLayout_2.addWidget(self.label_14, 1, 3, 1, 1) - self.max_occasion_tags = QtGui.QSpinBox(self.groupBox_5) - self.max_occasion_tags.setObjectName("max_occasion_tags") - self.gridLayout_2.addWidget(self.max_occasion_tags, 1, 4, 1, 1) - self.label_15 = QtGui.QLabel(self.groupBox_5) - self.label_15.setLayoutDirection(QtCore.Qt.RightToLeft) - self.label_15.setObjectName("label_15") - self.gridLayout_2.addWidget(self.label_15, 2, 3, 1, 1) - self.max_category_tags = QtGui.QSpinBox(self.groupBox_5) - self.max_category_tags.setObjectName("max_category_tags") - self.gridLayout_2.addWidget(self.max_category_tags, 2, 4, 1, 1) - self.app_major2minor_tag = QtGui.QCheckBox(self.groupBox_5) - self.app_major2minor_tag.setObjectName("app_major2minor_tag") - self.gridLayout_2.addWidget(self.app_major2minor_tag, 3, 0, 1, 2) - self.label_26 = QtGui.QLabel(self.groupBox_5) - self.label_26.setLayoutDirection(QtCore.Qt.RightToLeft) - self.label_26.setObjectName("label_26") - self.gridLayout_2.addWidget(self.label_26, 3, 3, 1, 1) - self.join_tags_sign = QtGui.QLineEdit(self.groupBox_5) - self.join_tags_sign.setObjectName("join_tags_sign") - self.gridLayout_2.addWidget(self.join_tags_sign, 3, 4, 1, 1) - self.gridLayout_3.addWidget(self.groupBox_5, 0, 0, 1, 1) - self.groupBox_4 = QtGui.QGroupBox(self.tab_4) - self.groupBox_4.setObjectName("groupBox_4") - self.gridLayout_6 = QtGui.QGridLayout(self.groupBox_4) - self.gridLayout_6.setObjectName("gridLayout_6") - self.use_country_tag = QtGui.QCheckBox(self.groupBox_4) - self.use_country_tag.setObjectName("use_country_tag") - self.gridLayout_6.addWidget(self.use_country_tag, 0, 0, 1, 1) - self.use_city_tag = QtGui.QCheckBox(self.groupBox_4) - self.use_city_tag.setTristate(False) - self.use_city_tag.setObjectName("use_city_tag") - self.gridLayout_6.addWidget(self.use_city_tag, 1, 0, 1, 1) - self.use_year_tag = QtGui.QCheckBox(self.groupBox_4) - self.use_year_tag.setObjectName("use_year_tag") - self.gridLayout_6.addWidget(self.use_year_tag, 0, 1, 1, 1) - self.use_decade_tag = QtGui.QCheckBox(self.groupBox_4) - self.use_decade_tag.setObjectName("use_decade_tag") - self.gridLayout_6.addWidget(self.use_decade_tag, 1, 1, 1, 1) - self.gridLayout_3.addWidget(self.groupBox_4, 1, 0, 1, 1) - self.groupBox_9 = QtGui.QGroupBox(self.tab_4) - self.groupBox_9.setObjectName("groupBox_9") - self.gridLayout_4 = QtGui.QGridLayout(self.groupBox_9) - self.gridLayout_4.setObjectName("gridLayout_4") - self.use_track_tags = QtGui.QCheckBox(self.groupBox_9) - self.use_track_tags.setChecked(False) - self.use_track_tags.setObjectName("use_track_tags") - self.gridLayout_4.addWidget(self.use_track_tags, 0, 0, 1, 1) - self.label_19 = QtGui.QLabel(self.groupBox_9) - self.label_19.setLayoutDirection(QtCore.Qt.RightToLeft) - self.label_19.setObjectName("label_19") - self.gridLayout_4.addWidget(self.label_19, 0, 2, 1, 1) - self.min_tracktag_weight = QtGui.QSpinBox(self.groupBox_9) - self.min_tracktag_weight.setObjectName("min_tracktag_weight") - self.gridLayout_4.addWidget(self.min_tracktag_weight, 0, 3, 1, 1) - self.label_20 = QtGui.QLabel(self.groupBox_9) - self.label_20.setLayoutDirection(QtCore.Qt.RightToLeft) - self.label_20.setObjectName("label_20") - self.gridLayout_4.addWidget(self.label_20, 1, 2, 1, 1) - self.max_tracktag_drop = QtGui.QSpinBox(self.groupBox_9) - self.max_tracktag_drop.setObjectName("max_tracktag_drop") - self.gridLayout_4.addWidget(self.max_tracktag_drop, 1, 3, 1, 1) - spacerItem1 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) - self.gridLayout_4.addItem(spacerItem1, 0, 1, 2, 1) - self.gridLayout_3.addWidget(self.groupBox_9, 2, 0, 1, 1) - self.groupBox_10 = QtGui.QGroupBox(self.tab_4) - self.groupBox_10.setObjectName("groupBox_10") - self.gridLayout_5 = QtGui.QGridLayout(self.groupBox_10) - self.gridLayout_5.setObjectName("gridLayout_5") - spacerItem2 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) - self.gridLayout_5.addItem(spacerItem2, 0, 1, 3, 1) - self.artist_tag_us_no = QtGui.QRadioButton(self.groupBox_10) - self.artist_tag_us_no.setObjectName("artist_tag_us_no") - self.gridLayout_5.addWidget(self.artist_tag_us_no, 0, 0, 1, 1) - self.label_21 = QtGui.QLabel(self.groupBox_10) - self.label_21.setLayoutDirection(QtCore.Qt.RightToLeft) - self.label_21.setObjectName("label_21") - self.gridLayout_5.addWidget(self.label_21, 0, 2, 1, 1) - self.artist_tags_weight = QtGui.QSpinBox(self.groupBox_10) - self.artist_tags_weight.setObjectName("artist_tags_weight") - self.gridLayout_5.addWidget(self.artist_tags_weight, 0, 3, 1, 1) - self.artist_tag_us_ex = QtGui.QRadioButton(self.groupBox_10) - self.artist_tag_us_ex.setObjectName("artist_tag_us_ex") - self.gridLayout_5.addWidget(self.artist_tag_us_ex, 1, 0, 1, 1) - self.label_22 = QtGui.QLabel(self.groupBox_10) - self.label_22.setLayoutDirection(QtCore.Qt.RightToLeft) - self.label_22.setObjectName("label_22") - self.gridLayout_5.addWidget(self.label_22, 1, 2, 1, 1) - self.min_artisttag_weight = QtGui.QSpinBox(self.groupBox_10) - self.min_artisttag_weight.setObjectName("min_artisttag_weight") - self.gridLayout_5.addWidget(self.min_artisttag_weight, 1, 3, 1, 1) - self.artist_tag_us_yes = QtGui.QRadioButton(self.groupBox_10) - self.artist_tag_us_yes.setObjectName("artist_tag_us_yes") - self.gridLayout_5.addWidget(self.artist_tag_us_yes, 2, 0, 1, 1) - self.label_23 = QtGui.QLabel(self.groupBox_10) - self.label_23.setLayoutDirection(QtCore.Qt.RightToLeft) - self.label_23.setObjectName("label_23") - self.gridLayout_5.addWidget(self.label_23, 2, 2, 1, 1) - self.max_artisttag_drop = QtGui.QSpinBox(self.groupBox_10) - self.max_artisttag_drop.setObjectName("max_artisttag_drop") - self.gridLayout_5.addWidget(self.max_artisttag_drop, 2, 3, 1, 1) - self.gridLayout_3.addWidget(self.groupBox_10, 3, 0, 1, 1) - self.tabWidget.addTab(self.tab_4, "") - self.tab_3 = QtGui.QWidget() - self.tab_3.setObjectName("tab_3") - self.gridLayout_7 = QtGui.QGridLayout(self.tab_3) - self.gridLayout_7.setObjectName("gridLayout_7") - self.groupBox_3 = QtGui.QGroupBox(self.tab_3) - self.groupBox_3.setObjectName("groupBox_3") - self.gridLayout = QtGui.QGridLayout(self.groupBox_3) - self.gridLayout.setObjectName("gridLayout") - self.label = QtGui.QLabel(self.groupBox_3) - self.label.setLayoutDirection(QtCore.Qt.LeftToRight) - self.label.setObjectName("label") - self.gridLayout.addWidget(self.label, 0, 0, 1, 1) - self.genre_major = QtGui.QLineEdit(self.groupBox_3) - self.genre_major.setObjectName("genre_major") - self.gridLayout.addWidget(self.genre_major, 0, 1, 1, 1) - self.label_2 = QtGui.QLabel(self.groupBox_3) - self.label_2.setLayoutDirection(QtCore.Qt.LeftToRight) - self.label_2.setObjectName("label_2") - self.gridLayout.addWidget(self.label_2, 1, 0, 1, 1) - self.genre_minor = QtGui.QLineEdit(self.groupBox_3) - self.genre_minor.setObjectName("genre_minor") - self.gridLayout.addWidget(self.genre_minor, 1, 1, 1, 1) - self.label_3 = QtGui.QLabel(self.groupBox_3) - self.label_3.setLayoutDirection(QtCore.Qt.LeftToRight) - self.label_3.setObjectName("label_3") - self.gridLayout.addWidget(self.label_3, 2, 0, 1, 1) - self.genre_mood = QtGui.QLineEdit(self.groupBox_3) - self.genre_mood.setObjectName("genre_mood") - self.gridLayout.addWidget(self.genre_mood, 2, 1, 1, 1) - self.label_5 = QtGui.QLabel(self.groupBox_3) - self.label_5.setLayoutDirection(QtCore.Qt.LeftToRight) - self.label_5.setObjectName("label_5") - self.gridLayout.addWidget(self.label_5, 3, 0, 1, 1) - self.genre_year = QtGui.QLineEdit(self.groupBox_3) - self.genre_year.setObjectName("genre_year") - self.gridLayout.addWidget(self.genre_year, 3, 1, 1, 1) - self.label_4 = QtGui.QLabel(self.groupBox_3) - self.label_4.setLayoutDirection(QtCore.Qt.LeftToRight) - self.label_4.setObjectName("label_4") - self.gridLayout.addWidget(self.label_4, 4, 0, 1, 1) - self.genre_occasion = QtGui.QLineEdit(self.groupBox_3) - self.genre_occasion.setObjectName("genre_occasion") - self.gridLayout.addWidget(self.genre_occasion, 4, 1, 1, 1) - self.label_6 = QtGui.QLabel(self.groupBox_3) - self.label_6.setLayoutDirection(QtCore.Qt.LeftToRight) - self.label_6.setObjectName("label_6") - self.gridLayout.addWidget(self.label_6, 5, 0, 1, 1) - self.genre_decade = QtGui.QLineEdit(self.groupBox_3) - self.genre_decade.setObjectName("genre_decade") - self.gridLayout.addWidget(self.genre_decade, 5, 1, 1, 1) - self.label_7 = QtGui.QLabel(self.groupBox_3) - self.label_7.setLayoutDirection(QtCore.Qt.LeftToRight) - self.label_7.setObjectName("label_7") - self.gridLayout.addWidget(self.label_7, 6, 0, 1, 1) - self.genre_country = QtGui.QLineEdit(self.groupBox_3) - self.genre_country.setObjectName("genre_country") - self.gridLayout.addWidget(self.genre_country, 6, 1, 1, 1) - self.label_9 = QtGui.QLabel(self.groupBox_3) - self.label_9.setLayoutDirection(QtCore.Qt.LeftToRight) - self.label_9.setObjectName("label_9") - self.gridLayout.addWidget(self.label_9, 7, 0, 1, 1) - self.genre_city = QtGui.QLineEdit(self.groupBox_3) - self.genre_city.setObjectName("genre_city") - self.gridLayout.addWidget(self.genre_city, 7, 1, 1, 1) - self.label_8 = QtGui.QLabel(self.groupBox_3) - self.label_8.setLayoutDirection(QtCore.Qt.LeftToRight) - self.label_8.setObjectName("label_8") - self.gridLayout.addWidget(self.label_8, 8, 0, 1, 1) - self.genre_category = QtGui.QLineEdit(self.groupBox_3) - self.genre_category.setObjectName("genre_category") - self.gridLayout.addWidget(self.genre_category, 8, 1, 1, 1) - self.gridLayout_7.addWidget(self.groupBox_3, 0, 0, 1, 2) - self.groupBox_17 = QtGui.QGroupBox(self.tab_3) - self.groupBox_17.setObjectName("groupBox_17") - self.horizontalLayout_4 = QtGui.QHBoxLayout(self.groupBox_17) - self.horizontalLayout_4.setObjectName("horizontalLayout_4") - self.genre_translations = QtGui.QTextEdit(self.groupBox_17) - self.genre_translations.setObjectName("genre_translations") - self.horizontalLayout_4.addWidget(self.genre_translations) - self.gridLayout_7.addWidget(self.groupBox_17, 1, 0, 1, 1) - self.groupBox_2 = QtGui.QGroupBox(self.tab_3) - self.groupBox_2.setObjectName("groupBox_2") - self.verticalLayout = QtGui.QVBoxLayout(self.groupBox_2) - self.verticalLayout.setObjectName("verticalLayout") - self.filter_report = QtGui.QPushButton(self.groupBox_2) - self.filter_report.setObjectName("filter_report") - self.verticalLayout.addWidget(self.filter_report) - self.check_word_lists = QtGui.QPushButton(self.groupBox_2) - self.check_word_lists.setObjectName("check_word_lists") - self.verticalLayout.addWidget(self.check_word_lists) - self.check_translation_list = QtGui.QPushButton(self.groupBox_2) - self.check_translation_list.setEnabled(False) - self.check_translation_list.setObjectName("check_translation_list") - self.verticalLayout.addWidget(self.check_translation_list) - spacerItem3 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) - self.verticalLayout.addItem(spacerItem3) - self.load_default_lists = QtGui.QPushButton(self.groupBox_2) - font = QtGui.QFont() - font.setWeight(75) - font.setBold(True) - self.load_default_lists.setFont(font) - self.load_default_lists.setObjectName("load_default_lists") - self.verticalLayout.addWidget(self.load_default_lists) - self.gridLayout_7.addWidget(self.groupBox_2, 1, 1, 1, 1) - self.tabWidget.addTab(self.tab_3, "") - self.horizontalLayout.addWidget(self.tabWidget) - - self.retranslateUi(LastfmOptionsPage) - self.tabWidget.setCurrentIndex(0) - QtCore.QMetaObject.connectSlotsByName(LastfmOptionsPage) - LastfmOptionsPage.setTabOrder(self.max_group_tags, self.max_minor_tags) - LastfmOptionsPage.setTabOrder(self.max_minor_tags, self.use_track_tags) - LastfmOptionsPage.setTabOrder(self.use_track_tags, self.min_tracktag_weight) - LastfmOptionsPage.setTabOrder(self.min_tracktag_weight, self.max_tracktag_drop) - LastfmOptionsPage.setTabOrder(self.max_tracktag_drop, self.artist_tag_us_no) - LastfmOptionsPage.setTabOrder(self.artist_tag_us_no, self.artist_tag_us_ex) - LastfmOptionsPage.setTabOrder(self.artist_tag_us_ex, self.artist_tag_us_yes) - LastfmOptionsPage.setTabOrder(self.artist_tag_us_yes, self.artist_tags_weight) - LastfmOptionsPage.setTabOrder(self.artist_tags_weight, self.min_artisttag_weight) - LastfmOptionsPage.setTabOrder(self.min_artisttag_weight, self.max_artisttag_drop) - LastfmOptionsPage.setTabOrder(self.max_artisttag_drop, self.genre_major) - LastfmOptionsPage.setTabOrder(self.genre_major, self.genre_minor) - LastfmOptionsPage.setTabOrder(self.genre_minor, self.genre_mood) - LastfmOptionsPage.setTabOrder(self.genre_mood, self.genre_year) - LastfmOptionsPage.setTabOrder(self.genre_year, self.genre_occasion) - LastfmOptionsPage.setTabOrder(self.genre_occasion, self.genre_decade) - LastfmOptionsPage.setTabOrder(self.genre_decade, self.genre_country) - LastfmOptionsPage.setTabOrder(self.genre_country, self.genre_category) - LastfmOptionsPage.setTabOrder(self.genre_category, self.genre_translations) - LastfmOptionsPage.setTabOrder(self.genre_translations, self.filter_report) - LastfmOptionsPage.setTabOrder(self.filter_report, self.check_word_lists) - LastfmOptionsPage.setTabOrder(self.check_word_lists, self.check_translation_list) - LastfmOptionsPage.setTabOrder(self.check_translation_list, self.load_default_lists) - - def retranslateUi(self, LastfmOptionsPage): - LastfmOptionsPage.setWindowTitle(QtGui.QApplication.translate("LastfmOptionsPage", "Form", None, QtGui.QApplication.UnicodeUTF8)) - self.tabWidget.setWindowTitle(QtGui.QApplication.translate("LastfmOptionsPage", "LastfmOptionsPage", None, QtGui.QApplication.UnicodeUTF8)) - self.groupBox_5.setTitle(QtGui.QApplication.translate("LastfmOptionsPage", "Max Tags Written 0=Disabled 1=One Tag 2+= Multiple Tags", None, QtGui.QApplication.UnicodeUTF8)) - self.label_10.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Major Tags - Group", None, QtGui.QApplication.UnicodeUTF8)) - self.max_group_tags.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Max Grouping (Major Genres) Tags

\n" -"

Tag Name: %GROUPING%

\n" -"

Top-level genres ex: Classical, Rock, Soundtracks

\n" -"

Written to Grouping tag. Can also be appended to

\n" -"

Genre tag if \'Append Major\' box (below) is checked.

", None, QtGui.QApplication.UnicodeUTF8)) - self.label_12.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Max Mood Tags", None, QtGui.QApplication.UnicodeUTF8)) - self.max_mood_tags.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Max Mood Tags ID3v2.4+ Only!

\n" -"

Tag: %MOOD%

\n" -"

How a track \'feels\'. ex: Happy, Introspective, Drunk

\n" -"

Note: The TMOO frame is only standard in ID3v2.4 tags.

\n" -"

For all other tags, Moods will be saved as a Comment.

", None, QtGui.QApplication.UnicodeUTF8)) - self.label_11.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Minor Tags - Genre", None, QtGui.QApplication.UnicodeUTF8)) - self.max_minor_tags.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Max Genre Tags

\n" -"

Tag: %GENRE%

\n" -"

These are specific, detailed genres. ex: Baroque, Classic Rock, Delta Blues

\n" -"

Set this to 1 if using this tag for file naming,

\n" -"

or if your player doesn\'t support multi-value tags

\n" -"

\n" -"

Consider setting this to 3+ if you use Genre

\n" -"

for searching in your music library.

", None, QtGui.QApplication.UnicodeUTF8)) - self.label_14.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Max Occasion Tags", None, QtGui.QApplication.UnicodeUTF8)) - self.max_occasion_tags.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Max Occasion Tags Nonstandard!

\n" -"

Tag: %Comment:Songs-db_Occasion%

\n" -"

Good situations to play a track, ex: Driving, Love, Party

\n" -"

Set to 2+ to increase this tag\'s usefulness.

", None, QtGui.QApplication.UnicodeUTF8)) - self.label_15.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Max Category Tags", None, QtGui.QApplication.UnicodeUTF8)) - self.max_category_tags.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Max Category Tags Nonstandard!

\n" -"

Tag: %Comment:Songs-db_Custom2%

\n" -"

Another Top-level grouping tag.

\n" -"

Contains tags like: Female Vocalists, Singer-Songwriter

", None, QtGui.QApplication.UnicodeUTF8)) - self.app_major2minor_tag.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Append Major to Minor Tags

\n" -"

This will prepend any Grouping tags

\n" -"

onto the Genre tag at tagging time. The effect is

\n" -"

that the Grouping Tag which is also the Major Genre

\n" -"

Becomes the First Genre in the List of Minor Genres

", None, QtGui.QApplication.UnicodeUTF8)) - self.app_major2minor_tag.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Prepend Major to Minor Tags", None, QtGui.QApplication.UnicodeUTF8)) - self.label_26.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Join Tags With", None, QtGui.QApplication.UnicodeUTF8)) - self.join_tags_sign.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

The Separator to use for Multi-Value tags

\n" -"

You may want to add a trailing space to

\n" -"

help with readability.

\n" -"

To use Separate Tags rather than a single

\n" -"

multi-value tag leave this field blank ie. no space at all.

", None, QtGui.QApplication.UnicodeUTF8)) - self.join_tags_sign.setText(QtGui.QApplication.translate("LastfmOptionsPage", ";", None, QtGui.QApplication.UnicodeUTF8)) - self.groupBox_4.setTitle(QtGui.QApplication.translate("LastfmOptionsPage", "Enable (Selected) or Disable (Not Selected) other Tags", None, QtGui.QApplication.UnicodeUTF8)) - self.use_country_tag.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Country Nonstandard!

\n" -"

Tag: %Comment:Songs-db_Custom2%

\n" -"

The country the artist or track is most

\n" -"

associated with. Will retrieve results using the Country

\n" -"

tag list on Tag Filter List Page

", None, QtGui.QApplication.UnicodeUTF8)) - self.use_country_tag.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Country", None, QtGui.QApplication.UnicodeUTF8)) - self.use_city_tag.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Country Nonstandard!

\n" -"

Tag: %Comment:Songs-db_Custom2%

\n" -"

The city or region the artist or track is most

\n" -"

associated with. If Enabled will use the most popular

\n" -"

tag in the City list on Tag Filter Options page. If

\n" -"

Country option has been selected as well the City tag

\n" -"

will be displayed second in the tag list.

", None, QtGui.QApplication.UnicodeUTF8)) - self.use_city_tag.setText(QtGui.QApplication.translate("LastfmOptionsPage", "City", None, QtGui.QApplication.UnicodeUTF8)) - self.use_year_tag.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Original Year Nonstandard!

\n" -"

Tag: %ID3:TDOR% or %ID3:TORY%

\n" -"

The year the song was created or most popular in. Quite often

\n" -"

this is the correct original release year of the track. The tag

\n" -"

written to is determined by the settings you have selected

\n" -"

in Picard Tag options. Ie. if ID3v2.3 is selected the original year tag

\n" -"

will be ID3:TORY rather than the default of ID3:TDOR

", None, QtGui.QApplication.UnicodeUTF8)) - self.use_year_tag.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Original Year", None, QtGui.QApplication.UnicodeUTF8)) - self.use_decade_tag.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Decade Nonstandard!

\n" -"

Tag: %Comment:Songs-db_Custom1%

\n" -"

The decade the song was created, ex: 1970s

\n" -"

This is based on the last fm tags first, if none found then

\n" -"

the originalyear tag, and then the release date of the album.

", None, QtGui.QApplication.UnicodeUTF8)) - self.use_decade_tag.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Decade", None, QtGui.QApplication.UnicodeUTF8)) - self.groupBox_9.setTitle(QtGui.QApplication.translate("LastfmOptionsPage", "Track Based Tags: Tags based on Track Title and Artist", None, QtGui.QApplication.UnicodeUTF8)) - self.use_track_tags.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Check this to use Track-based tags.

\n" -"

These are tags relevant to the song

", None, QtGui.QApplication.UnicodeUTF8)) - self.use_track_tags.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Use Track Based Tags", None, QtGui.QApplication.UnicodeUTF8)) - self.label_19.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Minimum Tag Weight", None, QtGui.QApplication.UnicodeUTF8)) - self.min_tracktag_weight.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

The minimum weight track-based tag

\n" -"

to use, in terms of popularity

", None, QtGui.QApplication.UnicodeUTF8)) - self.min_tracktag_weight.setSuffix(QtGui.QApplication.translate("LastfmOptionsPage", " %", None, QtGui.QApplication.UnicodeUTF8)) - self.label_20.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Maximum Inter-Tag Drop", None, QtGui.QApplication.UnicodeUTF8)) - self.max_tracktag_drop.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

The maximum allowed drop in relevance

\n" -"

for the tag to still be a match

", None, QtGui.QApplication.UnicodeUTF8)) - self.max_tracktag_drop.setSuffix(QtGui.QApplication.translate("LastfmOptionsPage", " %", None, QtGui.QApplication.UnicodeUTF8)) - self.groupBox_10.setTitle(QtGui.QApplication.translate("LastfmOptionsPage", "Artist Based Tags: Based on the Artist, not the Track Title.", None, QtGui.QApplication.UnicodeUTF8)) - self.artist_tag_us_no.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Select this to Never use Artist based tags

\n" -"

Be sure you have Use Track-Based Tags

\n" -"

checked, though.

", None, QtGui.QApplication.UnicodeUTF8)) - self.artist_tag_us_no.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Don\'t use Artist Tags", None, QtGui.QApplication.UnicodeUTF8)) - self.label_21.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Artist-Tags Weight", None, QtGui.QApplication.UnicodeUTF8)) - self.artist_tags_weight.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

How strongly Artist-based tags

\n" -"

are considered for inclusion

", None, QtGui.QApplication.UnicodeUTF8)) - self.artist_tags_weight.setSuffix(QtGui.QApplication.translate("LastfmOptionsPage", " %", None, QtGui.QApplication.UnicodeUTF8)) - self.artist_tag_us_ex.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Enabling this uses Artist-based tags only

\n" -"

if there aren\'t enough Track-based tags.

\n" -"

Default: Enabled

", None, QtGui.QApplication.UnicodeUTF8)) - self.artist_tag_us_ex.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Extend Track-Tags", None, QtGui.QApplication.UnicodeUTF8)) - self.label_22.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Minimum Tag Weight", None, QtGui.QApplication.UnicodeUTF8)) - self.min_artisttag_weight.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

The minimum weight Artist-based tag

\n" -"

to use, in terms of popularity

", None, QtGui.QApplication.UnicodeUTF8)) - self.min_artisttag_weight.setSuffix(QtGui.QApplication.translate("LastfmOptionsPage", " %", None, QtGui.QApplication.UnicodeUTF8)) - self.artist_tag_us_yes.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Enable this to Always use Artist-based tags

", None, QtGui.QApplication.UnicodeUTF8)) - self.artist_tag_us_yes.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Use Artist-Tags", None, QtGui.QApplication.UnicodeUTF8)) - self.label_23.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Maximum Inter-Tag Drop", None, QtGui.QApplication.UnicodeUTF8)) - self.max_artisttag_drop.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

The maximum allowed drop in relevance

\n" -"

for the tag to still be a match

", None, QtGui.QApplication.UnicodeUTF8)) - self.max_artisttag_drop.setSuffix(QtGui.QApplication.translate("LastfmOptionsPage", " %", None, QtGui.QApplication.UnicodeUTF8)) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_4), QtGui.QApplication.translate("LastfmOptionsPage", "General Options", None, QtGui.QApplication.UnicodeUTF8)) - self.groupBox_3.setTitle(QtGui.QApplication.translate("LastfmOptionsPage", "Tag Lists", None, QtGui.QApplication.UnicodeUTF8)) - self.label.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Grouping", None, QtGui.QApplication.UnicodeUTF8)) - self.genre_major.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Major Genres

\n" -"

Top-level genres ex: Classical, Rock, Soundtracks

\n" -"

Written to Grouping tag. Can also be appended to

\n" -"

Genre tag if enabled in General Options.

\n" -"

\n" -"

Tag Name: %GROUPING% ID3 Frame: TIT1

\n" -"

Supported: Mp3, Ogg, Flac, Wma, AAC

\n" -"

\n" -"

Compatibility Tag Mapping

\n" -"

______________________________________________________

\n" -"

Foobar YES CONTENT GROUP

\n" -"

iTunes YES GROUPING

\n" -"

MediaMonkey YES GROUPING

\n" -"

MP3Tag YES CONTENTGROUP

\n" -"

Winamp UNK ---

\n" -"

WMP YES MUSIC CATEGORY DESCRIPTION

\n" -"

", None, QtGui.QApplication.UnicodeUTF8)) - self.label_2.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Genres", None, QtGui.QApplication.UnicodeUTF8)) - self.genre_minor.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Minor Genres

\n" -"

More specific genres. ex: Baroque, Classic Rock, Delta Blues

\n" -"

Written to Genre tag.

\n" -"

\n" -"

Tag Name: %GENRE% ID3 Frame: TCON

\n" -"

Supported: Mp3, Ogg, Flac, Wma, AAC, Ape, Wav

\n" -"

\n" -"

Compatibility Tag Mapping

\n" -"

______________________________________________________

\n" -"

Foobar YES GENRE

\n" -"

iTunes YES GENRE

\n" -"

MediaMonkey YES GENRE

\n" -"

MP3Tag YES GENRE

\n" -"

Winamp YES GENRE

\n" -"

WMP YES GENRE

", None, QtGui.QApplication.UnicodeUTF8)) - self.label_3.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Mood", None, QtGui.QApplication.UnicodeUTF8)) - self.genre_mood.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Mood ID3v2.4+ Only!

\n" -"

How a track \'feels\'. ex: Happy, Introspective, Drunk

\n" -"

Note: The TMOO frame is only standard in ID3v2.4 tags.

\n" -"

For all other tags, Moods are saved as a Comment.

\n" -"

\n" -"

Tag Name: %MOOD% ID3 Frame: TMOO

\n" -"

Supported: Mp3, Ogg, Flac, Wma, AAC

\n" -"

\n" -"

Compatibility Tag Mapping

\n" -"

______________________________________________________

\n" -"

Foobar NO ---

\n" -"

iTunes UNK ---

\n" -"

MediaMonkey YES MOOD

\n" -"

MP3Tag YES MOOD

\n" -"

Winamp UNK ---

\n" -"

WMP YES WM/MOOD

\n" -"

", None, QtGui.QApplication.UnicodeUTF8)) - self.label_5.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Years", None, QtGui.QApplication.UnicodeUTF8)) - self.genre_year.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Original Year

\n" -"

The year the track was first recorded.

\n" -"

Note: This tag is often missing or wrong.

\n" -"

If Blank, the album release date is used.

\n" -"

\n" -"

Tag Name: %ORIGINALDATE%

\n" -"

ID3 Frame: V2.3: TORY v2.4: TDOR

\n" -"

Supported: Mp3, Ogg, Flac, Wma, AAC

\n" -"

\n" -"

Compatibility Tag Mapping

\n" -"

______________________________________________________

\n" -"

Foobar YES ORIGINAL RELEASE DATE

\n" -"

iTunes UNK ---

\n" -"

MediaMonkey YES ORIGINAL DATE

\n" -"

MP3Tag YES ORIGYEAR

\n" -"

Winamp UNK ---

\n" -"

WMP UNK ---

\n" -"

", None, QtGui.QApplication.UnicodeUTF8)) - self.label_4.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Occasion", None, QtGui.QApplication.UnicodeUTF8)) - self.genre_occasion.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Occasions Nonstandard!

\n" -"

Good times to play the track, ex: Driving, Love, Party

\n" -"

Written to the Comment tag. Has very limited support.

\n" -"

\n" -"

Tag Name: %Comment:Songs-db_Occasion%

\n" -"

ID3 Frame: COMM:Songs-db_Occasion

\n" -"

Supported: Mp3, Ogg, Flac

\n" -"

\n" -"

Compatibility Tag Mapping

\n" -"

______________________________________________________

\n" -"

Foobar NO ---

\n" -"

iTunes NO ---

\n" -"

MediaMonkey YES Custom tag

\n" -"

MP3Tag YES Comment:Songs-db_Occasion

\n" -"

Winamp NO ---

\n" -"

WMP UNK ---

", None, QtGui.QApplication.UnicodeUTF8)) - self.label_6.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Decades", None, QtGui.QApplication.UnicodeUTF8)) - self.genre_decade.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Decade Nonstandard!

\n" -"

The decade the song was created. Based on

\n" -"

originalyear, so will frequently be wrong.

\n" -"

Unless your app can map Comment subvalues

\n" -"

this tag will show as part of any existing comment.

\n" -"

\n" -"

Tag Name: %Comment:Songs-db_Custom1%

\n" -"

ID3 Frame: COMM:Songs-db_Custom1

\n" -"

Supported: Mp3, Ogg, Flac

\n" -"

\n" -"

Compatibility Tag Mapping

\n" -"

______________________________________________________

\n" -"

Foobar NO ---

\n" -"

iTunes NO ---

\n" -"

MediaMonkey YES Custom tag

\n" -"

MP3Tag YES Comment:Songs-db_Custom1

\n" -"

Winamp NO ---

\n" -"

WMP NO ---

", None, QtGui.QApplication.UnicodeUTF8)) - self.label_7.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Country", None, QtGui.QApplication.UnicodeUTF8)) - self.genre_country.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Country Nonstandard!

\n" -"

Artist country/location info, ex: America, New York, NYC

\n" -"

Allowing more tags will usually give more detailed info.

\n" -"

Unless your app maps Comment subvalues

\n" -"

this tag will show as part of any existing comment.

\n" -"

\n" -"

Tag Name: %Comment:Songs-db_Custom3%

\n" -"

ID3 Frame: COMM:Songs-db_Custom3

\n" -"

Supported: Mp3, Ogg, Flac

\n" -"

\n" -"

Compatibility Tag Mapping

\n" -"

______________________________________________________

\n" -"

Foobar NO ---

\n" -"

iTunes NO ---

\n" -"

MediaMonkey YES Custom tag

\n" -"

MP3Tag YES Comment:Songs-db_Custom3

\n" -"

Winamp NO ---

\n" -"

WMP NO ---

", None, QtGui.QApplication.UnicodeUTF8)) - self.label_9.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Cities", None, QtGui.QApplication.UnicodeUTF8)) - self.genre_city.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Country Nonstandard!

\n" -"

Artist country/location info, ex: America, New York, NYC

\n" -"

Allowing more tags will usually give more detailed info.

\n" -"

Unless your app maps Comment subvalues

\n" -"

this tag will show as part of any existing comment.

\n" -"

\n" -"

Tag Name: %Comment:Songs-db_Custom3%

\n" -"

ID3 Frame: COMM:Songs-db_Custom3

\n" -"

Supported: Mp3, Ogg, Flac

\n" -"

\n" -"

Compatibility Tag Mapping

\n" -"

______________________________________________________

\n" -"

Foobar NO ---

\n" -"

iTunes NO ---

\n" -"

MediaMonkey YES Custom tag

\n" -"

MP3Tag YES Comment:Songs-db_Custom3

\n" -"

Winamp NO ---

\n" -"

WMP NO ---

", None, QtGui.QApplication.UnicodeUTF8)) - self.label_8.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Category", None, QtGui.QApplication.UnicodeUTF8)) - self.genre_category.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Category Nonstandard!

\n" -"

Another Top-level grouping tag.

\n" -"

Returns terms like Female Vocalists, Singer-Songwriter

\n" -"

Unless your app can map Comment subvalues

\n" -"

this tag will show as part of any existing comment.

\n" -"

\n" -"

Tag Name: %Comment:Songs-db_Custom2%

\n" -"

ID3 Frame: COMM:Songs-db_Custom2

\n" -"

Supported: Mp3, Ogg, Flac

\n" -"

\n" -"

Compatibility Tag Mapping

\n" -"

______________________________________________________

\n" -"

Foobar NO ---

\n" -"

iTunes NO ---

\n" -"

MediaMonkey YES Custom tag

\n" -"

MP3Tag YES Comment:Songs-db_Custom2

\n" -"

Winamp NO ---

\n" -"

WMP NO ---

", None, QtGui.QApplication.UnicodeUTF8)) - self.groupBox_17.setTitle(QtGui.QApplication.translate("LastfmOptionsPage", "Tag Translations", None, QtGui.QApplication.UnicodeUTF8)) - self.genre_translations.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Tag Translations

\n" -"

This list lets you change how matches from the

\n" -"

tag lists are actually written into your tags.

\n" -"

\n" -"

Typical Uses:

\n" -"

- Standardize spellings, ex: rock-n-roll , rock and roll , rock \'n roll --> rock & roll

\n" -"

- Clean formatting, ex: lovesongs --> love songs

\n" -"

- Condense related tags, ex: heavy metal, hair metal, power metal --> metal

\n" -"

\n" -"

Usage: Old Name, New Name

\n" -"

One Rule per line:

\n" -"

\n" -"

Death Metal, Metal

\n" -"

Sunshine-Pop, Pop

\n" -"

Super-awesome-musics, Nice

", None, QtGui.QApplication.UnicodeUTF8)) - self.groupBox_2.setTitle(QtGui.QApplication.translate("LastfmOptionsPage", "Tools", None, QtGui.QApplication.UnicodeUTF8)) - self.filter_report.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Filter Report

\n" -"

Tells you how many tags and

\n" -"

translations you have in each list.

\n" -"

", None, QtGui.QApplication.UnicodeUTF8)) - self.filter_report.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Filter Report", None, QtGui.QApplication.UnicodeUTF8)) - self.check_word_lists.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Check Tag Lists

\n" -"

Each tag may appear only once across all lists.

\n" -"

This scans all the lists for duplicated tags

\n" -"

so you can easily remove them.

\n" -"

Run this whenever you add tags to a list!

\n" -"

", None, QtGui.QApplication.UnicodeUTF8)) - self.check_word_lists.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Check Tag Lists", None, QtGui.QApplication.UnicodeUTF8)) - self.check_translation_list.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

Not implemented yet.

", None, QtGui.QApplication.UnicodeUTF8)) - self.check_translation_list.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Translations", None, QtGui.QApplication.UnicodeUTF8)) - self.load_default_lists.setToolTip(QtGui.QApplication.translate("LastfmOptionsPage", "\n" -"\n" -"

WARNING!

\n" -"

This will overwrite all current

\n" -"

Tag Lists and Translations!

", None, QtGui.QApplication.UnicodeUTF8)) - self.load_default_lists.setText(QtGui.QApplication.translate("LastfmOptionsPage", "Load Defaults", None, QtGui.QApplication.UnicodeUTF8)) - self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_3), QtGui.QApplication.translate("LastfmOptionsPage", "Tag Filter Lists", None, QtGui.QApplication.UnicodeUTF8)) - From 42e91a94ba753cd885e52c15eff464666bd2ead5 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 7 Sep 2018 14:43:20 +0200 Subject: [PATCH 012/123] lastfm: Handle errors without crashing picard --- plugins/lastfm/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/lastfm/__init__.py b/plugins/lastfm/__init__.py index 2e279328..df2b3c2d 100644 --- a/plugins/lastfm/__init__.py +++ b/plugins/lastfm/__init__.py @@ -3,7 +3,7 @@ PLUGIN_NAME = 'Last.fm' PLUGIN_AUTHOR = 'Lukáš Lalinský, Philipp Wolfer' PLUGIN_DESCRIPTION = 'Use tags from Last.fm as genre.' -PLUGIN_VERSION = "0.6" +PLUGIN_VERSION = "0.7" PLUGIN_API_VERSIONS = ["2.0"] import re @@ -83,6 +83,11 @@ def _tags_finalize(album, metadata, tags, next_): def _tags_downloaded(album, metadata, min_usage, ignore, next_, current, data, reply, error): + if error: + album._requests -= 1 + album._finalize_loading(None) + return + try: try: intags = data.lfm[0].toptags[0].tag @@ -116,7 +121,6 @@ def _tags_downloaded(album, metadata, min_usage, ignore, next_, current, data, except Exception: log.error('Problem processing download tags', exc_info=True) - raise finally: album._requests -= 1 album._finalize_loading(None) From d91481cbebcd449f1f68fedb58721c3ec5bef628 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 10 Sep 2018 16:32:42 +0200 Subject: [PATCH 013/123] lastfm: Fix for Python < 3.7 --- plugins/lastfm/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/lastfm/__init__.py b/plugins/lastfm/__init__.py index df2b3c2d..519566c4 100644 --- a/plugins/lastfm/__init__.py +++ b/plugins/lastfm/__init__.py @@ -3,7 +3,7 @@ PLUGIN_NAME = 'Last.fm' PLUGIN_AUTHOR = 'Lukáš Lalinský, Philipp Wolfer' PLUGIN_DESCRIPTION = 'Use tags from Last.fm as genre.' -PLUGIN_VERSION = "0.7" +PLUGIN_VERSION = "0.8" PLUGIN_API_VERSIONS = ["2.0"] import re @@ -60,7 +60,7 @@ def parse_ignored_tags(ignore_tags_setting): def matches_ignored(ignore_tags, tag): tag = tag.lower().strip() for pattern in ignore_tags: - if isinstance(pattern, re.Pattern): + if hasattr(pattern, 'match'): match = pattern.match(tag) else: match = pattern == tag From 3d79651f207bcee7f7540c3beb622020c2343b47 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 10 Sep 2018 16:47:16 +0200 Subject: [PATCH 014/123] lastfm: Clear cache when config changes --- plugins/lastfm/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/lastfm/__init__.py b/plugins/lastfm/__init__.py index 519566c4..f8c725d7 100644 --- a/plugins/lastfm/__init__.py +++ b/plugins/lastfm/__init__.py @@ -232,7 +232,11 @@ def load(self): self.ui.join_tags.setEditText(setting["lastfm_join_tags"]) def save(self): + global _cache setting = config.setting + if setting["lastfm_min_tag_usage"] != self.ui.min_tag_usage.value() \ + or setting["lastfm_ignore_tags"] != str(self.ui.ignore_tags.text()): + _cache = {} setting["lastfm_use_track_tags"] = self.ui.use_track_tags.isChecked() setting["lastfm_use_artist_tags"] = self.ui.use_artist_tags.isChecked() setting["lastfm_min_tag_usage"] = self.ui.min_tag_usage.value() From 365c92efdf0fc6f9122331bbb31e173b5054ce2e Mon Sep 17 00:00:00 2001 From: Sophist Date: Sat, 9 Dec 2017 23:30:56 +0000 Subject: [PATCH 015/123] Make albumartistsort consistent with albumartist --- plugins/soundtrack/soundtrack.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/soundtrack/soundtrack.py b/plugins/soundtrack/soundtrack.py index 3afb9958..f13168fa 100644 --- a/plugins/soundtrack/soundtrack.py +++ b/plugins/soundtrack/soundtrack.py @@ -19,5 +19,6 @@ def soundtrack(tagger, metadata, release): if "soundtrack" in metadata["releasetype"]: metadata["albumartist"] = "Soundtrack" + metadata["albumartistsort"] = "Soundtrack" register_album_metadata_processor(soundtrack) From e231e864ebbc883c6797968de7770f1aeb21b3c7 Mon Sep 17 00:00:00 2001 From: Bob Swift Date: Tue, 11 Sep 2018 13:10:36 -0600 Subject: [PATCH 016/123] Update smart_title_case plugin for Picard 2.0 --- plugins/smart_title_case/smart_title_case.py | 135 +++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 plugins/smart_title_case/smart_title_case.py diff --git a/plugins/smart_title_case/smart_title_case.py b/plugins/smart_title_case/smart_title_case.py new file mode 100644 index 00000000..7c30b589 --- /dev/null +++ b/plugins/smart_title_case/smart_title_case.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# This is the Smart Title Case plugin for MusicBrainz Picard. +# Copyright (C) 2017 Sophist. +# +# Updated for use with Picard v2.0 by Bob Swift (rdswift). +# +# It is based on the Title Case plugin by Javier Kohen +# Copyright 2007 Javier Kohen +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +PLUGIN_NAME = "Smart Title Case" +PLUGIN_AUTHOR = "Sophist based on an earlier plugin by Javier Kohen" +PLUGIN_DESCRIPTION = """ +Capitalize First Character In Every Word Of Album/Track Title/Artist.
+Leaves words containing embedded uppercase as-is i.e. USA or DoA.
+For Artist/AlbumArtist, title cases only artists not join phrases
+e.g. The Beatles feat. The Who. +""" +PLUGIN_VERSION = "0.1" +PLUGIN_API_VERSIONS = ["2.0"] +PLUGIN_LICENSE = "GPL-3.0" +PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-3.0.html" + +import re, unicodedata + +title_tags = ['title', 'album'] +artist_tags = [ + ('artist', 'artists'), + ('artistsort', '~artists_sort'), + ('albumartist', '~albumartists'), + ('albumartistsort', '~albumartists_sort'), + ] +title_re = re.compile(r'\w[^-,/\s\u2010\u2011]*') + +def match_word(match): + word = match.group(0) + if word == word.lower(): + word = word[0].upper() + word[1:] + return word + +def string_title_match(match_word, string): + return title_re.sub(match_word, string) + +def string_cleanup(string): + if not string: + return "" + return unicodedata.normalize("NFKC", string) + +def string_title_case(string): + """Title-case a string using a less destructive method than str.title.""" + return string_title_match(match_word, string_cleanup(string)) + + +assert "Make Title Case" == string_title_case("make title case") +assert "Already Title Case" == string_title_case("Already Title Case") +assert "mIxEd cAsE" == string_title_case("mIxEd cAsE") +assert "A" == string_title_case("a") +assert "Apostrophe's Apostrophe's" == string_title_case("apostrophe's apostrophe's") +assert "(Bracketed Text)" == string_title_case("(bracketed text)") +assert "'Single Quotes'" == string_title_case("'single quotes'") +assert '"Double Quotes"' == string_title_case('"double quotes"') +assert "A,B" == string_title_case("a,b") +assert "A-B" == string_title_case("a-b") +assert "A/B" == string_title_case("a/b") +assert "Flügel" == string_title_case("flügel") +assert "HARVEST STORY By 杉山清貴" == string_title_case("HARVEST STORY by 杉山清貴") + + +def artist_title_case(text, artists, artists_upper): + """ + Use the array of artists and the joined string + to identify artists to make title case + and the join strings to leave as-is. + """ + find = "^(" + r")(\s+\S+?\s+)(".join((map(re.escape, map(string_cleanup,artists)))) + ")(.*$)" + replace = "".join([r"%s\%d" % (a, x*2 + 2) for x, a in enumerate(artists_upper)]) + result = re.sub(find, replace, string_cleanup(text), re.UNICODE) + return result + +assert "The Beatles feat. The Who" == artist_title_case( + "the beatles feat. the who", + ["the beatles", "the who"], + ["The Beatles", "The Who"] + ) + + +# Put this here so that above unit tests can run standalone before getting an import error +from picard import log +from picard.metadata import ( + register_track_metadata_processor, + register_album_metadata_processor, +) + +def title_case(tagger, metadata, release, track=None): + for name in title_tags: + if name in metadata: + values = metadata.getall(name) + new_values = [string_title_case(v) for v in values] + if values != new_values: + log.debug("SmartTitleCase: %s: %r replaced with %r", name, values, new_values) + metadata[name] = new_values + for artist_string, artists_list in artist_tags: + if artist_string in metadata and artists_list in metadata: + artist = metadata.getall(artist_string) + artists = metadata.getall(artists_list) + new_artists = map(string_title_case, artists) + new_artist = [artist_title_case(x, artists, new_artists) for x in artist] + if artists != new_artists and artist != new_artist: + log.debug("SmartTitleCase: %s: %s replaced with %s", artist_string, artist, new_artist) + log.debug("SmartTitleCase: %s: %r replaced with %r", artists_list, artists, new_artists) + metadata[artist_string] = new_artist + metadata[artists_list] = new_artists + elif artists != new_artists or artist != new_artist: + if artists != new_artists: + log.warning("SmartTitleCase: %s changed, %s wasn't", artists_list, artist_string) + log.warning("SmartTitleCase: %s: %r changed to %r", artists_list, artists, new_artists) + log.warning("SmartTitleCase: %s: %r unchanged", artist_string, artist) + else: + log.warning("SmartTitleCase: %s changed, %s wasn't", artist_string, artists_list) + log.warning("SmartTitleCase: %s: %r changed to %r", artist_string, artist, new_artist) + log.warning("SmartTitleCase: %s: %r unchanged", artists_list, artists) + +register_track_metadata_processor(title_case) +register_album_metadata_processor(title_case) From 9ff2bcbb739b087911aa6977b3ead472bf4796ea Mon Sep 17 00:00:00 2001 From: Bob Swift Date: Wed, 12 Sep 2018 16:00:55 -0600 Subject: [PATCH 017/123] Comment out the unit testing statements --- plugins/smart_title_case/smart_title_case.py | 45 +++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/plugins/smart_title_case/smart_title_case.py b/plugins/smart_title_case/smart_title_case.py index 7c30b589..22b45973 100644 --- a/plugins/smart_title_case/smart_title_case.py +++ b/plugins/smart_title_case/smart_title_case.py @@ -62,21 +62,6 @@ def string_title_case(string): return string_title_match(match_word, string_cleanup(string)) -assert "Make Title Case" == string_title_case("make title case") -assert "Already Title Case" == string_title_case("Already Title Case") -assert "mIxEd cAsE" == string_title_case("mIxEd cAsE") -assert "A" == string_title_case("a") -assert "Apostrophe's Apostrophe's" == string_title_case("apostrophe's apostrophe's") -assert "(Bracketed Text)" == string_title_case("(bracketed text)") -assert "'Single Quotes'" == string_title_case("'single quotes'") -assert '"Double Quotes"' == string_title_case('"double quotes"') -assert "A,B" == string_title_case("a,b") -assert "A-B" == string_title_case("a-b") -assert "A/B" == string_title_case("a/b") -assert "Flügel" == string_title_case("flügel") -assert "HARVEST STORY By 杉山清貴" == string_title_case("HARVEST STORY by 杉山清貴") - - def artist_title_case(text, artists, artists_upper): """ Use the array of artists and the joined string @@ -85,14 +70,32 @@ def artist_title_case(text, artists, artists_upper): """ find = "^(" + r")(\s+\S+?\s+)(".join((map(re.escape, map(string_cleanup,artists)))) + ")(.*$)" replace = "".join([r"%s\%d" % (a, x*2 + 2) for x, a in enumerate(artists_upper)]) - result = re.sub(find, replace, string_cleanup(text), re.UNICODE) + result = re.sub(find, replace, string_cleanup(text)) return result -assert "The Beatles feat. The Who" == artist_title_case( - "the beatles feat. the who", - ["the beatles", "the who"], - ["The Beatles", "The Who"] - ) + +################################################ +# Uncomment the following to enable unit tests # +################################################ +# +# assert "Make Title Case" == string_title_case("make title case") +# assert "Already Title Case" == string_title_case("Already Title Case") +# assert "mIxEd cAsE" == string_title_case("mIxEd cAsE") +# assert "A" == string_title_case("a") +# assert "Apostrophe's Apostrophe's" == string_title_case("apostrophe's apostrophe's") +# assert "(Bracketed Text)" == string_title_case("(bracketed text)") +# assert "'Single Quotes'" == string_title_case("'single quotes'") +# assert '"Double Quotes"' == string_title_case('"double quotes"') +# assert "A,B" == string_title_case("a,b") +# assert "A-B" == string_title_case("a-b") +# assert "A/B" == string_title_case("a/b") +# assert "Flügel" == string_title_case("flügel") +# assert "HARVEST STORY By 杉山清貴" == string_title_case("HARVEST STORY by 杉山清貴") +# assert "The Beatles feat. The Who" == artist_title_case( +# "the beatles feat. the who", +# ["the beatles", "the who"], +# ["The Beatles", "The Who"] +# ) # Put this here so that above unit tests can run standalone before getting an import error From 8dd7ca53aab52302e93c266e3c72514983f4bf13 Mon Sep 17 00:00:00 2001 From: Bob Swift Date: Thu, 13 Sep 2018 09:51:51 -0600 Subject: [PATCH 018/123] Update version number --- plugins/smart_title_case/smart_title_case.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/smart_title_case/smart_title_case.py b/plugins/smart_title_case/smart_title_case.py index 22b45973..449c5914 100644 --- a/plugins/smart_title_case/smart_title_case.py +++ b/plugins/smart_title_case/smart_title_case.py @@ -27,7 +27,7 @@ For Artist/AlbumArtist, title cases only artists not join phrases
e.g. The Beatles feat. The Who. """ -PLUGIN_VERSION = "0.1" +PLUGIN_VERSION = "0.2" PLUGIN_API_VERSIONS = ["2.0"] PLUGIN_LICENSE = "GPL-3.0" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-3.0.html" From 37684f40a9710f625202706714992ca5b5fb6115 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 11 Sep 2018 08:48:54 +0200 Subject: [PATCH 019/123] soundtrack: Bump version due to changes --- plugins/soundtrack/soundtrack.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/soundtrack/soundtrack.py b/plugins/soundtrack/soundtrack.py index f13168fa..6fb3c3b2 100644 --- a/plugins/soundtrack/soundtrack.py +++ b/plugins/soundtrack/soundtrack.py @@ -10,7 +10,7 @@ PLUGIN_LICENSE = 'WTFPL' PLUGIN_LICENSE_URL = 'http://www.wtfpl.net/' PLUGIN_DESCRIPTION = '''Sets the albumartist to "Soundtrack" if releasetype is a soundtrack.''' -PLUGIN_VERSION = "0.1" +PLUGIN_VERSION = "0.2" PLUGIN_API_VERSIONS = ["1.0", "2.0"] from picard.metadata import register_album_metadata_processor From 8968e912bf25877774eeeeaf9874c1a36dff66e5 Mon Sep 17 00:00:00 2001 From: hoxia Date: Sun, 11 Jun 2017 14:03:59 -0400 Subject: [PATCH 020/123] featartistsintitles.py: also remove feat. artists from sort tags Add matches for sort tags, so these are modified when the corresponding metadata tags are cleaned of featured artists. --- plugins/featartistsintitles/featartistsintitles.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/featartistsintitles/featartistsintitles.py b/plugins/featartistsintitles/featartistsintitles.py index 648675a9..4fb6dd9e 100644 --- a/plugins/featartistsintitles/featartistsintitles.py +++ b/plugins/featartistsintitles/featartistsintitles.py @@ -1,7 +1,7 @@ PLUGIN_NAME = 'Feat. Artists in Titles' -PLUGIN_AUTHOR = 'Lukas Lalinsky, Michael Wiencek, Bryan Toth' +PLUGIN_AUTHOR = 'Lukas Lalinsky, Michael Wiencek, Bryan Toth, JeromyNix (NobahdiAtoll)' PLUGIN_DESCRIPTION = 'Move "feat." from artist names to album and track titles. Match is case insensitive.' -PLUGIN_VERSION = "0.3" +PLUGIN_VERSION = "0.4" PLUGIN_API_VERSIONS = ["0.9.0", "0.10", "0.15", "0.16", "2.0"] from picard.metadata import register_album_metadata_processor, register_track_metadata_processor @@ -15,6 +15,9 @@ def move_album_featartists(tagger, metadata, release): if match: metadata["albumartist"] = match.group(1) metadata["album"] += " (feat.%s)" % match.group(2) + match = _feat_re.match(metadata["albumartistsort"]) + if match: + metadata["albumartistsort"] = match.group(1) def move_track_featartists(tagger, metadata, release, track): @@ -22,6 +25,9 @@ def move_track_featartists(tagger, metadata, release, track): if match: metadata["artist"] = match.group(1) metadata["title"] += " (feat.%s)" % match.group(2) + match = _feat_re.match(metadata["artistsort"]) + if match: + metadata["artistsort"] = match.group(1) register_album_metadata_processor(move_album_featartists) register_track_metadata_processor(move_track_featartists) From ed6564a5e2b08a25f5fd9d3d34040c5a8aaa78da Mon Sep 17 00:00:00 2001 From: hoxia Date: Mon, 17 Sep 2018 08:27:12 -0400 Subject: [PATCH 021/123] Merge 1.0 branch debug cleanup by @mineo into 2.0 Merges two commits (7ddadcd92366c3038c19dd083c5f641a6deba696, bbd4b919c840be78eb6ac8dd138a0fd86a7f5e1f) by @mineo made against 1.0 branch into 2.0. These commits clean up debug code, and reportedly fix an error which renders the plugin nonfunctional. This merge was suggested in #166, however I have not tested it and the changes should be reviewed with particular attention to the API bump. I've also incremented the plugin version to reflect the changes. --- .../abbreviate_artistsort.py | 39 ++++++------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/plugins/abbreviate_artistsort/abbreviate_artistsort.py b/plugins/abbreviate_artistsort/abbreviate_artistsort.py index d15f9937..a26392e2 100644 --- a/plugins/abbreviate_artistsort/abbreviate_artistsort.py +++ b/plugins/abbreviate_artistsort/abbreviate_artistsort.py @@ -13,7 +13,6 @@ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. -from __future__ import print_function PLUGIN_NAME = "Abbreviate artist-sort" PLUGIN_AUTHOR = "Sophist" PLUGIN_DESCRIPTION = '''Abbreviate Artist-Sort and Album-Artist-Sort Tags. @@ -21,7 +20,7 @@ This is particularly useful for classical albums that can have a long list of artists. %artistsort% is abbreviated into %_artistsort_abbrev% and %albumartistsort% is abbreviated into %_albumartistsort_abbrev%.''' -PLUGIN_VERSION = "0.2" +PLUGIN_VERSION = "0.3" PLUGIN_API_VERSIONS = ["1.0", "2.0"] PLUGIN_LICENSE = "GPL-2.0" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" @@ -70,7 +69,6 @@ # Case c. If first word is same in sorted and unsorted, move words that match to new strings, then treat as b. # Case d. Try to handle without abbreviating and get to next name which might not be foreign -_debug_level = 0 _abbreviate_tags = [ ('albumartistsort', 'albumartist', '~albumartistsort_abbrev'), ('artistsort', 'artist', '~artistsort_abbrev'), @@ -90,11 +88,9 @@ def abbreviate_artistsort(tagger, metadata, track, release): unsorts = list(metadata.getall(unsortTag)) for i in range(0, min(len(sorts), len(unsorts))): sort = sorts[i] - if _debug_level > 1: - print("%s: Trying to abbreviate '%s'." % (PLUGIN_NAME, sort)) + log.debug("%s: Trying to abbreviate '%s'." % (PLUGIN_NAME, sort)) if sort in _abbreviate_cache: - if _debug_level > 3: - print(" Using abbreviation found in cache: '%s'." % (_abbreviate_cache[sort])) + log.debug(" Using abbreviation found in cache: '%s'." % (_abbreviate_cache[sort])) sorts[i] = _abbreviate_cache[sort] continue unsort = unsorts[i] @@ -104,8 +100,7 @@ def abbreviate_artistsort(tagger, metadata, track, release): while len(sort) > 0 and len(unsort) > 0: if not _split in sort: - if _debug_level > 3: - print(" Ending without separator '%s' - moving '%s'." % (_split, sort)) + log.debug(" Ending without separator '%s' - moving '%s'." % (_split, sort)) new_sort += sort new_unsort += unsort sort = unsort = "" @@ -113,8 +108,7 @@ def abbreviate_artistsort(tagger, metadata, track, release): surname, rest = sort.split(_split, 1) if rest == "": - if _debug_level > 3: - print(" Ending with separator '%s' - moving '%s'." % (_split, surname)) + log.debug(" Ending with separator '%s' - moving '%s'." % (_split, surname)) new_sort += sort new_unsort += unsort sort = unsort = "" @@ -129,8 +123,7 @@ def abbreviate_artistsort(tagger, metadata, track, release): temp = surname + _split l = len(temp) if unsort[:l] == temp: - if _debug_level > 3: - print(" No forename - moving '%s'." % (surname)) + log.debug(" No forename - moving '%s'." % (surname)) new_sort += temp new_unsort += temp sort = sort[l:] @@ -143,8 +136,7 @@ def abbreviate_artistsort(tagger, metadata, track, release): if unsort.find(' ' + surname) == -1: while surname.split(None, 1)[0] == unsort.split(None, 1)[0]: x = unsort.split(None, 1)[0] - if _debug_level > 3: - print(" Moving matching word '%s'." % (x)) + log.debug(" Moving matching word '%s'." % (x)) new_sort += x new_unsort += x surname = surname[len(x):] @@ -165,8 +157,7 @@ def abbreviate_artistsort(tagger, metadata, track, release): unsortTag, unsort[i], ) - if _debug_level > 0: - print(" Could not match surname (%s) in remaining unsorted:" % (surname, unsort)) + log.warning(" Could not match surname (%s) in remaining unsorted:" % (surname, unsort)) break # Sorted: Surname, Forename(s)... @@ -182,8 +173,7 @@ def abbreviate_artistsort(tagger, metadata, track, release): unsortTag, unsort[i], ) - if _debug_level > 0: - print(" Could not match forename (%s) for surname (%s) in remaining unsorted (%s):" % (forename, surname, unsort)) + log.warning(" Could not match forename (%s) for surname (%s) in remaining unsorted (%s):" % (forename, surname, unsort)) break inits = ' '.join([x[0] + '.' for x in forename.split()]) @@ -216,8 +206,7 @@ def abbreviate_artistsort(tagger, metadata, track, release): inits, sortTag, ) - if _debug_level > 2: - print("Abbreviated (%s, %s) to (%s, %s)." % (surname, forename, surname, inits)) + log.debug("Abbreviated (%s, %s) to (%s, %s)." % (surname, forename, surname, inits)) else: # while loop ended without a break i.e. no errors if unsorts[i] != new_unsort: log.error( @@ -228,13 +217,9 @@ def abbreviate_artistsort(tagger, metadata, track, release): unsorts[i], new_unsort, ) - if _debug_level > 0: - print() - print("Error: Unsorted text for %s has changed from '%s' to '%s'!" % (unsortTag, unsorts[i], new_unsort)) - print() + log.warning("Error: Unsorted text for %s has changed from '%s' to '%s'!" % (unsortTag, unsorts[i], new_unsort)) _abbreviate_cache[sorts[i]] = new_sort - if _debug_level > 1: - print(" Abbreviated and cached (%s) as (%s)." % (sorts[i], new_sort)) + log.debug(" Abbreviated and cached (%s) as (%s)." % (sorts[i], new_sort)) if sorts[i] != new_sort: log.debug(_("%s: Abbreviated tag '%s' to '%s'."), PLUGIN_NAME, From 6a0e297ac3869c2f56aeab44536b7c485d94b71c Mon Sep 17 00:00:00 2001 From: Daniel Sobey Date: Sun, 7 Oct 2018 12:40:19 +1030 Subject: [PATCH 022/123] Hoefully this should fix the problems with the plugin. The problems come from not tracking how many outstanding requests there are currently and only when there are no outstanding requests. When there are more than one wikidata link for example this would get out of sync and cause problems. This should hopefully fix this. --- plugins/wikidata/wikidata.py | 83 ++++++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 33 deletions(-) diff --git a/plugins/wikidata/wikidata.py b/plugins/wikidata/wikidata.py index 4c8dec35..bd4cd7a6 100644 --- a/plugins/wikidata/wikidata.py +++ b/plugins/wikidata/wikidata.py @@ -8,7 +8,7 @@ PLUGIN_NAME = 'wikidata-genre' PLUGIN_AUTHOR = 'Daniel Sobey, Sambhav Kothari' PLUGIN_DESCRIPTION = 'query wikidata to get genre tags' -PLUGIN_VERSION = '1.0' +PLUGIN_VERSION = '1.1' PLUGIN_API_VERSIONS = ["2.0"] PLUGIN_LICENSE = 'WTFPL' PLUGIN_LICENSE_URL = 'http://www.wtfpl.net/' @@ -23,13 +23,18 @@ class wikidata: def __init__(self): self.lock = threading.Lock() - # active request queue + # Key: mbid, value: List of metadata entries to be updated when we have parsed everything self.requests = {} + + + # Key: mbid, value: List of items to track the number of outstanding requests self.albums = {} - # cache + # cache, items that have been found + # key: mbid, value: list of strings containing the genre's self.cache = {} - + + # not used def process_release(self, album, metadata, release): self.ws = album.tagger.webservice @@ -42,7 +47,14 @@ def process_release(self, album, metadata, release): item_id = artist log.info('WIKIDATA: processing release artist %s' % item_id) self.process_request(metadata, album, item_id, type='artist') - + + # Main processing function + # First see if we have already found what we need in the cache, finalize loading + # Next see if we are already looking for the item + # If we are add this item to the list of items to be updated once we find what we are looking for. + # Otherwise we are the first one to look up this item, start a new request + # metadata, map containing the new metadata + # def process_request(self, metadata, album, item_id, type): with self.lock: log.debug('WIKIDATA: Looking up cache for item %s' % item_id) @@ -54,33 +66,34 @@ def process_request(self, metadata, album, item_id, type): new_genre = set(metadata.getall("genre")) new_genre.update(genre_list) metadata["genre"] = list(new_genre) - - album._finalize_loading(None) + if album._requests == 0: + album._finalize_loading(None) return else: # pending requests are handled by adding the metadata object to a # list of things to be updated when the genre is found - if item_id in list(self.requests.keys()): + if item_id in list(self.albums.keys()): log.debug( 'WIKIDATA: request already pending, add it to the list of items to update once this has been found') self.requests[item_id].append(metadata) album._requests += 1 self.albums[item_id].append(album) - return - self.requests[item_id] = [metadata] - album._requests += 1 - self.albums[item_id] = [album] - log.debug('WIKIDATA: first request for this item') + else: + self.requests[item_id]=[metadata] + album._requests += 1 + self.albums[item_id]=[album] - log.info('WIKIDATA: about to call musicbrainz to look up %s ' % item_id) - # find the wikidata url if this exists - host = config.setting["server_host"] - port = config.setting["server_port"] + log.debug('WIKIDATA: first request for this item') - path = '/ws/2/%s/%s' % (type, item_id) - queryargs = {"inc": "url-rels"} - self.ws.get(host, port, path, + log.info('WIKIDATA: about to call musicbrainz to look up %s ' % item_id) + # find the wikidata url if this exists + host = config.setting["server_host"] + port = config.setting["server_port"] + + path = '/ws/2/%s/%s' % (type, item_id) + queryargs = {"inc": "url-rels"} + self.ws.get(host, port, path, partial(self.musicbrainz_release_lookup, item_id, metadata), parse_response_type="xml", priority=False, @@ -97,7 +110,7 @@ def musicbrainz_release_lookup(self, item_id, metadata, response, reply, error): for relation in response.metadata[0].release_group[0].relation_list[0].relation: if relation.type == 'wikidata' and 'target' in relation.children: found = True - wikidata_url = relation.target[0].text + wikidata_url = relation.target[0].text self.process_wikidata(wikidata_url, item_id) if 'artist' in response.metadata[0].children: if 'relation_list' in response.metadata[0].artist[0].children: @@ -106,7 +119,6 @@ def musicbrainz_release_lookup(self, item_id, metadata, response, reply, error): found = True wikidata_url = relation.target[0].text self.process_wikidata(wikidata_url, item_id) - if 'work' in response.metadata[0].children: if 'relation_list' in response.metadata[0].work[0].children: for relation in response.metadata[0].work[0].relation_list[0].relation: @@ -115,16 +127,21 @@ def musicbrainz_release_lookup(self, item_id, metadata, response, reply, error): wikidata_url = relation.target[0].text self.process_wikidata(wikidata_url, item_id) if not found: - log.info('WIKIDATA: no wikidata url') - with self.lock: - for album in self.albums[item_id]: - album._requests -= 1 + log.info('WIKIDATA: no wikidata url found for item_id: %s ', item_id) + with self.lock: + for album in self.albums[item_id]: + album._requests -= 1 + log.debug('WIKIDATA: TOTAL REMAINING REQUESTS %s' % + album._requests) + if album._requests == 0 : + self.albums[item_id].remove(album) album._finalize_loading(None) - log.debug('WIKIDATA: TOTAL REMAINING REQUESTS %s' % - album._requests) - del self.requests[item_id] + #del self.requests[item_id] def process_wikidata(self, wikidata_url, item_id): + with self.lock: + for album in self.albums[item_id]: + album._requests += 1 item = wikidata_url.split('/')[4] path = "/wiki/Special:EntityData/" + item + ".rdf" log.info('WIKIDATA: fetching the folowing url wikidata.org%s' % path) @@ -187,10 +204,10 @@ def parse_wikidata_response(self, item, item_id, response, reply, error): for album in self.albums[item_id]: album._requests -= 1 - album._finalize_loading(None) - log.info('WIKIDATA: TOTAL REMAINING REQUESTS %s' % - album._requests) - del self.requests[item_id] + if album._requests == 0 : + self.albums[item_id].remove(album) + album._finalize_loading(None) + log.info('WIKIDATA: TOTAL REMAINING REQUESTS %s' % album._requests) def process_track(self, album, metadata, trackXmlNode, releaseXmlNode): self.ws = album.tagger.webservice From c5c8130bc8af53f5504a1c33ed4709c7fe81d640 Mon Sep 17 00:00:00 2001 From: Bob Swift Date: Sat, 6 Oct 2018 16:48:46 -0600 Subject: [PATCH 023/123] Show 'guest, 'solo' and 'additional' on all related performer tags --- .../standardise_performers/standardise_performers.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins/standardise_performers/standardise_performers.py b/plugins/standardise_performers/standardise_performers.py index 484d3f99..6431f318 100644 --- a/plugins/standardise_performers/standardise_performers.py +++ b/plugins/standardise_performers/standardise_performers.py @@ -22,7 +22,7 @@ Performer [tambourine]: Graham Gouldman ''' -PLUGIN_VERSION = '0.3' +PLUGIN_VERSION = '0.4' PLUGIN_API_VERSIONS = ["0.15.0", "0.15.1", "0.16.0", "1.0.0", "1.1.0", "1.2.0", "1.3.0", "2.0"] PLUGIN_LICENSE = "GPL-2.0" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" @@ -49,8 +49,16 @@ def standardise_performers(album, metadata, *args): PLUGIN_NAME, subkey, ) + extra = '' + temp = '' + for part in instruments[0].split(): + if part in ['guest', 'solo', 'additional']: + extra = "{0}{1} ".format(extra, part) + else: + temp = "{0} {1}".format(temp, part).strip() + instruments[0] = temp for instrument in instruments: - newkey = '%s:%s' % (mainkey, instrument) + newkey = '%s:%s%s' % (mainkey, extra, instrument) for value in values: metadata.add_unique(newkey, value) del metadata[key] From c6e78731cf1b13338658290a1ea28c5c21a4c9cf Mon Sep 17 00:00:00 2001 From: Daniel Sobey Date: Sun, 7 Oct 2018 13:21:18 +1030 Subject: [PATCH 024/123] whitespace fixes --- plugins/wikidata/wikidata.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/plugins/wikidata/wikidata.py b/plugins/wikidata/wikidata.py index bd4cd7a6..1c6917e1 100644 --- a/plugins/wikidata/wikidata.py +++ b/plugins/wikidata/wikidata.py @@ -26,21 +26,19 @@ def __init__(self): # Key: mbid, value: List of metadata entries to be updated when we have parsed everything self.requests = {} - # Key: mbid, value: List of items to track the number of outstanding requests self.albums = {} - + # cache, items that have been found # key: mbid, value: list of strings containing the genre's self.cache = {} # not used def process_release(self, album, metadata, release): - self.ws = album.tagger.webservice self.log = album.log item_id = dict.get(metadata, 'musicbrainz_releasegroupid')[0] - + log.info('WIKIDATA: processing release group %s ' % item_id) self.process_request(metadata, album, item_id, type='release-group') for artist in dict.get(metadata, 'musicbrainz_albumartistid'): @@ -49,12 +47,12 @@ def process_release(self, album, metadata, release): self.process_request(metadata, album, item_id, type='artist') # Main processing function - # First see if we have already found what we need in the cache, finalize loading + # First see if we have already found what we need in the cache, finalize loading # Next see if we are already looking for the item # If we are add this item to the list of items to be updated once we find what we are looking for. # Otherwise we are the first one to look up this item, start a new request # metadata, map containing the new metadata - # + # def process_request(self, metadata, album, item_id, type): with self.lock: log.debug('WIKIDATA: Looking up cache for item %s' % item_id) @@ -110,7 +108,7 @@ def musicbrainz_release_lookup(self, item_id, metadata, response, reply, error): for relation in response.metadata[0].release_group[0].relation_list[0].relation: if relation.type == 'wikidata' and 'target' in relation.children: found = True - wikidata_url = relation.target[0].text + wikidata_url = relation.target[0].text self.process_wikidata(wikidata_url, item_id) if 'artist' in response.metadata[0].children: if 'relation_list' in response.metadata[0].artist[0].children: From 418e57e4594c9ebe1565d33dc0da84acf5b35e04 Mon Sep 17 00:00:00 2001 From: Daniel Sobey Date: Sun, 7 Oct 2018 21:33:20 +1030 Subject: [PATCH 025/123] style fixes --- plugins/wikidata/wikidata.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/wikidata/wikidata.py b/plugins/wikidata/wikidata.py index 1c6917e1..f3197b34 100644 --- a/plugins/wikidata/wikidata.py +++ b/plugins/wikidata/wikidata.py @@ -78,9 +78,9 @@ def process_request(self, metadata, album, item_id, type): album._requests += 1 self.albums[item_id].append(album) else: - self.requests[item_id]=[metadata] + self.requests[item_id] = [metadata] album._requests += 1 - self.albums[item_id]=[album] + self.albums[item_id] = [album] log.debug('WIKIDATA: first request for this item') @@ -131,10 +131,9 @@ def musicbrainz_release_lookup(self, item_id, metadata, response, reply, error): album._requests -= 1 log.debug('WIKIDATA: TOTAL REMAINING REQUESTS %s' % album._requests) - if album._requests == 0 : + if not album._requests: self.albums[item_id].remove(album) album._finalize_loading(None) - #del self.requests[item_id] def process_wikidata(self, wikidata_url, item_id): with self.lock: @@ -202,7 +201,7 @@ def parse_wikidata_response(self, item, item_id, response, reply, error): for album in self.albums[item_id]: album._requests -= 1 - if album._requests == 0 : + if not album._requests: self.albums[item_id].remove(album) album._finalize_loading(None) log.info('WIKIDATA: TOTAL REMAINING REQUESTS %s' % album._requests) From e5a47dceebf097b0da329f69f23526f59943b8b4 Mon Sep 17 00:00:00 2001 From: Bob Swift Date: Sun, 7 Oct 2018 10:47:50 -0600 Subject: [PATCH 026/123] Use list and add check for 'minor' as requested. --- .../standardise_performers.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/plugins/standardise_performers/standardise_performers.py b/plugins/standardise_performers/standardise_performers.py index 6431f318..b1429f87 100644 --- a/plugins/standardise_performers/standardise_performers.py +++ b/plugins/standardise_performers/standardise_performers.py @@ -49,16 +49,17 @@ def standardise_performers(album, metadata, *args): PLUGIN_NAME, subkey, ) - extra = '' - temp = '' - for part in instruments[0].split(): - if part in ['guest', 'solo', 'additional']: - extra = "{0}{1} ".format(extra, part) - else: - temp = "{0} {1}".format(temp, part).strip() - instruments[0] = temp + prefixes = [] + words = instruments[0].split() + for word in words[:]: + if not word in ['guest', 'solo', 'additional', 'minor']: + break + prefixes.append(word) + words.remove(word) + instruments[0] = " ".join(words) + prefix = " ".join(prefixes) + " " if prefixes else "" for instrument in instruments: - newkey = '%s:%s%s' % (mainkey, extra, instrument) + newkey = '%s:%s%s' % (mainkey, prefix, instrument) for value in values: metadata.add_unique(newkey, value) del metadata[key] From e3c9d306b0aa82f6d71763d735ade74a2f96aa98 Mon Sep 17 00:00:00 2001 From: Bob Swift Date: Sun, 7 Oct 2018 12:44:17 -0600 Subject: [PATCH 027/123] Use filter to generate list --- plugins/standardise_performers/standardise_performers.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugins/standardise_performers/standardise_performers.py b/plugins/standardise_performers/standardise_performers.py index b1429f87..f90aa781 100644 --- a/plugins/standardise_performers/standardise_performers.py +++ b/plugins/standardise_performers/standardise_performers.py @@ -35,10 +35,7 @@ def standardise_performers(album, metadata, *args): - for key, values in list(metadata.rawitems()): - if not key.startswith('performer:') \ - and not key.startswith('~performersort:'): - continue + for key, values in list(filter(lambda filter_tuple: filter_tuple[0].startswith('performer:') or filter_tuple[0].startswith('~performersort:'), metadata.rawitems())): mainkey, subkey = key.split(':', 1) if not subkey: continue From 356a2be02c7825b44a1d6a0eb1698b74e8f47b55 Mon Sep 17 00:00:00 2001 From: Bob Swift Date: Thu, 18 Oct 2018 14:02:19 -0600 Subject: [PATCH 028/123] Revert commit switching loop to use filter. --- plugins/standardise_performers/standardise_performers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/standardise_performers/standardise_performers.py b/plugins/standardise_performers/standardise_performers.py index f90aa781..b1429f87 100644 --- a/plugins/standardise_performers/standardise_performers.py +++ b/plugins/standardise_performers/standardise_performers.py @@ -35,7 +35,10 @@ def standardise_performers(album, metadata, *args): - for key, values in list(filter(lambda filter_tuple: filter_tuple[0].startswith('performer:') or filter_tuple[0].startswith('~performersort:'), metadata.rawitems())): + for key, values in list(metadata.rawitems()): + if not key.startswith('performer:') \ + and not key.startswith('~performersort:'): + continue mainkey, subkey = key.split(':', 1) if not subkey: continue From 5f819e04526dab3a58938623351685dff251215e Mon Sep 17 00:00:00 2001 From: tungol Date: Fri, 19 Oct 2018 08:42:26 +0200 Subject: [PATCH 029/123] New plugin: Compatible TXXX frames This plugin improves the compatibility of ID3 tags by using only a single value for TXXX frames. Multiple value TXXX frames technically don't comply with the ID3 specification. --- plugins/compatible_TXXX/compatible_TXXX.py | 66 ++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 plugins/compatible_TXXX/compatible_TXXX.py diff --git a/plugins/compatible_TXXX/compatible_TXXX.py b/plugins/compatible_TXXX/compatible_TXXX.py new file mode 100644 index 00000000..f8b16ec5 --- /dev/null +++ b/plugins/compatible_TXXX/compatible_TXXX.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- + +PLUGIN_NAME = u"Compatible TXXX frames" +PLUGIN_AUTHOR = u'Tungol' +PLUGIN_DESCRIPTION = """This plugin improves the compatibility of ID3 tags \ +by using only a single value for TXXX frames. Multiple value TXXX frames \ +technically don't comply with the ID3 specification.""" +PLUGIN_VERSION = "0.1" +PLUGIN_API_VERSIONS = ["1.0.0", "2.0"] + +from picard import config +from picard.formats import register_format +from picard.formats.id3 import MP3File, TrueAudioFile, AiffFile +from mutagen import id3 +try: + import mutagen.aiff +except ImportError: + mutagen.aiff = None + + +id3v24_join_with = '; ' + + +def build_compliant_TXXX(self, encoding, desc, values): + """Return a TXXX frame with only a single value. + + Use id3v23_join_with as the sperator if using id3v2.3, otherwise the value + set in this plugin (default "; "). + """ + if config.setting['write_id3v23']: + sep = config.setting['id3v23_join_with'] + else: + sep = id3v24_join_with + joined_values = [sep.join(values)] + return id3.TXXX(encoding=encoding, desc=desc, text=joined_values) + + +# I can't actually remove the original MP3File et al formats once they're +# registered. This depends on the name of the replacements sorting after the +# name of the originals, because picard.formats.guess_format picks the last +# item from a sorted list. + + +class MP3FileCompliant(MP3File): + """Alternate MP3 format class which uses single-value TXXX frames.""" + + build_TXXX = build_compliant_TXXX + + +class TrueAudioFileCompliant(TrueAudioFile): + """Alternate TTA format class which uses single-value TXXX frames.""" + + build_TXXX = build_compliant_TXXX + +register_format(MP3FileCompliant) +register_format(TrueAudioFileCompliant) + + +if mutagen.aiff: + + class AiffFileCompliant(AiffFile): + """Alternate AIFF format class which uses single-value TXXX frames.""" + + build_TXXX = build_compliant_TXXX + + register_format(AiffFileCompliant) From 1e8f1d40453c9c6de930acdee68c6e56ae76d20a Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 19 Oct 2018 08:52:57 +0200 Subject: [PATCH 030/123] compatible_TXXX: Support DSFFile --- plugins/compatible_TXXX/compatible_TXXX.py | 25 +++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/plugins/compatible_TXXX/compatible_TXXX.py b/plugins/compatible_TXXX/compatible_TXXX.py index f8b16ec5..1955712c 100644 --- a/plugins/compatible_TXXX/compatible_TXXX.py +++ b/plugins/compatible_TXXX/compatible_TXXX.py @@ -10,12 +10,8 @@ from picard import config from picard.formats import register_format -from picard.formats.id3 import MP3File, TrueAudioFile, AiffFile +from picard.formats.id3 import MP3File, TrueAudioFile, DSFFile, AiffFile from mutagen import id3 -try: - import mutagen.aiff -except ImportError: - mutagen.aiff = None id3v24_join_with = '; ' @@ -52,15 +48,20 @@ class TrueAudioFileCompliant(TrueAudioFile): build_TXXX = build_compliant_TXXX -register_format(MP3FileCompliant) -register_format(TrueAudioFileCompliant) + +class DSFFileCompliant(DSFFile): + """Alternate DSF format class which uses single-value TXXX frames.""" + + build_TXXX = build_compliant_TXXX -if mutagen.aiff: +class AiffFileCompliant(AiffFile): + """Alternate AIFF format class which uses single-value TXXX frames.""" - class AiffFileCompliant(AiffFile): - """Alternate AIFF format class which uses single-value TXXX frames.""" + build_TXXX = build_compliant_TXXX - build_TXXX = build_compliant_TXXX - register_format(AiffFileCompliant) +register_format(MP3FileCompliant) +register_format(TrueAudioFileCompliant) +register_format(DSFFileCompliant) +register_format(AiffFileCompliant) From 8db72634abbda913b132cd5e9994e2f68478b572 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 19 Oct 2018 11:58:48 +0200 Subject: [PATCH 031/123] tangoinfo: Fix crashes on requests --- plugins/tangoinfo/tangoinfo.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/plugins/tangoinfo/tangoinfo.py b/plugins/tangoinfo/tangoinfo.py index ed92ad55..a79da77a 100644 --- a/plugins/tangoinfo/tangoinfo.py +++ b/plugins/tangoinfo/tangoinfo.py @@ -142,21 +142,21 @@ def website_add_track(self, album, track, barcode, tint, zeros=0): path = '/%s' % (barcode) # Call website_process as a partial func - return album.tagger.webservice.get(host, port, path, + return album.tagger.webservice.download(host, port, path, partial(self.website_process, barcode, zeros), - parse_response_type=None, priority=False, important=False) + priority=False, important=False) def website_process(self, barcode, zeros, response, reply, error): if error: - log.error("%s: Error retrieving info for barcode %s" % \ - PLUGIN_NAME, barcode) + log.error("%s: Error retrieving info for barcode %s", PLUGIN_NAME, barcode) tuples = self.albumpage_queue.remove(barcode) - for track, album in tuples: + for track, album, tint in tuples: self.album_remove_request(album) return - tangoinfo_album_data = self.barcode_process_metadata(barcode, response) + html = bytes(response).decode() + tangoinfo_album_data = self.barcode_process_metadata(barcode, html) self.albumpage_cache[barcode] = tangoinfo_album_data tuples = self.albumpage_queue.remove(barcode) @@ -225,10 +225,15 @@ def barcode_process_metadata(self, barcode, response): # Check whether we have a concealed 404 and get the homepage if "Contents - tango.info" in response: log.debug("%s: No album with barcode %s on tango.info" % \ - (PLUGIN_NAME, barcode)) + (PLUGIN_NAME, barcode)) + return + + table = table_regex.search(response) + if not table: + log.warning("%s: No table found on page for barcode %s on tango.info" % \ + (PLUGIN_NAME, barcode)) return - table = re.findall(table_regex, response)[0] albuminfo = {} trcontent = [match.groups()[0] for match in trs.finditer(table)] From 18ef813a9921bac04ceb15d67472a2e13d29888f Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 19 Oct 2018 12:10:33 +0200 Subject: [PATCH 032/123] tangoinfo: Minor code cleanup --- plugins/tangoinfo/tangoinfo.py | 61 ++++++++++++++++------------------ 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/plugins/tangoinfo/tangoinfo.py b/plugins/tangoinfo/tangoinfo.py index a79da77a..380e8ba2 100644 --- a/plugins/tangoinfo/tangoinfo.py +++ b/plugins/tangoinfo/tangoinfo.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- PLUGIN_NAME = "Tango.info Adapter" -PLUGIN_AUTHOR = "Felix Elsner, Sambhav Kothari" +PLUGIN_AUTHOR = "Felix Elsner, Sambhav Kothari, Philipp Wolfer" PLUGIN_DESCRIPTION = """

Load genre, date and vocalist tags from the online database tango.info.

@@ -8,7 +8,7 @@ it does not cause unnecessary server load for either MusicBrainz.org or tango.info

""" -PLUGIN_VERSION = "1.0" +PLUGIN_VERSION = "1.1" PLUGIN_API_VERSIONS = ["2.0"] PLUGIN_LICENSE = "GPL-2.0" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" @@ -98,18 +98,16 @@ def add_tangoinfo_data(self, album, track_metadata, # ) return - tint = str("0%s-%s-%s" % ( - barcode, - str(track_metadata["discnumber"])\ - if track_metadata.get("discnumber") else "1", - str(track_metadata["tracknumber"]) - )) + tint = "0%s-%s-%s" % ( + barcode, + str(track_metadata.get("discnumber", "1")), + str(track_metadata["tracknumber"])) if barcode in self.albumpage_cache: if self.albumpage_cache[barcode]: if not self.albumpage_cache[barcode].get(tint): - log.debug("%s: No information on tango.info for barcode \ - %s" % (PLUGIN_NAME, barcode)) + log.debug("%s: No information on tango.info for barcode %s", + PLUGIN_NAME, barcode) else: for field in ['genre', 'date', 'vocal']: # Checks, as not to overwrite with empty data @@ -117,8 +115,8 @@ def add_tangoinfo_data(self, album, track_metadata, track_metadata[field] = self\ .albumpage_cache[barcode][tint][field] else: - log.debug("%s: No information on tango.info for barcode %s" \ - % (PLUGIN_NAME, barcode)) + log.debug("%s: No information on tango.info for barcode %s", + PLUGIN_NAME, barcode) else: #log.debug("%s: Adding to queue: %s, new track: %s" \ # % (PLUGIN_NAME, barcode, album._new_tracks[-1])) @@ -133,9 +131,7 @@ def website_add_track(self, album, track, barcode, tint, zeros=0): if self.albumpage_queue.append(barcode, (track, album, tint)): log.debug("%s: Downloading from tango.info: track %s, album %s, \ - with TINT %s" % (\ - PLUGIN_NAME, str(track), str(album), tint) - ) + with TINT %s", PLUGIN_NAME, str(track), str(album), tint) host = 'tango.info' port = 443 @@ -149,7 +145,8 @@ def website_add_track(self, album, track, barcode, tint, zeros=0): def website_process(self, barcode, zeros, response, reply, error): if error: - log.error("%s: Error retrieving info for barcode %s", PLUGIN_NAME, barcode) + log.error("%s: Error retrieving info for barcode %s", + PLUGIN_NAME, barcode) tuples = self.albumpage_queue.remove(barcode) for track, album, tint in tuples: self.album_remove_request(album) @@ -164,13 +161,12 @@ def website_process(self, barcode, zeros, response, reply, error): if tangoinfo_album_data: if zeros > 0: log.debug("%s: " - "tango.info does not seem to have data for barcode %s. However, " - "retrying with barcode %s (i.e. the same with 0 prepended) was " - "successful. This most likely means either MusicBrainz or " - "tango.info has stored a wrong barcode for this release. You might " - "want to investigate this discrepancy and report it." \ - % (PLUGIN_NAME, barcode[zeros:], barcode) - ) + "tango.info does not seem to have data for barcode %s. However, " + "retrying with barcode %s (i.e. the same with 0 prepended) was " + "successful. This most likely means either MusicBrainz or " + "tango.info has stored a wrong barcode for this release. You might " + "want to investigate this discrepancy and report it.", + PLUGIN_NAME, barcode[zeros:], barcode) for track, album, tint in tuples: tm = track.metadata @@ -193,22 +189,21 @@ def website_process(self, barcode, zeros, response, reply, error): "Could not load album with barcode %s even with zero " "prepended(%s). This most likely means tango.info does " "not have a release for this barcode (or MusicBrainz has a " - "wrong barcode)" \ - % (PLUGIN_NAME, barcode[1:], barcode) - ) + "wrong barcode)", + PLUGIN_NAME, barcode[1:], barcode) for track, album, tint in tuples: self.album_remove_request(album) return - log.debug("%s: Retrying with 0-padded barcode for barcode %s" % \ - (PLUGIN_NAME, barcode)) + log.debug("%s: Retrying with 0-padded barcode for barcode %s", + PLUGIN_NAME, barcode) for track, album, tint in tuples: retry_barcode = "0" + str(barcode) retry_tint = "0" + tint # Try again with new barcode, but at most two times(param zero) self.website_add_track( - album, track, retry_barcode, retry_tint, zeros=(zeros+1) + album, track, retry_barcode, retry_tint, zeros=(zeros+1) ) self.album_remove_request(album) @@ -224,14 +219,14 @@ def barcode_process_metadata(self, barcode, response): # Check whether we have a concealed 404 and get the homepage if "Contents - tango.info" in response: - log.debug("%s: No album with barcode %s on tango.info" % \ - (PLUGIN_NAME, barcode)) + log.debug("%s: No album with barcode %s on tango.info", + PLUGIN_NAME, barcode) return table = table_regex.search(response) if not table: - log.warning("%s: No table found on page for barcode %s on tango.info" % \ - (PLUGIN_NAME, barcode)) + log.warning("%s: No table found on page for barcode %s on tango.info", + PLUGIN_NAME, barcode) return albuminfo = {} From 2240c6905ea92e877064299fa3be32576b4d9db7 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 22 Oct 2018 15:32:56 +0200 Subject: [PATCH 033/123] compatible_TXXX: Support only Picard 2.0 API --- plugins/compatible_TXXX/compatible_TXXX.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/compatible_TXXX/compatible_TXXX.py b/plugins/compatible_TXXX/compatible_TXXX.py index 1955712c..5104dc1b 100644 --- a/plugins/compatible_TXXX/compatible_TXXX.py +++ b/plugins/compatible_TXXX/compatible_TXXX.py @@ -6,7 +6,7 @@ by using only a single value for TXXX frames. Multiple value TXXX frames \ technically don't comply with the ID3 specification.""" PLUGIN_VERSION = "0.1" -PLUGIN_API_VERSIONS = ["1.0.0", "2.0"] +PLUGIN_API_VERSIONS = ["2.0"] from picard import config from picard.formats import register_format From 4be06c7cae79c04248b2b39f5152002fcadf0a1f Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 22 Oct 2018 15:33:56 +0200 Subject: [PATCH 034/123] compatible_TXXX: Set license to GPL-2.0 or later --- plugins/compatible_TXXX/compatible_TXXX.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/compatible_TXXX/compatible_TXXX.py b/plugins/compatible_TXXX/compatible_TXXX.py index 5104dc1b..ab931c8e 100644 --- a/plugins/compatible_TXXX/compatible_TXXX.py +++ b/plugins/compatible_TXXX/compatible_TXXX.py @@ -7,6 +7,8 @@ technically don't comply with the ID3 specification.""" PLUGIN_VERSION = "0.1" PLUGIN_API_VERSIONS = ["2.0"] +PLUGIN_LICENSE = "GPL-2.0 or later" +PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" from picard import config from picard.formats import register_format From 72169578e9d3e311ce198c19ef6869935081061d Mon Sep 17 00:00:00 2001 From: virusMac Date: Mon, 29 Oct 2018 17:54:55 +0100 Subject: [PATCH 035/123] Add better error handling for AcousticBrainz plugin --- plugins/acousticbrainz/acousticbrainz.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/acousticbrainz/acousticbrainz.py b/plugins/acousticbrainz/acousticbrainz.py index a5f36fcd..667f0405 100644 --- a/plugins/acousticbrainz/acousticbrainz.py +++ b/plugins/acousticbrainz/acousticbrainz.py @@ -39,6 +39,11 @@ def result(album, metadata, data, reply, error): + if error or not "highlevel" in data: + album._requests -= 1 + album._finalize_loading(None) + return + moods = [] genres = [] try: @@ -60,12 +65,13 @@ def result(album, metadata, data, reply, error): def process_track(album, metadata, release, track): - album.tagger.webservice.download( + album.tagger.webservice.get( ACOUSTICBRAINZ_HOST, ACOUSTICBRAINZ_PORT, "/%s/high-level" % (metadata["musicbrainz_recordingid"]), partial(result, album, metadata), - priority=True + priority=True, + parse_response_type=None ) album._requests += 1 From 97291deb07508138e8c659912950c8270522e731 Mon Sep 17 00:00:00 2001 From: abhi-ohri <44491059+abhi-ohri@users.noreply.github.com> Date: Tue, 30 Oct 2018 23:01:44 +0530 Subject: [PATCH 036/123] Handle exceptions with in Tonal Rhythm Plugin When AcousticBrainz was down during a recent migration and was returning HTML instead of the expected JSON data it caused Picard to crash if the AB plugin was enabled. I have update the AcousticBrainz Tonal-Rhythm plugin for better error handling to avoid crashes. --- .../acousticbrainz_tonal-rhythm.py | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/plugins/acousticbrainz_tonal-rhythm/acousticbrainz_tonal-rhythm.py b/plugins/acousticbrainz_tonal-rhythm/acousticbrainz_tonal-rhythm.py index 82ffd77a..7022654e 100644 --- a/plugins/acousticbrainz_tonal-rhythm/acousticbrainz_tonal-rhythm.py +++ b/plugins/acousticbrainz_tonal-rhythm/acousticbrainz_tonal-rhythm.py @@ -34,6 +34,7 @@ from picard.metadata import register_track_metadata_processor from functools import partial from picard.webservice import ratecontrol +from picard.util import parse_json ACOUSTICBRAINZ_HOST = "acousticbrainz.org" ACOUSTICBRAINZ_PORT = 80 @@ -44,9 +45,14 @@ class AcousticBrainz_Key: def get_data(self, album, track_metadata, trackXmlNode, releaseXmlNode): + if not musicbrainz_recordingid in track_metadata: + log.error("%s: Error parsing response. No MusicBrainz recording id found.", + PLUGIN_NAME) + return recordingId = track_metadata['musicbrainz_recordingid'] if recordingId: - log.debug("%s: Add AcusticBrainz request for %s (%s)", PLUGIN_NAME, track_metadata['title'], recordingId) + log.debug("%s: Add AcousticBrainz request for %s (%s)", + PLUGIN_NAME, track_metadata['title'], recordingId) self.album_add_request(album) path = "/%s/low-level" % recordingId return album.tagger.webservice.get( @@ -54,16 +60,23 @@ def get_data(self, album, track_metadata, trackXmlNode, releaseXmlNode): ACOUSTICBRAINZ_PORT, path, partial(self.process_data, album, track_metadata), - priority=True, important=False) - return + priority=True, + important=False, + parse_response_type=None) def process_data(self, album, track_metadata, response, reply, error): if error: log.error("%s: Network error retrieving acousticBrainz data for recordingId %s", - PLUGIN_NAME, track_metadata['musicbrainz_recordingid']) + PLUGIN_NAME, track_metadata['musicbrainz_recordingid']) + self.album_remove_request(album) + return + try: + data = parse_json(response) + except JSONDecodeError: + log.error("%s: Network error retrieving AcousticBrainz data for recordingId %s", + PLUGIN_NAME, track_metadata['musicbrainz_recordingid']) self.album_remove_request(album) return - data = response if "tonal" in data: if "key_key" in data["tonal"]: key = data["tonal"]["key_key"] From df65df5e447f48d2e43bff6a632c5101419bc387 Mon Sep 17 00:00:00 2001 From: Wieland Hoffmann Date: Mon, 5 Nov 2018 16:53:13 +0100 Subject: [PATCH 037/123] Don't test on Python 3.4 Picard doesn't support Python 3.4, so making sure plugins work on it is not useful. Python 3.4 is also not a requirement for the infrastructure that builds the website. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 127c0a93..a7553623 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: python python: - - "3.4" - "3.5" - "3.6" script: python test.py From e07c1a05839bcd6ca85edef9782fa1eaa36ed6d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederik=20=E2=80=9CFreso=E2=80=9D=20S=2E=20Olesen?= Date: Mon, 5 Nov 2018 18:45:39 +0100 Subject: [PATCH 038/123] Do test on Python 3.7 Picard supports Python 3.7, so making sure plugins work on it is very useful. Heavily inspired by https://github.com/metabrainz/picard-plugins/pull/177 ;) --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index a7553623..2899d825 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,4 +2,5 @@ language: python python: - "3.5" - "3.6" + - "3.7" script: python test.py From ba5d9dc577bb506f632b92b55610a7c400d3e2c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frederik=20=E2=80=9CFreso=E2=80=9D=20S=2E=20Olesen?= Date: Tue, 6 Nov 2018 10:23:53 +0100 Subject: [PATCH 039/123] Use Ubuntu 16.04 Xenial test environment distro Ubuntu 14.04 Trusty (Travis' default distribution) doesn't contain Python 3.7 and is generally more than 4 years old and I think not supported by Picard 2.0 anyway, so moving to a 2 years never version of Ubuntu that does contain Python 3.7. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 2899d825..372d00c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +dist: xenial language: python python: - "3.5" From 8c3ef1ed93a68e7809e9d60c38c1b454b7a86e06 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 9 Nov 2018 11:48:39 +0100 Subject: [PATCH 040/123] acousticbrainz: bump version --- plugins/acousticbrainz/acousticbrainz.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/acousticbrainz/acousticbrainz.py b/plugins/acousticbrainz/acousticbrainz.py index 667f0405..e6dcd1bb 100644 --- a/plugins/acousticbrainz/acousticbrainz.py +++ b/plugins/acousticbrainz/acousticbrainz.py @@ -23,7 +23,7 @@ WARNING: Experimental plugin. All guarantees voided by use.''' PLUGIN_LICENSE = "GPL-2.0" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.txt" -PLUGIN_VERSION = "1.1" +PLUGIN_VERSION = "1.1.1" PLUGIN_API_VERSIONS = ["2.0"] from functools import partial From 03bde26c8c8a78f88145e20f1852e7ac914ab6ca Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 9 Nov 2018 11:53:13 +0100 Subject: [PATCH 041/123] acousticbrainz_tonal-rhythm: bump version --- .../acousticbrainz_tonal-rhythm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/acousticbrainz_tonal-rhythm/acousticbrainz_tonal-rhythm.py b/plugins/acousticbrainz_tonal-rhythm/acousticbrainz_tonal-rhythm.py index 7022654e..ada0dc80 100644 --- a/plugins/acousticbrainz_tonal-rhythm/acousticbrainz_tonal-rhythm.py +++ b/plugins/acousticbrainz_tonal-rhythm/acousticbrainz_tonal-rhythm.py @@ -27,7 +27,7 @@ ''' PLUGIN_LICENSE = "GPL-2.0" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.txt" -PLUGIN_VERSION = '1.1' +PLUGIN_VERSION = '1.1.1' PLUGIN_API_VERSIONS = ["2.0"] # Requires support for TKEY which is in 1.4 from picard import log @@ -45,7 +45,7 @@ class AcousticBrainz_Key: def get_data(self, album, track_metadata, trackXmlNode, releaseXmlNode): - if not musicbrainz_recordingid in track_metadata: + if not musicbrainz_recordingid in track_metadata: log.error("%s: Error parsing response. No MusicBrainz recording id found.", PLUGIN_NAME) return From 13e5920bdad5391baaec6421dccb11955d06ed8d Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 9 Nov 2018 20:15:46 +0100 Subject: [PATCH 042/123] acousticbrainz: fix exception on data validation --- plugins/acousticbrainz/acousticbrainz.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/plugins/acousticbrainz/acousticbrainz.py b/plugins/acousticbrainz/acousticbrainz.py index e6dcd1bb..5e9a037f 100644 --- a/plugins/acousticbrainz/acousticbrainz.py +++ b/plugins/acousticbrainz/acousticbrainz.py @@ -39,7 +39,7 @@ def result(album, metadata, data, reply, error): - if error or not "highlevel" in data: + if error: album._requests -= 1 album._finalize_loading(None) return @@ -47,16 +47,18 @@ def result(album, metadata, data, reply, error): moods = [] genres = [] try: - data = load_json(data)["highlevel"] - for k, v in data.items(): - if k.startswith("genre_") and not v["value"].startswith("not_"): - genres.append(v["value"]) - if k.startswith("mood_") and not v["value"].startswith("not_"): - moods.append(v["value"]) + data = load_json(data) + if "highlevel" in data: + data = data["highlevel"] + for k, v in data.items(): + if k.startswith("genre_") and not v["value"].startswith("not_"): + genres.append(v["value"]) + if k.startswith("mood_") and not v["value"].startswith("not_"): + moods.append(v["value"]) - metadata["genre"] = genres - metadata["mood"] = moods - log.debug("%s: Track %s (%s) Parsed response (genres: %s, moods: %s)", PLUGIN_NAME, metadata["musicbrainz_recordingid"], metadata["title"], str(genres), str(moods)) + metadata["genre"] = genres + metadata["mood"] = moods + log.debug("%s: Track %s (%s) Parsed response (genres: %s, moods: %s)", PLUGIN_NAME, metadata["musicbrainz_recordingid"], metadata["title"], str(genres), str(moods)) except Exception as e: log.error("%s: Track %s (%s) Error parsing response: %s", PLUGIN_NAME, metadata["musicbrainz_recordingid"], metadata["title"], str(e)) finally: From ab502abe2a6756151cfdf73c509448644589a249 Mon Sep 17 00:00:00 2001 From: Wieland Hoffmann Date: Thu, 1 Nov 2018 11:52:07 +0100 Subject: [PATCH 043/123] addrelease: Split the disc number extraction into a function It's a large enough block that it's worth it. --- plugins/addrelease/addrelease.py | 53 +++++++++++++++++++------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/plugins/addrelease/addrelease.py b/plugins/addrelease/addrelease.py index b00fdd35..a2ca45df 100644 --- a/plugins/addrelease/addrelease.py +++ b/plugins/addrelease/addrelease.py @@ -9,7 +9,7 @@ PLUGIN_VERSION = "0.7.2" PLUGIN_API_VERSIONS = ["2.0"] -from picard import config +from picard import config, log from picard.cluster import Cluster from picard.const import MUSICBRAINZ_SERVERS from picard.file import File @@ -116,38 +116,47 @@ class AddClusterAsRelease(AddObjectAsEntity): objtype = Cluster submit_path = '/release/add' + def __init__(self): + super().__init__() + self.discnumber_shift = -1 + + def extract_discnumber(self, metadata): + # As per https://musicbrainz.org/doc/Development/Release_Editor_Seeding#Tracklists_data + # the medium numbers ("m") must be starting with 0. + # Maybe the existing tags don't have disc numbers in them or + # they're starting with something smaller than or equal to 0, so try + # to produce a sane disc number. + try: + discnumber = metadata.get("discnumber", "1") + m = int(discnumber) + if m <= 0: + # A disc number was smaller than or equal to 0 - all other + # disc numbers need to be changed to accommodate that. + self.discnumber_shift = max(self.discnumber_shift, 0 - m) + m = m + self.discnumber_shift + except Exception as e: + # The most likely reason for an exception at this point is a + # ValueError because the disc number in the tags was not a + # number. Just log the exception and assume the medium number + # is 0. + log.info("Trying to get the disc number of %s caused the following error: %s; assuming 0", + metadata["~filename"], e) + m = 0 + return m + def set_form_values(self, cluster): nv = self.add_form_value nv("artist_credit.names.0.artist.name", cluster.metadata["albumartist"]) nv("name", cluster.metadata["album"]) - discnumber_shift = -1 for i, file in enumerate(cluster.files): try: i = int(file.metadata["tracknumber"]) - 1 except: pass - # As per https://musicbrainz.org/doc/Development/Release_Editor_Seeding#Tracklists_data - # the medium numbers ("m") must be starting with 0. - # Maybe the existing tags don't have disc numbers in them or - # they're starting with something smaller than or equal to 0, so try - # to produce a sane disc number. - try: - m = int(file.metadata.get("discnumber", 1)) - if m <= 0: - # A disc number was smaller than or equal to 0 - all other - # disc numbers need to be changed to accommodate that. - discnumber_shift = max(discnumber_shift, 0 - m) - m = m + discnumber_shift - except Exception as e: - # The most likely reason for an exception at this point is a - # ValueError because the disc number in the tags was not a - # number. Just log the exception and assume the medium number - # is 0. - file.log.info("Trying to get the disc number of %s caused the following error: %s; assuming 0", - file.filename, e) - m = 0 + + m = self.extract_discnumber(file.metadata) # add a track-level name-value def tnv(n, v): From cc7ac3ae1287f68324f73c2424f38cf8d1ac1f21 Mon Sep 17 00:00:00 2001 From: Wieland Hoffmann Date: Thu, 1 Nov 2018 12:13:06 +0100 Subject: [PATCH 044/123] Handle disc number tags with totaldisc information The files of https://musicbrainz.org/release/49b267b5-8bca-4a35-b2ce-d3cd5591c207 for example have discnumber tags containing "1/1". --- plugins/addrelease/addrelease.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/addrelease/addrelease.py b/plugins/addrelease/addrelease.py index a2ca45df..4efa6e08 100644 --- a/plugins/addrelease/addrelease.py +++ b/plugins/addrelease/addrelease.py @@ -6,7 +6,7 @@ files to help you quickly add them as releases or standalone recordings to\ the MusicBrainz database via the website by pre-populating artists,\ track names and times." -PLUGIN_VERSION = "0.7.2" +PLUGIN_VERSION = "0.7.3" PLUGIN_API_VERSIONS = ["2.0"] from picard import config, log @@ -128,6 +128,8 @@ def extract_discnumber(self, metadata): # to produce a sane disc number. try: discnumber = metadata.get("discnumber", "1") + # Split off any totaldiscs information + discnumber = discnumber.split("/", 1)[0] m = int(discnumber) if m <= 0: # A disc number was smaller than or equal to 0 - all other From 9c559a05c0a8a027fb242365e5adc787aa065605 Mon Sep 17 00:00:00 2001 From: Wieland Hoffmann Date: Thu, 1 Nov 2018 12:14:20 +0100 Subject: [PATCH 045/123] Add doctests for extract_discnumber --- plugins/addrelease/addrelease.py | 39 ++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/plugins/addrelease/addrelease.py b/plugins/addrelease/addrelease.py index 4efa6e08..059d0201 100644 --- a/plugins/addrelease/addrelease.py +++ b/plugins/addrelease/addrelease.py @@ -121,6 +121,41 @@ def __init__(self): self.discnumber_shift = -1 def extract_discnumber(self, metadata): + """ + >>> from picard.metadata import Metadata + >>> m = Metadata() + >>> AddClusterAsRelease().extract_discnumber(m) + 0 + >>> m["discnumber"] = "boop" + >>> AddClusterAsRelease().extract_discnumber(m) + 0 + >>> m["discnumber"] = "1" + >>> AddClusterAsRelease().extract_discnumber(m) + 0 + >>> m["discnumber"] = 1 + >>> AddClusterAsRelease().extract_discnumber(m) + 0 + >>> m["discnumber"] = -1 + >>> AddClusterAsRelease().extract_discnumber(m) + 0 + >>> m["discnumber"] = "1/1" + >>> AddClusterAsRelease().extract_discnumber(m) + 0 + >>> m["discnumber"] = "2/2" + >>> AddClusterAsRelease().extract_discnumber(m) + 1 + >>> a = AddClusterAsRelease() + >>> m["discnumber"] = "-2/2" + >>> a.extract_discnumber(m) + 0 + >>> m["discnumber"] = "-1/4" + >>> a.extract_discnumber(m) + 1 + >>> m["discnumber"] = "1/4" + >>> a.extract_discnumber(m) + 3 + + """ # As per https://musicbrainz.org/doc/Development/Release_Editor_Seeding#Tracklists_data # the medium numbers ("m") must be starting with 0. # Maybe the existing tags don't have disc numbers in them or @@ -209,3 +244,7 @@ def set_form_values(self, track): register_cluster_action(AddClusterAsRelease()) register_file_action(AddFileAsRecording()) register_file_action(AddFileAsRelease()) + +if __name__ == "__main__": + import doctest + doctest.testmod() From 70ede1e98e9ea38296a489c9ee6650adbc017c34 Mon Sep 17 00:00:00 2001 From: Wieland Hoffmann Date: Thu, 1 Nov 2018 12:16:05 +0100 Subject: [PATCH 046/123] Remove Python 2 compatibility code --- test.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/test.py b/test.py index 1384ef35..dd3515ae 100644 --- a/test.py +++ b/test.py @@ -6,12 +6,6 @@ import unittest from generate import build_json, zip_files -# python 2 & 3 compatibility -try: - basestring -except NameError: - basestring = str - class GenerateTestCase(unittest.TestCase): @@ -86,11 +80,13 @@ def test_valid_json(self): # All plugins should contain all required fields for module_name, data in plugin_json.items(): - self.assertIsInstance(data['name'], basestring) + self.assertIsInstance(data['name'], str) self.assertIsInstance(data['api_versions'], list) - self.assertIsInstance(data['author'], basestring) - self.assertIsInstance(data['description'], basestring) - self.assertIsInstance(data['version'], basestring) + self.assertIsInstance(data['author'], str) + self.assertIsInstance(data['description'], str) + self.assertIsInstance(data['version'], str) + + if __name__ == '__main__': From 8ff8e984c807d86e6074c2c664e34a7987bbef40 Mon Sep 17 00:00:00 2001 From: Wieland Hoffmann Date: Thu, 1 Nov 2018 12:22:00 +0100 Subject: [PATCH 047/123] Run doctests in test.py --- test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test.py b/test.py index dd3515ae..272343ed 100644 --- a/test.py +++ b/test.py @@ -1,3 +1,4 @@ +import doctest import os import glob import json @@ -87,6 +88,10 @@ def test_valid_json(self): self.assertIsInstance(data['version'], str) +def load_tests(loader, tests, ignore): + from plugins.addrelease import addrelease + tests.addTests(doctest.DocTestSuite(addrelease)) + return tests if __name__ == '__main__': From 3dcb1a9cb2a5910a541670e432c66a80e7674247 Mon Sep 17 00:00:00 2001 From: Wieland Hoffmann Date: Thu, 1 Nov 2018 12:46:53 +0100 Subject: [PATCH 048/123] Install picard on travis This allows importing picard modules in tests --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 372d00c1..b0caee80 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,11 @@ dist: xenial language: python +cache: + pip: true python: - "3.5" - "3.6" - "3.7" +before_install: + - pip3 install picard script: python test.py From a63ff909135072cdcb6ffe3688c537b13c12598f Mon Sep 17 00:00:00 2001 From: Wieland Hoffmann Date: Sat, 10 Nov 2018 09:58:24 +0100 Subject: [PATCH 049/123] Disable doctests for addrelease.py They require being able to install & import picard from PyPI, but that doesn't work at the moment (https://tickets.metabrainz.org/browse/PICARD-1373). --- .travis.yml | 2 -- test.py | 7 ------- 2 files changed, 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index b0caee80..d31c641f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,4 @@ python: - "3.5" - "3.6" - "3.7" -before_install: - - pip3 install picard script: python test.py diff --git a/test.py b/test.py index 272343ed..3bd1f762 100644 --- a/test.py +++ b/test.py @@ -1,4 +1,3 @@ -import doctest import os import glob import json @@ -88,11 +87,5 @@ def test_valid_json(self): self.assertIsInstance(data['version'], str) -def load_tests(loader, tests, ignore): - from plugins.addrelease import addrelease - tests.addTests(doctest.DocTestSuite(addrelease)) - return tests - - if __name__ == '__main__': unittest.main() From db7949d79b8530f8b15888f2c8ddf6487661f457 Mon Sep 17 00:00:00 2001 From: Wieland Hoffmann Date: Sat, 10 Nov 2018 10:11:46 +0100 Subject: [PATCH 050/123] addrelease: Specifically catch ValueError The old comment mentioned a specific error, but the except clause caught all exceptions. Be more specific during exception handling because we really don't know if the behaviour for others makes sense. --- plugins/addrelease/addrelease.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/addrelease/addrelease.py b/plugins/addrelease/addrelease.py index 059d0201..0682d5ee 100644 --- a/plugins/addrelease/addrelease.py +++ b/plugins/addrelease/addrelease.py @@ -171,11 +171,10 @@ def extract_discnumber(self, metadata): # disc numbers need to be changed to accommodate that. self.discnumber_shift = max(self.discnumber_shift, 0 - m) m = m + self.discnumber_shift - except Exception as e: + except ValueError as e: # The most likely reason for an exception at this point is a - # ValueError because the disc number in the tags was not a - # number. Just log the exception and assume the medium number - # is 0. + # because the disc number in the tags was not a number. Just log + # the exception and assume the medium number is 0. log.info("Trying to get the disc number of %s caused the following error: %s; assuming 0", metadata["~filename"], e) m = 0 From aaf833de6fe0d4e56163a8678316f75ffe2df141 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 9 Nov 2018 12:46:54 +0100 Subject: [PATCH 051/123] bpm: update user interface --- plugins/bpm/__init__.py | 5 +- plugins/bpm/options_bpm.ui | 283 ++++++++++++++++++---------------- plugins/bpm/ui_options_bpm.py | 100 +++++++----- 3 files changed, 208 insertions(+), 180 deletions(-) diff --git a/plugins/bpm/__init__.py b/plugins/bpm/__init__.py index 799d479f..58f1d010 100644 --- a/plugins/bpm/__init__.py +++ b/plugins/bpm/__init__.py @@ -12,7 +12,7 @@ PLUGIN_DESCRIPTION = """Calculate BPM for selected files and albums. Linux only version with dependancy on Aubio and Numpy""" PLUGIN_LICENSE = "GPL-2.0" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" -PLUGIN_VERSION = "1.2" +PLUGIN_VERSION = "1.3" PLUGIN_API_VERSIONS = ["2.0"] # PLUGIN_INCOMPATIBLE_PLATFORMS = [ # 'win32', 'cygwyn', 'darwin', 'os2', 'os2emx', 'riscos', 'atheos'] @@ -128,6 +128,7 @@ def __init__(self, parent=None): self.ui = Ui_BPMOptionsPage() self.ui.setupUi(self) self.ui.slider_parameter.valueChanged.connect(self.update_parameters) + self.update_parameters() def load(self): cfg = self.config.setting @@ -139,7 +140,7 @@ def save(self): def update_parameters(self): val = self.ui.slider_parameter.value() - samplerate, buf_size, hop_size = [string_(v) for v in + samplerate, buf_size, hop_size = [str(v) for v in bpm_slider_settings[val]] self.ui.samplerate_value.setText(samplerate) self.ui.win_s_value.setText(buf_size) diff --git a/plugins/bpm/options_bpm.ui b/plugins/bpm/options_bpm.ui index 418a7b74..e112015d 100644 --- a/plugins/bpm/options_bpm.ui +++ b/plugins/bpm/options_bpm.ui @@ -6,149 +6,160 @@ 0 0 - 495 + 611 273
- - + + BPM Analyze Parameters: - - - - 8 - 80 - 43 - 19 - - - - - - - Default - - - - - - 20 - 51 - 391 - 23 - - - - 1 - - - 3 - - - Qt::Horizontal - - - QSlider::TicksBelow - - - 1 - - - - - - 370 - 80 - 61 - 19 - - - - Qt::RightToLeft - - - Super Fast - - - - - - 10 - 120 - 471 - 20 - - - - Qt::Horizontal - - - - - - 6 - 140 - 471 - 91 - - - - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Number of frames between two consecutive runs: - - - - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Length of FFT: - - - - - - - Samplerate: - - - - - + + + + + + 500 + 16777215 + + + + + + + 1 + + + 3 + + + 1 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 1 + + + + + + + 6 + + + + + Qt::RightToLeft + + + Super Fast + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + + + + Default + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + + + + + + + Qt::Horizontal + + + + + + + + + Samplerate: + + + + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Number of frames between two consecutive runs: + + + + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Length of FFT: + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + diff --git a/plugins/bpm/ui_options_bpm.py b/plugins/bpm/ui_options_bpm.py index feca3fc3..8808f856 100644 --- a/plugins/bpm/ui_options_bpm.py +++ b/plugins/bpm/ui_options_bpm.py @@ -1,82 +1,98 @@ # -*- coding: utf-8 -*- -# Automatically generated - don't edit. -# Use `python setup.py build_ui` to update it. +# Form implementation generated from reading ui file 'options_bpm.ui' +# +# Created by: PyQt5 UI code generator 5.11.3 +# +# WARNING! All changes made in this file will be lost! from PyQt5 import QtCore, QtGui, QtWidgets - class Ui_BPMOptionsPage(object): def setupUi(self, BPMOptionsPage): BPMOptionsPage.setObjectName("BPMOptionsPage") - BPMOptionsPage.resize(495, 273) - self.gridLayout_2 = QtWidgets.QGridLayout(BPMOptionsPage) - self.gridLayout_2.setObjectName("gridLayout_2") + BPMOptionsPage.resize(611, 273) + self.verticalLayout = QtWidgets.QVBoxLayout(BPMOptionsPage) + self.verticalLayout.setObjectName("verticalLayout") self.bpm_options = QtWidgets.QGroupBox(BPMOptionsPage) self.bpm_options.setObjectName("bpm_options") - self.slider_default = QtWidgets.QLabel(self.bpm_options) - self.slider_default.setGeometry(QtCore.QRect(8, 80, 43, 19)) - self.slider_default.setToolTip("") - self.slider_default.setObjectName("slider_default") - self.slider_parameter = QtWidgets.QSlider(self.bpm_options) - self.slider_parameter.setGeometry(QtCore.QRect(20, 51, 391, 23)) + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.bpm_options) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.verticalWidget = QtWidgets.QWidget(self.bpm_options) + self.verticalWidget.setMaximumSize(QtCore.QSize(500, 16777215)) + self.verticalWidget.setObjectName("verticalWidget") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.verticalWidget) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.slider_parameter = QtWidgets.QSlider(self.verticalWidget) self.slider_parameter.setMinimum(1) self.slider_parameter.setMaximum(3) + self.slider_parameter.setPageStep(1) self.slider_parameter.setOrientation(QtCore.Qt.Horizontal) self.slider_parameter.setTickPosition(QtWidgets.QSlider.TicksBelow) self.slider_parameter.setTickInterval(1) self.slider_parameter.setObjectName("slider_parameter") - self.slider_super_fast = QtWidgets.QLabel(self.bpm_options) - self.slider_super_fast.setGeometry(QtCore.QRect(370, 80, 61, 19)) + self.verticalLayout_3.addWidget(self.slider_parameter) + self.slider_labels = QtWidgets.QHBoxLayout() + self.slider_labels.setSpacing(6) + self.slider_labels.setObjectName("slider_labels") + self.slider_super_fast = QtWidgets.QLabel(self.verticalWidget) self.slider_super_fast.setLayoutDirection(QtCore.Qt.RightToLeft) + self.slider_super_fast.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) self.slider_super_fast.setObjectName("slider_super_fast") - self.line = QtWidgets.QFrame(self.bpm_options) - self.line.setGeometry(QtCore.QRect(10, 120, 471, 20)) + self.slider_labels.addWidget(self.slider_super_fast) + self.slider_default = QtWidgets.QLabel(self.verticalWidget) + self.slider_default.setToolTip("") + self.slider_default.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTop|QtCore.Qt.AlignTrailing) + self.slider_default.setObjectName("slider_default") + self.slider_labels.addWidget(self.slider_default) + self.verticalLayout_3.addLayout(self.slider_labels) + self.line = QtWidgets.QFrame(self.verticalWidget) self.line.setFrameShape(QtWidgets.QFrame.HLine) self.line.setFrameShadow(QtWidgets.QFrame.Sunken) self.line.setObjectName("line") - self.horizontalLayoutWidget = QtWidgets.QWidget(self.bpm_options) - self.horizontalLayoutWidget.setGeometry(QtCore.QRect(6, 140, 471, 91)) - self.horizontalLayoutWidget.setObjectName("horizontalLayoutWidget") - self.gridLayout = QtWidgets.QGridLayout(self.horizontalLayoutWidget) - self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout_3.addWidget(self.line) + self.gridLayout = QtWidgets.QGridLayout() self.gridLayout.setObjectName("gridLayout") - self.samplerate_value = QtWidgets.QLabel(self.horizontalLayoutWidget) + self.samplerate_label = QtWidgets.QLabel(self.verticalWidget) + self.samplerate_label.setObjectName("samplerate_label") + self.gridLayout.addWidget(self.samplerate_label, 2, 0, 1, 1) + self.samplerate_value = QtWidgets.QLabel(self.verticalWidget) self.samplerate_value.setText("") self.samplerate_value.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) self.samplerate_value.setObjectName("samplerate_value") self.gridLayout.addWidget(self.samplerate_value, 2, 1, 1, 1) - self.hop_s_label = QtWidgets.QLabel(self.horizontalLayoutWidget) + self.hop_s_value = QtWidgets.QLabel(self.verticalWidget) + self.hop_s_value.setText("") + self.hop_s_value.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.hop_s_value.setObjectName("hop_s_value") + self.gridLayout.addWidget(self.hop_s_value, 1, 1, 1, 1) + self.hop_s_label = QtWidgets.QLabel(self.verticalWidget) self.hop_s_label.setObjectName("hop_s_label") self.gridLayout.addWidget(self.hop_s_label, 1, 0, 1, 1) - self.win_s_value = QtWidgets.QLabel(self.horizontalLayoutWidget) + self.win_s_value = QtWidgets.QLabel(self.verticalWidget) self.win_s_value.setText("") self.win_s_value.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) self.win_s_value.setObjectName("win_s_value") self.gridLayout.addWidget(self.win_s_value, 0, 1, 1, 1) - self.hop_s_value = QtWidgets.QLabel(self.horizontalLayoutWidget) - self.hop_s_value.setText("") - self.hop_s_value.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.hop_s_value.setObjectName("hop_s_value") - self.gridLayout.addWidget(self.hop_s_value, 1, 1, 1, 1) - self.win_s_label = QtWidgets.QLabel(self.horizontalLayoutWidget) + self.win_s_label = QtWidgets.QLabel(self.verticalWidget) self.win_s_label.setObjectName("win_s_label") self.gridLayout.addWidget(self.win_s_label, 0, 0, 1, 1) - self.samplerate_label = QtWidgets.QLabel(self.horizontalLayoutWidget) - self.samplerate_label.setObjectName("samplerate_label") - self.gridLayout.addWidget(self.samplerate_label, 2, 0, 1, 1) - self.gridLayout.setColumnStretch(0, 1) - self.gridLayout.setColumnStretch(1, 1) - self.gridLayout_2.addWidget(self.bpm_options, 0, 0, 1, 1) + self.gridLayout.setColumnStretch(0, 4) + self.verticalLayout_3.addLayout(self.gridLayout) + self.verticalLayout_2.addWidget(self.verticalWidget) + spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout_2.addItem(spacerItem) + self.verticalLayout.addWidget(self.bpm_options) self.retranslateUi(BPMOptionsPage) QtCore.QMetaObject.connectSlotsByName(BPMOptionsPage) def retranslateUi(self, BPMOptionsPage): _translate = QtCore.QCoreApplication.translate - self.bpm_options.setTitle(_("BPM Analyze Parameters:")) - self.slider_default.setText(_("Default")) - self.slider_super_fast.setText(_("Super Fast")) - self.hop_s_label.setText(_("Number of frames between two consecutive runs:")) - self.win_s_label.setText(_("Length of FFT:")) - self.samplerate_label.setText(_("Samplerate:")) + self.bpm_options.setTitle(_translate("BPMOptionsPage", "BPM Analyze Parameters:")) + self.slider_super_fast.setText(_translate("BPMOptionsPage", "Super Fast")) + self.slider_default.setText(_translate("BPMOptionsPage", "Default")) + self.samplerate_label.setText(_translate("BPMOptionsPage", "Samplerate:")) + self.hop_s_label.setText(_translate("BPMOptionsPage", "Number of frames between two consecutive runs:")) + self.win_s_label.setText(_translate("BPMOptionsPage", "Length of FFT:")) + From 5d0222f6138674444bf7f5e668202f1fbb787c00 Mon Sep 17 00:00:00 2001 From: Wieland Hoffmann Date: Sun, 11 Nov 2018 17:03:39 +0100 Subject: [PATCH 052/123] Remove a superfluous 'a' --- plugins/addrelease/addrelease.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/addrelease/addrelease.py b/plugins/addrelease/addrelease.py index 0682d5ee..12f538bd 100644 --- a/plugins/addrelease/addrelease.py +++ b/plugins/addrelease/addrelease.py @@ -172,9 +172,9 @@ def extract_discnumber(self, metadata): self.discnumber_shift = max(self.discnumber_shift, 0 - m) m = m + self.discnumber_shift except ValueError as e: - # The most likely reason for an exception at this point is a - # because the disc number in the tags was not a number. Just log - # the exception and assume the medium number is 0. + # The most likely reason for an exception at this point is because + # the disc number in the tags was not a number. Just log the + # exception and assume the medium number is 0. log.info("Trying to get the disc number of %s caused the following error: %s; assuming 0", metadata["~filename"], e) m = 0 From 018754b3638b5ef43e4754376211d7b7b45d2d19 Mon Sep 17 00:00:00 2001 From: Paul Brackin Date: Mon, 12 Nov 2018 08:31:36 -0800 Subject: [PATCH 053/123] Update plugins/wikidata/wikidata.py Remove recursion, Remove redundant album insertions, Sort genre list, Make mb host+post instance variables. --- plugins/wikidata/wikidata.py | 38 +++++++++++++++--------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/plugins/wikidata/wikidata.py b/plugins/wikidata/wikidata.py index f3197b34..1a7ae935 100644 --- a/plugins/wikidata/wikidata.py +++ b/plugins/wikidata/wikidata.py @@ -32,7 +32,11 @@ def __init__(self): # cache, items that have been found # key: mbid, value: list of strings containing the genre's self.cache = {} - + + # find the mb url if this exists + self.mb_host = config.setting["server_host"] + self.mb_port = config.setting["server_port"] + # not used def process_release(self, album, metadata, release): self.ws = album.tagger.webservice @@ -63,9 +67,8 @@ def process_request(self, metadata, album, item_id, type): genre_list = self.cache.get(item_id) new_genre = set(metadata.getall("genre")) new_genre.update(genre_list) - metadata["genre"] = list(new_genre) - if album._requests == 0: - album._finalize_loading(None) + #sort the new genre list so that they don't appear as new entries (not a change) next time + metadata["genre"] = sorted(new_genre) return else: # pending requests are handled by adding the metadata object to a @@ -75,27 +78,20 @@ def process_request(self, metadata, album, item_id, type): 'WIKIDATA: request already pending, add it to the list of items to update once this has been found') self.requests[item_id].append(metadata) - album._requests += 1 - self.albums[item_id].append(album) else: self.requests[item_id] = [metadata] album._requests += 1 self.albums[item_id] = [album] log.debug('WIKIDATA: first request for this item') - log.info('WIKIDATA: about to call musicbrainz to look up %s ' % item_id) - # find the wikidata url if this exists - host = config.setting["server_host"] - port = config.setting["server_port"] path = '/ws/2/%s/%s' % (type, item_id) queryargs = {"inc": "url-rels"} - self.ws.get(host, port, path, - partial(self.musicbrainz_release_lookup, - item_id, metadata), - parse_response_type="xml", priority=False, - important=False, queryargs=queryargs) + + self.ws.get(self.mb_host, self.mb_port, path, partial(self.musicbrainz_release_lookup, item_id, + metadata), + parse_response_type="xml", priority=False, important=False, queryargs=queryargs) def musicbrainz_release_lookup(self, item_id, metadata, response, reply, error): found = False @@ -129,8 +125,7 @@ def musicbrainz_release_lookup(self, item_id, metadata, response, reply, error): with self.lock: for album in self.albums[item_id]: album._requests -= 1 - log.debug('WIKIDATA: TOTAL REMAINING REQUESTS %s' % - album._requests) + log.debug('WIKIDATA: TOTAL REMAINING REQUESTS %s' % album._requests) if not album._requests: self.albums[item_id].remove(album) album._finalize_loading(None) @@ -180,8 +175,7 @@ def parse_wikidata_response(self, item, item_id, response, reply, error): with self.lock: if len(genre_list) > 0: - log.info('WiKIDATA: final list of wikidata id found: %s' % - genre_entries) + log.info('WIKIDATA: final list of wikidata id found: %s' % genre_entries) log.info('WIKIDATA: final list of genre: %s' % genre_list) log.debug('WIKIDATA: total items to update: %s ' % @@ -189,15 +183,15 @@ def parse_wikidata_response(self, item, item_id, response, reply, error): for metadata in self.requests[item_id]: new_genre = set(metadata.getall("genre")) new_genre.update(genre_list) - metadata["genre"] = list(new_genre) + #sort the new genre list so that they don't appear as new entries (not a change) next time + metadata["genre"] = sorted(new_genre) self.cache[item_id] = genre_list log.debug('WIKIDATA: setting genre : %s ' % genre_list) else: log.info('WIKIDATA: Genre not found in wikidata') - log.info('WIKIDATA: Seeing if we can finalize tags %s ' % - len(self.albums[item_id])) + log.info('WIKIDATA: Seeing if we can finalize tags %s ' % len(self.albums[item_id])) for album in self.albums[item_id]: album._requests -= 1 From 6bf8205cd419a92164bb9b2bfa86361a4fc5e9f0 Mon Sep 17 00:00:00 2001 From: Paul Brackin Date: Tue, 13 Nov 2018 09:37:07 -0800 Subject: [PATCH 054/123] Update plugins/wikidata/wikidata.py allow mb host & port config update checks once per process_track call. clean some empty spaces, change a log call formatting --- plugins/wikidata/wikidata.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/plugins/wikidata/wikidata.py b/plugins/wikidata/wikidata.py index 1a7ae935..7568df9c 100644 --- a/plugins/wikidata/wikidata.py +++ b/plugins/wikidata/wikidata.py @@ -33,23 +33,27 @@ def __init__(self): # key: mbid, value: list of strings containing the genre's self.cache = {} - # find the mb url if this exists - self.mb_host = config.setting["server_host"] - self.mb_port = config.setting["server_port"] + # metabrainz url + self.mb_host = '' + self.mb_port = '' + + # web service & logger + self.ws = None + self.log = None # not used def process_release(self, album, metadata, release): self.ws = album.tagger.webservice self.log = album.log item_id = dict.get(metadata, 'musicbrainz_releasegroupid')[0] - + log.info('WIKIDATA: processing release group %s ' % item_id) self.process_request(metadata, album, item_id, type='release-group') for artist in dict.get(metadata, 'musicbrainz_albumartistid'): item_id = artist log.info('WIKIDATA: processing release artist %s' % item_id) self.process_request(metadata, album, item_id, type='artist') - + # Main processing function # First see if we have already found what we need in the cache, finalize loading # Next see if we are already looking for the item @@ -183,7 +187,7 @@ def parse_wikidata_response(self, item, item_id, response, reply, error): for metadata in self.requests[item_id]: new_genre = set(metadata.getall("genre")) new_genre.update(genre_list) - #sort the new genre list so that they don't appear as new entries (not a change) next time + # sort the new genre list so that they don't appear as new entries (not a change) next time metadata["genre"] = sorted(new_genre) self.cache[item_id] = genre_list log.debug('WIKIDATA: setting genre : %s ' % genre_list) @@ -191,7 +195,7 @@ def parse_wikidata_response(self, item, item_id, response, reply, error): else: log.info('WIKIDATA: Genre not found in wikidata') - log.info('WIKIDATA: Seeing if we can finalize tags %s ' % len(self.albums[item_id])) + log.info('WIKIDATA: Seeing if we can finalize tags %d ' % len(self.albums[item_id])) for album in self.albums[item_id]: album._requests -= 1 @@ -201,6 +205,8 @@ def parse_wikidata_response(self, item, item_id, response, reply, error): log.info('WIKIDATA: TOTAL REMAINING REQUESTS %s' % album._requests) def process_track(self, album, metadata, trackXmlNode, releaseXmlNode): + self.mb_host = config.setting["server_host"] + self.mb_port = config.setting["server_port"] self.ws = album.tagger.webservice self.log = album.log From b06400bccb4d477615ed517c4adeb064871b2e38 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Wed, 14 Nov 2018 15:23:40 +0100 Subject: [PATCH 055/123] workandmovement: Initial implementation for extracting work and movement information from loaded metadata --- plugins/workandmovement/workandmovement.py | 162 +++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 plugins/workandmovement/workandmovement.py diff --git a/plugins/workandmovement/workandmovement.py b/plugins/workandmovement/workandmovement.py new file mode 100644 index 00000000..dc612ad5 --- /dev/null +++ b/plugins/workandmovement/workandmovement.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2018 Philipp Wolfer +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + +PLUGIN_NAME = 'Work & Movement' +PLUGIN_AUTHOR = 'Philipp Wolfer' +PLUGIN_DESCRIPTION = 'Set work and movement based on work relationships' +PLUGIN_VERSION = '1.0' +PLUGIN_API_VERSIONS = ['2.1'] +PLUGIN_LICENSE = 'GPL-2.0-or-later' +PLUGIN_LICENSE_URL = 'https://www.gnu.org/licenses/gpl-2.0.html' + + +import re + +from picard import log +from picard.metadata import register_track_metadata_processor + + +class Work: + def __init__(self, title, id): + self.id = id + self.title = title + self.is_movement = False + self.is_work = False + self.part_number = 0 + self.parent = None + + def __str__(self): + s = [] + if self.parent: + s.append(str(self.parent)) + if self.is_movement: + type = 'Movement' + elif self.is_work: + type = 'Work' + else: + type = 'Unknown' + s.append('%s %i: %s' % (type, self.part_number, self.title)) + return '\n'.join(s) + + +def is_performance_work(rel): + return (rel['target-type'] == 'work' + and rel['direction'] == 'forward' + and rel['type'] == 'performance') + + +def is_parent_work(rel): + return (rel['target-type'] == 'work' + and rel['direction'] == 'backward' + and rel['type'] == 'parts') + + +def is_movement_like(rel): + return ('movement' in rel['attributes'] + or 'act' in rel['attributes'] + or 'ordering-key' in rel) + + +def is_child_work(rel): + return (rel['target-type'] == 'work' + and rel['direction'] == 'forward' + and rel['type'] == 'parts') + + +part_number_re = re.compile(r'^[0-9IVXLC]+\.\s+') +def remove_part_number(title): + return part_number_re.sub("", title) + + +def normalize_movement_name(work, movement): + if movement.startswith(work): + movement = movement[len(work):].lstrip(':').strip() + # TODO: Extract the actual number, compate it to ordering-key + return remove_part_number(movement) + + +def parse_work(work_rel): + work = Work(work_rel['title'], work_rel['id']) + if 'relations' in work_rel: + for rel in work_rel['relations']: + # If this work has parents and is linked to those as 'movement' or + # 'act' we consider it a part of a larger work and store it + # in the movement tag. The parent will be set as the work. + if is_parent_work(rel): + if is_movement_like(rel): + work.is_movement = True + work.part_number = rel['ordering-key'] + if 'work' in rel: + work.parent = parse_work(rel['work']) + work.parent.is_work = True + else: + # Not a movement, but still part of a larger work. + # Mark it as a work. + work.is_work = True + # If this work has any parts, we consider it a proper work. + # This is a recording directly linked to a larger work. + if is_child_work(rel): + work.is_work = True + return work + + +def unset_work(metadata): + metadata.set('work', '') + metadata.set('musicbrainz_workid', '') + metadata.set('movement', '') + metadata.set('movementnumber', '') + metadata.set('movementtotal', '') + metadata.set('showmovement', '') + + +def set_work(metadata, work): + metadata['work'] = work.title + metadata['musicbrainz_workid'] = work.id + metadata['showmovement'] = 1 + + +def process_track(album, metadata, track, release): + if 'recording' in track: + recording = track['recording'] + else: + recording = track + + if not 'relations' in recording: + return + + for rel in recording['relations']: + if is_performance_work(rel): + work = parse_work(rel['work']) + # Only use the first work that qualifies as a work or movement + log.debug('Found work:\n%s', work) + if work.is_movement or work.is_work: + break + + unset_work(metadata) + if work: + if work.is_movement and work.parent and work.parent.is_work: + movement = normalize_movement_name(work.parent.title, work.title) + metadata['movement'] = movement + metadata['movementnumber'] = work.part_number + set_work(metadata, work.parent) + elif work.is_work: + set_work(metadata, work) + + +register_track_metadata_processor(process_track) From 01f1502dd0b4a015ed449cd15fb73db1c414bba9 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 15 Nov 2018 08:09:24 +0100 Subject: [PATCH 056/123] workandmovement: Attempt to parse movement name from work or recording title --- .../{workandmovement.py => __init__.py} | 62 +++++++++----- plugins/workandmovement/roman.py | 80 +++++++++++++++++++ 2 files changed, 123 insertions(+), 19 deletions(-) rename plugins/workandmovement/{workandmovement.py => __init__.py} (72%) create mode 100644 plugins/workandmovement/roman.py diff --git a/plugins/workandmovement/workandmovement.py b/plugins/workandmovement/__init__.py similarity index 72% rename from plugins/workandmovement/workandmovement.py rename to plugins/workandmovement/__init__.py index dc612ad5..10a63cb5 100644 --- a/plugins/workandmovement/workandmovement.py +++ b/plugins/workandmovement/__init__.py @@ -28,12 +28,14 @@ import re +from .roman import fromRoman + from picard import log from picard.metadata import register_track_metadata_processor class Work: - def __init__(self, title, id): + def __init__(self, title, id=None): self.id = id self.title = title self.is_movement = False @@ -79,16 +81,36 @@ def is_child_work(rel): and rel['type'] == 'parts') -part_number_re = re.compile(r'^[0-9IVXLC]+\.\s+') -def remove_part_number(title): - return part_number_re.sub("", title) - - -def normalize_movement_name(work, movement): - if movement.startswith(work): - movement = movement[len(work):].lstrip(':').strip() - # TODO: Extract the actual number, compate it to ordering-key - return remove_part_number(movement) +_re_work_title = re.compile(r'(?P.*):\s+(?P[IVXLCDM]+)\.\s+(?P.*)') +def parse_work_name(title): + return _re_work_title.search(title) + + +def normalize_movement_name(work): + """ + Attempts to parse work.title in the form ": . ", + where is in Roman numerals. + Sets the `is_movement` and `part_number` properties on `work` and creates + a `parent` work if not already present. + """ + title = work.title + m = parse_work_name(title) + if m: + work.title = m.group('movement') + work.is_movement = True + number = fromRoman(m.group('movementnumber')) + if not work.part_number: + work.part_number = number + elif work.part_number != number: + log.warn('Movement number mismatch for "%s": %s != %i' % ( + title, m.group('movementnumber'), work.part_number)) + if not work.parent: + work.parent = Work(m.group('work')) + work.parent.is_work = True + elif work.parent.title != m.group('work'): + log.warn('Movement work name mismatch for "%s": "%s" != "%s"' % ( + title, m.group('work'), work.parent.title)) + return work def parse_work(work_rel): @@ -140,6 +162,7 @@ def process_track(album, metadata, track, release): if not 'relations' in recording: return + work = Work(recording['title']) for rel in recording['relations']: if is_performance_work(rel): work = parse_work(rel['work']) @@ -149,14 +172,15 @@ def process_track(album, metadata, track, release): break unset_work(metadata) - if work: - if work.is_movement and work.parent and work.parent.is_work: - movement = normalize_movement_name(work.parent.title, work.title) - metadata['movement'] = movement - metadata['movementnumber'] = work.part_number - set_work(metadata, work.parent) - elif work.is_work: - set_work(metadata, work) + work = normalize_movement_name(work) + + if work.is_movement and work.parent and work.parent.is_work: + movement = work.title + metadata['movement'] = movement + metadata['movementnumber'] = work.part_number + set_work(metadata, work.parent) + elif work.is_work: + set_work(metadata, work) register_track_metadata_processor(process_track) diff --git a/plugins/workandmovement/roman.py b/plugins/workandmovement/roman.py new file mode 100644 index 00000000..944b7263 --- /dev/null +++ b/plugins/workandmovement/roman.py @@ -0,0 +1,80 @@ +"""Convert to and from Roman numerals""" + +__author__ = "Mark Pilgrim (f8dy@diveintopython.org)" +__version__ = "1.4" +__date__ = "8 August 2001" +__copyright__ = """Copyright (c) 2001 Mark Pilgrim + +This program is part of "Dive Into Python", a free Python tutorial for +experienced programmers. Visit http://diveintopython.org/ for the +latest version. + +This program is free software; you can redistribute it and/or modify +it under the terms of the Python 2.1.1 license, available at +http://www.python.org/2.1.1/license.html +""" + +import re + +#Define exceptions +class RomanError(Exception): pass +class OutOfRangeError(RomanError): pass +class NotIntegerError(RomanError): pass +class InvalidRomanNumeralError(RomanError): pass + +#Define digit mapping +romanNumeralMap = (('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1)) + +def toRoman(n): + """convert integer to Roman numeral""" + if not isinstance(n, int): + raise NotIntegerError("decimals can not be converted") + if not (0 < n < 5000): + raise OutOfRangeError("number out of range (must be 1..4999)") + + result = "" + for numeral, integer in romanNumeralMap: + while n >= integer: + result += numeral + n -= integer + return result + +#Define pattern to detect valid Roman numerals +romanNumeralPattern = re.compile(""" + ^ # beginning of string + M{0,4} # thousands - 0 to 4 M's + (CM|CD|D?C{0,3}) # hundreds - 900 (CM), 400 (CD), 0-300 (0 to 3 C's), + # or 500-800 (D, followed by 0 to 3 C's) + (XC|XL|L?X{0,3}) # tens - 90 (XC), 40 (XL), 0-30 (0 to 3 X's), + # or 50-80 (L, followed by 0 to 3 X's) + (IX|IV|V?I{0,3}) # ones - 9 (IX), 4 (IV), 0-3 (0 to 3 I's), + # or 5-8 (V, followed by 0 to 3 I's) + $ # end of string + """ ,re.VERBOSE) + +def fromRoman(s): + """convert Roman numeral to integer""" + if not s: + raise InvalidRomanNumeralError('Input can not be blank') + if not romanNumeralPattern.search(s): + raise InvalidRomanNumeralError('Invalid Roman numeral: %s' % s) + + result = 0 + index = 0 + for numeral, integer in romanNumeralMap: + while s[index:index+len(numeral)] == numeral: + result += integer + index += len(numeral) + return result From 20ed4607a414923d177dc2cdef3c6469d8b855a2 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 15 Nov 2018 08:31:13 +0100 Subject: [PATCH 057/123] workandmovement: Handle Roman numeral errors --- plugins/workandmovement/__init__.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/workandmovement/__init__.py b/plugins/workandmovement/__init__.py index 10a63cb5..11632bf0 100644 --- a/plugins/workandmovement/__init__.py +++ b/plugins/workandmovement/__init__.py @@ -28,7 +28,7 @@ import re -from .roman import fromRoman +from .roman import fromRoman, RomanError from picard import log from picard.metadata import register_track_metadata_processor @@ -98,7 +98,11 @@ def normalize_movement_name(work): if m: work.title = m.group('movement') work.is_movement = True - number = fromRoman(m.group('movementnumber')) + try: + number = fromRoman(m.group('movementnumber')) + except RomanError as e: + log.error(e) + number = 0 if not work.part_number: work.part_number = number elif work.part_number != number: @@ -177,7 +181,8 @@ def process_track(album, metadata, track, release): if work.is_movement and work.parent and work.parent.is_work: movement = work.title metadata['movement'] = movement - metadata['movementnumber'] = work.part_number + if work.part_number: + metadata['movementnumber'] = work.part_number set_work(metadata, work.parent) elif work.is_work: set_work(metadata, work) From d27321265fe653626ccbaf088fe1ba5dd019a9d6 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 15 Nov 2018 11:05:24 +0100 Subject: [PATCH 058/123] workandmovement: Normalize movement titles that do not follow the strict naming scheme --- plugins/workandmovement/__init__.py | 73 ++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/plugins/workandmovement/__init__.py b/plugins/workandmovement/__init__.py index 11632bf0..65232eda 100644 --- a/plugins/workandmovement/__init__.py +++ b/plugins/workandmovement/__init__.py @@ -28,7 +28,10 @@ import re -from .roman import fromRoman, RomanError +from .roman import ( + fromRoman, + RomanError, +) from picard import log from picard.metadata import register_track_metadata_processor @@ -81,12 +84,25 @@ def is_child_work(rel): and rel['type'] == 'parts') +def number_to_int(s): + """ + Converts a numeric string to int. `s` can also be a Roman numeral. + """ + try: + return int(s) + except ValueError: + try: + return fromRoman(s) + except RomanError as e: + raise ValueError(e) + + _re_work_title = re.compile(r'(?P.*):\s+(?P[IVXLCDM]+)\.\s+(?P.*)') def parse_work_name(title): return _re_work_title.search(title) -def normalize_movement_name(work): +def create_work_and_movement_from_title(work): """ Attempts to parse work.title in the form ": . ", where is in Roman numerals. @@ -94,29 +110,53 @@ def normalize_movement_name(work): a `parent` work if not already present. """ title = work.title - m = parse_work_name(title) - if m: - work.title = m.group('movement') + match = parse_work_name(title) + if match: + work.title = match.group('movement') work.is_movement = True try: - number = fromRoman(m.group('movementnumber')) - except RomanError as e: + number = number_to_int(match.group('movementnumber')) + except ValueError as e: log.error(e) number = 0 if not work.part_number: work.part_number = number elif work.part_number != number: - log.warn('Movement number mismatch for "%s": %s != %i' % ( - title, m.group('movementnumber'), work.part_number)) + log.warning('Movement number mismatch for "%s": %s != %i' % ( + title, match.group('movementnumber'), work.part_number)) if not work.parent: - work.parent = Work(m.group('work')) + work.parent = Work(match.group('work')) work.parent.is_work = True - elif work.parent.title != m.group('work'): - log.warn('Movement work name mismatch for "%s": "%s" != "%s"' % ( - title, m.group('work'), work.parent.title)) + elif work.parent.title != match.group('work'): + log.warning('Movement work name mismatch for "%s": "%s" != "%s"' % ( + title, match.group('work'), work.parent.title)) return work +_re_part_number = re.compile(r'(?P[0-9IVXLCDM]+)\.?\s+') +def normalize_movement_title(work): + """ + Removes the parent work title and part number from the beginning of `work.title`. + This ensures movement names don't contain duplicated information even if + they do not follow the strict naming format used by `create_work_and_movement_from_title`. + """ + movement_title = work.title + if work.parent: + work_title = work.parent.title + if movement_title.startswith(work_title): + movement_title = movement_title[len(work_title):].lstrip(':').strip() + match = _re_part_number.match(movement_title) + if match: + # Only remove the number if it matches the part_number + try: + number = number_to_int(match.group('number')) + if number == work.part_number: + movement_title = _re_part_number.sub("", movement_title) + except ValueError as e: + log.warning(e) + return movement_title + + def parse_work(work_rel): work = Work(work_rel['title'], work_rel['id']) if 'relations' in work_rel: @@ -131,6 +171,7 @@ def parse_work(work_rel): if 'work' in rel: work.parent = parse_work(rel['work']) work.parent.is_work = True + work.title = normalize_movement_title(work) else: # Not a movement, but still part of a larger work. # Mark it as a work. @@ -176,11 +217,11 @@ def process_track(album, metadata, track, release): break unset_work(metadata) - work = normalize_movement_name(work) + if not work.is_movement: + work = create_work_and_movement_from_title(work) if work.is_movement and work.parent and work.parent.is_work: - movement = work.title - metadata['movement'] = movement + metadata['movement'] = work.title if work.part_number: metadata['movementnumber'] = work.part_number set_work(metadata, work.parent) From bc022b1a9002fca02fcfb28d2fe111919e816e0c Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 15 Nov 2018 14:18:25 +0100 Subject: [PATCH 059/123] workandmovement: Mark unneeded work tags as deleted --- plugins/workandmovement/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/workandmovement/__init__.py b/plugins/workandmovement/__init__.py index 65232eda..97eb0b8b 100644 --- a/plugins/workandmovement/__init__.py +++ b/plugins/workandmovement/__init__.py @@ -184,12 +184,12 @@ def parse_work(work_rel): def unset_work(metadata): - metadata.set('work', '') - metadata.set('musicbrainz_workid', '') - metadata.set('movement', '') - metadata.set('movementnumber', '') - metadata.set('movementtotal', '') - metadata.set('showmovement', '') + metadata.delete('work') + metadata.delete('musicbrainz_workid') + metadata.delete('movement') + metadata.delete('movementnumber') + metadata.delete('movementtotal') + metadata.delete('showmovement') def set_work(metadata, work): From a9c46efb6251f4bbfc0c28aec68e9a7e268310d5 Mon Sep 17 00:00:00 2001 From: Bob Swift Date: Thu, 15 Nov 2018 15:48:29 -0700 Subject: [PATCH 060/123] Force map to list for compatibility. --- plugins/smart_title_case/smart_title_case.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/smart_title_case/smart_title_case.py b/plugins/smart_title_case/smart_title_case.py index 449c5914..4c8f7ba7 100644 --- a/plugins/smart_title_case/smart_title_case.py +++ b/plugins/smart_title_case/smart_title_case.py @@ -27,7 +27,7 @@ For Artist/AlbumArtist, title cases only artists not join phrases
e.g. The Beatles feat. The Who. """ -PLUGIN_VERSION = "0.2" +PLUGIN_VERSION = "0.3" PLUGIN_API_VERSIONS = ["2.0"] PLUGIN_LICENSE = "GPL-3.0" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-3.0.html" @@ -117,7 +117,7 @@ def title_case(tagger, metadata, release, track=None): if artist_string in metadata and artists_list in metadata: artist = metadata.getall(artist_string) artists = metadata.getall(artists_list) - new_artists = map(string_title_case, artists) + new_artists = list(map(string_title_case, artists)) new_artist = [artist_title_case(x, artists, new_artists) for x in artist] if artists != new_artists and artist != new_artist: log.debug("SmartTitleCase: %s: %s replaced with %s", artist_string, artist, new_artist) From 21a590616b8d703f690261e8ba473f383675a648 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 18 Nov 2018 22:27:04 +0100 Subject: [PATCH 061/123] wikidata: Code cleanup --- plugins/wikidata/wikidata.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/plugins/wikidata/wikidata.py b/plugins/wikidata/wikidata.py index 7568df9c..7cc902f8 100644 --- a/plugins/wikidata/wikidata.py +++ b/plugins/wikidata/wikidata.py @@ -13,22 +13,23 @@ PLUGIN_LICENSE = 'WTFPL' PLUGIN_LICENSE_URL = 'http://www.wtfpl.net/' -from picard import config, log -from picard.metadata import register_track_metadata_processor from functools import partial import threading +from picard import config, log +from picard.metadata import register_track_metadata_processor -class wikidata: + +class Wikidata: def __init__(self): self.lock = threading.Lock() # Key: mbid, value: List of metadata entries to be updated when we have parsed everything self.requests = {} - + # Key: mbid, value: List of items to track the number of outstanding requests self.albums = {} - + # cache, items that have been found # key: mbid, value: list of strings containing the genre's self.cache = {} @@ -66,9 +67,9 @@ def process_request(self, metadata, album, item_id, type): log.debug('WIKIDATA: Looking up cache for item %s' % item_id) log.debug('WIKIDATA: requests %s' % album._requests) log.debug('WIKIDATA: TYPE %s' % type) - if item_id in list(self.cache.keys()): + if item_id in self.cache: log.info('WIKIDATA: found in cache') - genre_list = self.cache.get(item_id) + genre_list = self.cache[item_id] new_genre = set(metadata.getall("genre")) new_genre.update(genre_list) #sort the new genre list so that they don't appear as new entries (not a change) next time @@ -77,7 +78,7 @@ def process_request(self, metadata, album, item_id, type): else: # pending requests are handled by adding the metadata object to a # list of things to be updated when the genre is found - if item_id in list(self.albums.keys()): + if item_id in self.albums: log.debug( 'WIKIDATA: request already pending, add it to the list of items to update once this has been found') self.requests[item_id].append(metadata) @@ -210,24 +211,24 @@ def process_track(self, album, metadata, trackXmlNode, releaseXmlNode): self.ws = album.tagger.webservice self.log = album.log - for release_group in dict.get(metadata, 'musicbrainz_releasegroupid', []): + for release_group in metadata.getall('musicbrainz_releasegroupid'): log.debug('WIKIDATA: looking up release group metadata for %s ' % release_group) self.process_request(metadata, album, release_group, type='release-group') - for artist in dict.get(metadata, 'musicbrainz_albumartistid', []): + for artist in metadata.getall('musicbrainz_albumartistid'): log.info('WIKIDATA: processing release artist %s' % artist) self.process_request(metadata, album, artist, type='artist') - for artist in dict.get(metadata, 'musicbrainz_artistid'): + for artist in metadata.getall('musicbrainz_artistid'): log.info('WIKIDATA: processing track artist %s' % artist) self.process_request(metadata, album, artist, type='artist') if 'musicbrainz_workid' in metadata: - for workid in dict.get(metadata, 'musicbrainz_workid'): + for workid in metadata.getall('musicbrainz_workid'): log.info('WIKIDATA: processing track artist %s' % workid) self.process_request(metadata, album, workid, type='work') -wikidata = wikidata() +wikidata = Wikidata() # register_album_metadata_processor(wikidata.process_release) register_track_metadata_processor(wikidata.process_track) From a12e22b710c6f5419e8213858411d96b8c676b35 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 25 Nov 2018 17:34:01 +0100 Subject: [PATCH 062/123] decode_cyrillic: Handle both encoding and decoding errors. Fixes PICARD-1421 --- plugins/decode_cyrillic/decode_cyrillic.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/plugins/decode_cyrillic/decode_cyrillic.py b/plugins/decode_cyrillic/decode_cyrillic.py index be6fc85b..a485440c 100644 --- a/plugins/decode_cyrillic/decode_cyrillic.py +++ b/plugins/decode_cyrillic/decode_cyrillic.py @@ -25,12 +25,12 @@ PLUGIN_NAME = "Decode Cyrillic" PLUGIN_AUTHOR = "aeontech" PLUGIN_DESCRIPTION = ''' -This plugin helps you quickly convert mis-encoded cyrillic Windows-1251 tags +This plugin helps you quickly convert mis-encoded cyrillic Windows-1251 tags to proper UTF-8 encoded strings. If your track/album names look something like "Àëèñà â ñò›àíå ÷óäåñ", run this plugin from the context menu before running the "Lookup" or "Scan" tools ''' -PLUGIN_VERSION = "1.0" +PLUGIN_VERSION = "1.1" PLUGIN_API_VERSIONS = ["1.0", "2.0"] PLUGIN_LICENSE = "MIT" PLUGIN_LICENSE_URL = "https://opensource.org/licenses/MIT" @@ -40,10 +40,10 @@ from picard.ui.itemviews import BaseAction, register_cluster_action _decode_tags = [ - 'title', - 'albumartist', - 'artist', - 'album', + 'title', + 'albumartist', + 'artist', + 'album', 'artistsort' ] # _from_encoding = "latin1" @@ -52,7 +52,7 @@ # TODO: # - extend to support multiple codepage decoding, not just cp1251->latin1 -# instead, try the common variations, and show a dialog to the user, +# instead, try the common variations, and show a dialog to the user, # allowing him to select the correct transcoding. See 2cyr.com for example. # - also see http://stackoverflow.com/questions/23326531/how-to-decode-cp1252-string @@ -61,8 +61,9 @@ class DecodeCyrillic(BaseAction): def unmangle(self, tag, value): try: + print(value, value.encode('latin1')) unmangled_value = value.encode('latin1').decode('cp1251') - except UnicodeEncodeError: + except UnicodeError: unmangled_value = value log.debug("%s: could not unmangle tag %s; original value: %s" % (PLUGIN_NAME, tag, value)) return unmangled_value @@ -84,7 +85,7 @@ def callback(self, objs): log.debug("%s: Trying to unmangle file - original metadata %s" % (PLUGIN_NAME, file.orig_metadata)) - for tag in _decode_tags: + for tag in _decode_tags: if not (tag in file.metadata): continue From edd57a80df04f7d036d52c931a4b6fc66be983cf Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 27 Nov 2018 07:35:40 +0100 Subject: [PATCH 063/123] workandmovement: Style fixes --- plugins/workandmovement/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/workandmovement/__init__.py b/plugins/workandmovement/__init__.py index 97eb0b8b..46c69666 100644 --- a/plugins/workandmovement/__init__.py +++ b/plugins/workandmovement/__init__.py @@ -38,8 +38,8 @@ class Work: - def __init__(self, title, id=None): - self.id = id + def __init__(self, title, mbid=None): + self.mbid = mbid self.title = title self.is_movement = False self.is_work = False @@ -51,12 +51,12 @@ def __str__(self): if self.parent: s.append(str(self.parent)) if self.is_movement: - type = 'Movement' + work_type = 'Movement' elif self.is_work: - type = 'Work' + work_type = 'Work' else: - type = 'Unknown' - s.append('%s %i: %s' % (type, self.part_number, self.title)) + work_type = 'Unknown' + s.append('%s %i: %s' % (work_type, self.part_number, self.title)) return '\n'.join(s) @@ -194,7 +194,7 @@ def unset_work(metadata): def set_work(metadata, work): metadata['work'] = work.title - metadata['musicbrainz_workid'] = work.id + metadata['musicbrainz_workid'] = work.mbid metadata['showmovement'] = 1 From 1f7bf88e98bd8ed9fc5df10f1dbf577b8ce9cbd4 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 27 Nov 2018 07:51:10 +0100 Subject: [PATCH 064/123] Correctly specify license as GPL-2.0-or-later The updated plugins, fanarttv, papercdcase and videotools, always were GP 2.0 or later, see the license header. --- plugins/fanarttv/__init__.py | 4 ++-- plugins/papercdcase/papercdcase.py | 4 ++-- plugins/videotools/__init__.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/fanarttv/__init__.py b/plugins/fanarttv/__init__.py index 67633e33..b9d3d049 100644 --- a/plugins/fanarttv/__init__.py +++ b/plugins/fanarttv/__init__.py @@ -20,9 +20,9 @@ PLUGIN_NAME = 'fanart.tv cover art' PLUGIN_AUTHOR = 'Philipp Wolfer, Sambhav Kothari' PLUGIN_DESCRIPTION = 'Use cover art from fanart.tv. To use this plugin you have to register a personal API key on https://fanart.tv/get-an-api-key/' -PLUGIN_VERSION = "1.4" +PLUGIN_VERSION = "1.5" PLUGIN_API_VERSIONS = ["2.0"] -PLUGIN_LICENSE = "GPL-2.0" +PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" from functools import partial diff --git a/plugins/papercdcase/papercdcase.py b/plugins/papercdcase/papercdcase.py index b1d0f992..bd2fcb22 100644 --- a/plugins/papercdcase/papercdcase.py +++ b/plugins/papercdcase/papercdcase.py @@ -20,9 +20,9 @@ PLUGIN_NAME = 'Paper CD case' PLUGIN_AUTHOR = 'Philipp Wolfer, Sambhav Kothari' PLUGIN_DESCRIPTION = 'Create a paper CD case from an album or cluster using http://papercdcase.com' -PLUGIN_VERSION = "1.1" +PLUGIN_VERSION = "1.2" PLUGIN_API_VERSIONS = ["2.0"] -PLUGIN_LICENSE = "GPL-2.0" +PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" diff --git a/plugins/videotools/__init__.py b/plugins/videotools/__init__.py index 42c741b7..209171e8 100644 --- a/plugins/videotools/__init__.py +++ b/plugins/videotools/__init__.py @@ -20,9 +20,9 @@ PLUGIN_NAME = 'Video tools' PLUGIN_AUTHOR = 'Philipp Wolfer' PLUGIN_DESCRIPTION = 'Improves the video support in Picard by adding support for Matroska, WebM, AVI, QuickTime and MPEG files (renaming and fingerprinting only, no tagging) and providing $is_audio() and $is_video() scripting functions.' -PLUGIN_VERSION = "0.2" +PLUGIN_VERSION = "0.3" PLUGIN_API_VERSIONS = ["1.3.0", "2.0"] -PLUGIN_LICENSE = "GPL-2.0" +PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" from picard.formats import register_format From 9d8b6b3018792f4f06f29ead33ef99c5dc4ed6d5 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 27 Nov 2018 07:52:49 +0100 Subject: [PATCH 065/123] More GPL 2 or later fixes This is not a license change, the affected plugins always stated this license in the headers. For a long time the PLUGIN_LICENSE was not set this specific. --- plugins/abbreviate_artistsort/abbreviate_artistsort.py | 4 ++-- plugins/albumartistextension/albumartistextension.py | 2 +- plugins/compatible_TXXX/compatible_TXXX.py | 2 +- plugins/fix_tracknums/fix_tracknums.py | 4 ++-- plugins/keep/keep.py | 2 +- plugins/non_ascii_equivalents/non_ascii_equivalents.py | 4 ++-- plugins/padded/padded.py | 2 +- plugins/playlist/playlist.py | 4 ++-- plugins/reorder_sides/reorder_sides.py | 4 ++-- plugins/save_and_rewrite_header/save_and_rewrite_header.py | 4 ++-- plugins/smart_title_case/smart_title_case.py | 2 +- plugins/sort_multivalue_tags/sort_multivalue_tags.py | 4 ++-- 12 files changed, 19 insertions(+), 19 deletions(-) diff --git a/plugins/abbreviate_artistsort/abbreviate_artistsort.py b/plugins/abbreviate_artistsort/abbreviate_artistsort.py index a26392e2..ba8a62d2 100644 --- a/plugins/abbreviate_artistsort/abbreviate_artistsort.py +++ b/plugins/abbreviate_artistsort/abbreviate_artistsort.py @@ -20,9 +20,9 @@ This is particularly useful for classical albums that can have a long list of artists. %artistsort% is abbreviated into %_artistsort_abbrev% and %albumartistsort% is abbreviated into %_albumartistsort_abbrev%.''' -PLUGIN_VERSION = "0.3" +PLUGIN_VERSION = "0.4" PLUGIN_API_VERSIONS = ["1.0", "2.0"] -PLUGIN_LICENSE = "GPL-2.0" +PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" diff --git a/plugins/albumartistextension/albumartistextension.py b/plugins/albumartistextension/albumartistextension.py index 68da5250..0bb7727d 100644 --- a/plugins/albumartistextension/albumartistextension.py +++ b/plugins/albumartistextension/albumartistextension.py @@ -25,7 +25,7 @@ PLUGIN_VERSION = "0.6" PLUGIN_API_VERSIONS = ["2.0"] -PLUGIN_LICENSE = "GPL-2.0 or later" +PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" from picard import config, log diff --git a/plugins/compatible_TXXX/compatible_TXXX.py b/plugins/compatible_TXXX/compatible_TXXX.py index ab931c8e..fd49088c 100644 --- a/plugins/compatible_TXXX/compatible_TXXX.py +++ b/plugins/compatible_TXXX/compatible_TXXX.py @@ -7,7 +7,7 @@ technically don't comply with the ID3 specification.""" PLUGIN_VERSION = "0.1" PLUGIN_API_VERSIONS = ["2.0"] -PLUGIN_LICENSE = "GPL-2.0 or later" +PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" from picard import config diff --git a/plugins/fix_tracknums/fix_tracknums.py b/plugins/fix_tracknums/fix_tracknums.py index 3a9de47a..4f990910 100644 --- a/plugins/fix_tracknums/fix_tracknums.py +++ b/plugins/fix_tracknums/fix_tracknums.py @@ -54,9 +54,9 @@ ''' -PLUGIN_VERSION = '0.1' +PLUGIN_VERSION = '0.2' PLUGIN_API_VERSIONS = ['0.15', '1.0', '2.0'] -PLUGIN_LICENSE = 'GPL-3.0' +PLUGIN_LICENSE = 'GPL-3.0-or-later' PLUGIN_LICENSE_URL = 'https://www.gnu.org/licenses/gpl.txt' from picard import log diff --git a/plugins/keep/keep.py b/plugins/keep/keep.py index ef636765..d277ce77 100644 --- a/plugins/keep/keep.py +++ b/plugins/keep/keep.py @@ -8,7 +8,7 @@ PLUGIN_VERSION = "1.1" PLUGIN_API_VERSIONS = ["0.15.0", "0.15.1", "0.16.0", "1.0.0", "1.1.0", "1.2.0", "1.3.0", "2.0"] -PLUGIN_LICENSE = "GPL-2.0 or later" +PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" from picard.script import register_script_function diff --git a/plugins/non_ascii_equivalents/non_ascii_equivalents.py b/plugins/non_ascii_equivalents/non_ascii_equivalents.py index ae002f0a..f40f6fe4 100644 --- a/plugins/non_ascii_equivalents/non_ascii_equivalents.py +++ b/plugins/non_ascii_equivalents/non_ascii_equivalents.py @@ -19,9 +19,9 @@ PLUGIN_NAME = "Non-ASCII Equivalents" PLUGIN_AUTHOR = "Anderson Mesquita " -PLUGIN_VERSION = "0.1" +PLUGIN_VERSION = "0.2" PLUGIN_API_VERSIONS = ["0.9", "0.10", "0.11", "0.15", "2.0"] -PLUGIN_LICENSE = "GPLv3" +PLUGIN_LICENSE = "GPL-3.0-or-later" PLUGIN_LICENSE_URL = "https://gnu.org/licenses/gpl.html" PLUGIN_DESCRIPTION = '''Replaces accented and otherwise non-ASCII characters with a somewhat equivalent version of their ASCII counterparts. This allows old diff --git a/plugins/padded/padded.py b/plugins/padded/padded.py index d6fbb9db..efe2e1b9 100644 --- a/plugins/padded/padded.py +++ b/plugins/padded/padded.py @@ -8,7 +8,7 @@ PLUGIN_VERSION = "1.0" PLUGIN_API_VERSIONS = ["0.15.0", "0.15.1", "0.16.0", "1.0.0", "1.1.0", "1.2.0", "1.3.0", "2.0", ] -PLUGIN_LICENSE = "GPL-2.0 or later" +PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" from picard.metadata import register_track_metadata_processor diff --git a/plugins/playlist/playlist.py b/plugins/playlist/playlist.py index d7d9ee72..f48bcdca 100644 --- a/plugins/playlist/playlist.py +++ b/plugins/playlist/playlist.py @@ -16,9 +16,9 @@ PLUGIN_DESCRIPTION = """Generate an Extended M3U playlist (.m3u8 file, UTF8 encoded text). Relative pathnames are used where audio files are in the same directory as the playlist, otherwise absolute (full) pathnames are used.""" -PLUGIN_VERSION = "1.0" +PLUGIN_VERSION = "1.1" PLUGIN_API_VERSIONS = ["2.0"] -PLUGIN_LICENSE = "GPL-2.0" +PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" import os.path diff --git a/plugins/reorder_sides/reorder_sides.py b/plugins/reorder_sides/reorder_sides.py index 23e487f4..d486a3bd 100644 --- a/plugins/reorder_sides/reorder_sides.py +++ b/plugins/reorder_sides/reorder_sides.py @@ -28,9 +28,9 @@ changers (https://en.wikipedia.org/wiki/Record_changer#Automatic_sequencing) play in the correct order.""" -PLUGIN_VERSION = '1.0' +PLUGIN_VERSION = '1.1' PLUGIN_API_VERSIONS = ['2.0'] -PLUGIN_LICENSE = 'GPL-3.0' +PLUGIN_LICENSE = 'GPL-3.0-or-later' PLUGIN_LICENSE_URL = 'https://www.gnu.org/licenses/gpl-3.0.html' import collections diff --git a/plugins/save_and_rewrite_header/save_and_rewrite_header.py b/plugins/save_and_rewrite_header/save_and_rewrite_header.py index 869a8109..4e10206f 100644 --- a/plugins/save_and_rewrite_header/save_and_rewrite_header.py +++ b/plugins/save_and_rewrite_header/save_and_rewrite_header.py @@ -23,9 +23,9 @@ PLUGIN_NAME = "Save and rewrite header" PLUGIN_AUTHOR = "Nicolas Cenerario" PLUGIN_DESCRIPTION = "This plugin adds a context menu action to save files and rewrite their header." -PLUGIN_VERSION = "0.2" +PLUGIN_VERSION = "0.3" PLUGIN_API_VERSIONS = ["0.9.0", "0.10", "0.15", "2.0"] -PLUGIN_LICENSE = "GPL-3.0" +PLUGIN_LICENSE = "GPL-3.0-or-later" PLUGIN_LICENSE_URL = "http://www.gnu.org/licenses/gpl-3.0.txt" from _functools import partial diff --git a/plugins/smart_title_case/smart_title_case.py b/plugins/smart_title_case/smart_title_case.py index 4c8f7ba7..eceb7ef8 100644 --- a/plugins/smart_title_case/smart_title_case.py +++ b/plugins/smart_title_case/smart_title_case.py @@ -29,7 +29,7 @@ """ PLUGIN_VERSION = "0.3" PLUGIN_API_VERSIONS = ["2.0"] -PLUGIN_LICENSE = "GPL-3.0" +PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-3.0.html" import re, unicodedata diff --git a/plugins/sort_multivalue_tags/sort_multivalue_tags.py b/plugins/sort_multivalue_tags/sort_multivalue_tags.py index c5f92f98..020a1f67 100644 --- a/plugins/sort_multivalue_tags/sort_multivalue_tags.py +++ b/plugins/sort_multivalue_tags/sort_multivalue_tags.py @@ -23,9 +23,9 @@
  • The sequence of one tag is linked to the sequence of another e.g. Label and Catalogue number.
  • ''' -PLUGIN_VERSION = "0.3" +PLUGIN_VERSION = "0.4" PLUGIN_API_VERSIONS = ["0.15", "2.0"] -PLUGIN_LICENSE = "GPL-2.0" +PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" from picard.metadata import register_track_metadata_processor From 636c9f1b87f63ba4d1f4ba04bec2ee25c5ff8678 Mon Sep 17 00:00:00 2001 From: Paul Brackin Date: Wed, 5 Dec 2018 11:20:33 -0800 Subject: [PATCH 066/123] Update plugins/wikidata/wikidata.py Made the albums dict a straight dict of albums (now called itemAlbums), ensured itemAlbums and requests dicts are empty after each run, removed locks, cleaned up logging. --- plugins/wikidata/wikidata.py | 165 +++++++++++++++++------------------ 1 file changed, 81 insertions(+), 84 deletions(-) diff --git a/plugins/wikidata/wikidata.py b/plugins/wikidata/wikidata.py index 7cc902f8..932a1fbe 100644 --- a/plugins/wikidata/wikidata.py +++ b/plugins/wikidata/wikidata.py @@ -8,14 +8,12 @@ PLUGIN_NAME = 'wikidata-genre' PLUGIN_AUTHOR = 'Daniel Sobey, Sambhav Kothari' PLUGIN_DESCRIPTION = 'query wikidata to get genre tags' -PLUGIN_VERSION = '1.1' +PLUGIN_VERSION = '1.2' PLUGIN_API_VERSIONS = ["2.0"] PLUGIN_LICENSE = 'WTFPL' PLUGIN_LICENSE_URL = 'http://www.wtfpl.net/' from functools import partial -import threading - from picard import config, log from picard.metadata import register_track_metadata_processor @@ -23,12 +21,11 @@ class Wikidata: def __init__(self): - self.lock = threading.Lock() # Key: mbid, value: List of metadata entries to be updated when we have parsed everything self.requests = {} # Key: mbid, value: List of items to track the number of outstanding requests - self.albums = {} + self.itemAlbums = {} # cache, items that have been found # key: mbid, value: list of strings containing the genre's @@ -48,60 +45,59 @@ def process_release(self, album, metadata, release): self.log = album.log item_id = dict.get(metadata, 'musicbrainz_releasegroupid')[0] - log.info('WIKIDATA: processing release group %s ' % item_id) + log.info('WIKIDATA: Processing release group %s ' % item_id) self.process_request(metadata, album, item_id, type='release-group') for artist in dict.get(metadata, 'musicbrainz_albumartistid'): item_id = artist - log.info('WIKIDATA: processing release artist %s' % item_id) + log.info('WIKIDATA: Processing release artist %s' % item_id) self.process_request(metadata, album, item_id, type='artist') # Main processing function # First see if we have already found what we need in the cache, finalize loading # Next see if we are already looking for the item - # If we are add this item to the list of items to be updated once we find what we are looking for. + # If we are, add this item to the list of items to be updated once we find what we are looking for. # Otherwise we are the first one to look up this item, start a new request # metadata, map containing the new metadata # def process_request(self, metadata, album, item_id, type): - with self.lock: - log.debug('WIKIDATA: Looking up cache for item %s' % item_id) - log.debug('WIKIDATA: requests %s' % album._requests) - log.debug('WIKIDATA: TYPE %s' % type) - if item_id in self.cache: - log.info('WIKIDATA: found in cache') - genre_list = self.cache[item_id] - new_genre = set(metadata.getall("genre")) - new_genre.update(genre_list) - #sort the new genre list so that they don't appear as new entries (not a change) next time - metadata["genre"] = sorted(new_genre) - return + log.debug('WIKIDATA: Looking up cache for item: %s' % item_id) + log.debug('WIKIDATA: Album request count: %s' % album._requests) + log.debug('WIKIDATA: Item type %s' % type) + if item_id in self.cache: + log.debug('WIKIDATA: Found item in cache') + genre_list = self.cache[item_id] + new_genre = set(metadata.getall("genre")) + new_genre.update(genre_list) + #sort the new genre list so that they don't appear as new entries (not a change) next time + metadata["genre"] = sorted(new_genre) + return + else: + # pending requests are handled by adding the metadata object to a + # list of things to be updated when the genre is found + if item_id in self.itemAlbums: + log.debug( + 'WIKIDATA: Request already pending, add it to the list of items to update once this has been' + 'found') + self.requests[item_id].append(metadata) else: - # pending requests are handled by adding the metadata object to a - # list of things to be updated when the genre is found - if item_id in self.albums: - log.debug( - 'WIKIDATA: request already pending, add it to the list of items to update once this has been found') - self.requests[item_id].append(metadata) - - else: - self.requests[item_id] = [metadata] - album._requests += 1 - self.albums[item_id] = [album] + self.requests[item_id] = [metadata] + self.itemAlbums[item_id] = album + album._requests += 1 - log.debug('WIKIDATA: first request for this item') - log.info('WIKIDATA: about to call musicbrainz to look up %s ' % item_id) + log.debug('WIKIDATA: First request for this item') + log.debug('WIKIDATA: About to call Musicbrainz to look up %s ' % item_id) - path = '/ws/2/%s/%s' % (type, item_id) - queryargs = {"inc": "url-rels"} + path = '/ws/2/%s/%s' % (type, item_id) + queryargs = {"inc": "url-rels"} - self.ws.get(self.mb_host, self.mb_port, path, partial(self.musicbrainz_release_lookup, item_id, - metadata), - parse_response_type="xml", priority=False, important=False, queryargs=queryargs) + self.ws.get(self.mb_host, self.mb_port, path, partial(self.musicbrainz_release_lookup, item_id, + metadata), + parse_response_type="xml", priority=False, important=False, queryargs=queryargs) def musicbrainz_release_lookup(self, item_id, metadata, response, reply, error): found = False if error: - log.info('WIKIDATA: error retrieving release group info') + log.error('WIKIDATA: Error retrieving release group info') else: if 'metadata' in response.children: if 'release_group' in response.metadata[0].children: @@ -126,22 +122,24 @@ def musicbrainz_release_lookup(self, item_id, metadata, response, reply, error): wikidata_url = relation.target[0].text self.process_wikidata(wikidata_url, item_id) if not found: - log.info('WIKIDATA: no wikidata url found for item_id: %s ', item_id) - with self.lock: - for album in self.albums[item_id]: - album._requests -= 1 - log.debug('WIKIDATA: TOTAL REMAINING REQUESTS %s' % album._requests) - if not album._requests: - self.albums[item_id].remove(album) - album._finalize_loading(None) + log.debug('WIKIDATA: No wikidata url found for item_id: %s ', item_id) + + album = self.itemAlbums[item_id] + album._requests -= 1 + if not album._requests: + self.itemAlbums = {k: v for k, v in self.itemAlbums.items() if v != album} + album._finalize_loading(None) + log.info('WIKIDATA: Total remaining requests: %s' % album._requests) + if not self.itemAlbums: + self.requests.clear() + log.info('WIKIDATA: Finished.') def process_wikidata(self, wikidata_url, item_id): - with self.lock: - for album in self.albums[item_id]: - album._requests += 1 + album = self.itemAlbums[item_id] + album._requests += 1 item = wikidata_url.split('/')[4] path = "/wiki/Special:EntityData/" + item + ".rdf" - log.info('WIKIDATA: fetching the folowing url wikidata.org%s' % path) + log.debug('WIKIDATA: Fetching from wikidata.org%s' % path) self.ws.get('www.wikidata.org', 443, path, partial(self.parse_wikidata_response, item, item_id), parse_response_type="xml", priority=False, important=False) @@ -164,7 +162,7 @@ def parse_wikidata_response(self, item, item_id, response, reply, error): tmp = i.attribs.get('resource') if 'entity' == tmp.split('/')[3] and len(tmp.split('/')) == 5: genre_id = tmp.split('/')[4] - log.info( + log.debug( 'WIKIDATA: Found the wikidata id for the genre: %s' % genre_id) genre_entries.append(tmp) else: @@ -175,35 +173,34 @@ def parse_wikidata_response(self, item, item_id, response, reply, error): if node2.attribs.get('lang') == 'en': genre = node2.text.title() genre_list.append(genre) - log.debug( - 'Our genre is: %s' % genre) - - with self.lock: - if len(genre_list) > 0: - log.info('WIKIDATA: final list of wikidata id found: %s' % genre_entries) - log.info('WIKIDATA: final list of genre: %s' % genre_list) - - log.debug('WIKIDATA: total items to update: %s ' % - len(self.requests[item_id])) - for metadata in self.requests[item_id]: - new_genre = set(metadata.getall("genre")) - new_genre.update(genre_list) - # sort the new genre list so that they don't appear as new entries (not a change) next time - metadata["genre"] = sorted(new_genre) - self.cache[item_id] = genre_list - log.debug('WIKIDATA: setting genre : %s ' % genre_list) - - else: - log.info('WIKIDATA: Genre not found in wikidata') + log.debug('Our genre is: %s' % genre) + + if len(genre_list) > 0: + log.debug('WIKIDATA: item_id: %s' % item_id) + log.debug('WIKIDATA: Final list of wikidata id found: %s' % genre_entries) + log.debug('WIKIDATA: Final list of genre: %s' % genre_list) + log.info('WIKIDATA: Total items to update: %d ' % len(self.requests[item_id])) + for metadata in self.requests[item_id]: + new_genre = set(metadata.getall("genre")) + new_genre.update(genre_list) + # sort the new genre list so that they don't appear as new entries (not a change) next time + metadata["genre"] = sorted(new_genre) + self.cache[item_id] = genre_list + log.debug('WIKIDATA: Setting genre: %s ' % genre_list) + else: + log.debug('WIKIDATA: Genre not found in wikidata') - log.info('WIKIDATA: Seeing if we can finalize tags %d ' % len(self.albums[item_id])) + log.debug('WIKIDATA: Seeing if we can finalize tags...') - for album in self.albums[item_id]: - album._requests -= 1 - if not album._requests: - self.albums[item_id].remove(album) - album._finalize_loading(None) - log.info('WIKIDATA: TOTAL REMAINING REQUESTS %s' % album._requests) + album = self.itemAlbums[item_id] + album._requests -= 1 + if not album._requests: + self.itemAlbums = {k: v for k, v in self.itemAlbums.items() if v != album} + album._finalize_loading(None) + log.info('WIKIDATA: Total remaining requests: %s' % album._requests) + if not self.itemAlbums: + self.requests.clear() + log.info('WIKIDATA: Finished.') def process_track(self, album, metadata, trackXmlNode, releaseXmlNode): self.mb_host = config.setting["server_host"] @@ -211,24 +208,24 @@ def process_track(self, album, metadata, trackXmlNode, releaseXmlNode): self.ws = album.tagger.webservice self.log = album.log + log.info('WIKIDATA: Processing Track...') for release_group in metadata.getall('musicbrainz_releasegroupid'): - log.debug('WIKIDATA: looking up release group metadata for %s ' % release_group) + log.debug('WIKIDATA: Looking up release group metadata for %s ' % release_group) self.process_request(metadata, album, release_group, type='release-group') for artist in metadata.getall('musicbrainz_albumartistid'): - log.info('WIKIDATA: processing release artist %s' % artist) + log.debug('WIKIDATA: Processing release artist %s' % artist) self.process_request(metadata, album, artist, type='artist') for artist in metadata.getall('musicbrainz_artistid'): - log.info('WIKIDATA: processing track artist %s' % artist) + log.debug('WIKIDATA: Processing track artist %s' % artist) self.process_request(metadata, album, artist, type='artist') if 'musicbrainz_workid' in metadata: for workid in metadata.getall('musicbrainz_workid'): - log.info('WIKIDATA: processing track artist %s' % workid) + log.debug('WIKIDATA: Processing track artist %s' % workid) self.process_request(metadata, album, workid, type='work') wikidata = Wikidata() -# register_album_metadata_processor(wikidata.process_release) register_track_metadata_processor(wikidata.process_track) From 3e13956a62272e7fc219ee94e7ff085ca7d4cd83 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 13 Dec 2018 07:47:53 +0100 Subject: [PATCH 067/123] acousticbrainz_tonal-rhythm: Fixed syntax and runtime errors --- .../acousticbrainz_tonal-rhythm.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/plugins/acousticbrainz_tonal-rhythm/acousticbrainz_tonal-rhythm.py b/plugins/acousticbrainz_tonal-rhythm/acousticbrainz_tonal-rhythm.py index ada0dc80..648b4443 100644 --- a/plugins/acousticbrainz_tonal-rhythm/acousticbrainz_tonal-rhythm.py +++ b/plugins/acousticbrainz_tonal-rhythm/acousticbrainz_tonal-rhythm.py @@ -25,16 +25,18 @@ from the AcousticBrainz database.

    ''' -PLUGIN_LICENSE = "GPL-2.0" +PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.txt" -PLUGIN_VERSION = '1.1.1' +PLUGIN_VERSION = '1.1.2' PLUGIN_API_VERSIONS = ["2.0"] # Requires support for TKEY which is in 1.4 +from json import JSONDecodeError + from picard import log from picard.metadata import register_track_metadata_processor from functools import partial from picard.webservice import ratecontrol -from picard.util import parse_json +from picard.util import load_json ACOUSTICBRAINZ_HOST = "acousticbrainz.org" ACOUSTICBRAINZ_PORT = 80 @@ -45,7 +47,7 @@ class AcousticBrainz_Key: def get_data(self, album, track_metadata, trackXmlNode, releaseXmlNode): - if not musicbrainz_recordingid in track_metadata: + if "musicbrainz_recordingid" not in track_metadata: log.error("%s: Error parsing response. No MusicBrainz recording id found.", PLUGIN_NAME) return @@ -71,7 +73,7 @@ def process_data(self, album, track_metadata, response, reply, error): self.album_remove_request(album) return try: - data = parse_json(response) + data = load_json(response) except JSONDecodeError: log.error("%s: Network error retrieving AcousticBrainz data for recordingId %s", PLUGIN_NAME, track_metadata['musicbrainz_recordingid']) @@ -101,4 +103,5 @@ def album_remove_request(self, album): if album._requests == 0: album._finalize_loading(None) + register_track_metadata_processor(AcousticBrainz_Key().get_data) From 318c0b7c3f6bfd5c260a021bd2f7301feab61384 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 13 Dec 2018 07:55:35 +0100 Subject: [PATCH 068/123] classical_extras: Fix exception when references XML file does not exist --- plugins/classical_extras/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/classical_extras/__init__.py b/plugins/classical_extras/__init__.py index f9af3f4e..9912c430 100644 --- a/plugins/classical_extras/__init__.py +++ b/plugins/classical_extras/__init__.py @@ -81,7 +81,7 @@ # # The main control routine is at the end of the module -PLUGIN_VERSION = '2.0.2' +PLUGIN_VERSION = '2.0.3' PLUGIN_API_VERSIONS = ["2.0"] PLUGIN_LICENSE = "GPL-2.0" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" @@ -542,6 +542,7 @@ def get_references_from_file(release_id, path, filename): composer_dict_list = [] period_dict_list = [] genre_dict_list = [] + xml_file = None try: xml_file = open(os.path.join(path, filename), encoding="utf8") reply = xml_file.read() @@ -587,7 +588,8 @@ def get_references_from_file(release_id, path, filename): path, filename)) finally: - xml_file.close() + if xml_file: + xml_file.close() return { 'composers': composer_dict_list, 'periods': period_dict_list, From d402e580a702f893094d08c3b99fe1c9a7d70f16 Mon Sep 17 00:00:00 2001 From: Bob Swift Date: Fri, 14 Dec 2018 09:57:27 -0700 Subject: [PATCH 069/123] Add format_performer_tags plugin --- plugins/format_performer_tags/__init__.py | 277 ++++ plugins/format_performer_tags/docs/README.md | 282 ++++ .../docs/default_settings.jpg | Bin 0 -> 42308 bytes .../options_format_performer_tags.ui | 1229 +++++++++++++++++ .../ui_options_format_performer_tags.py | 673 +++++++++ 5 files changed, 2461 insertions(+) create mode 100644 plugins/format_performer_tags/__init__.py create mode 100644 plugins/format_performer_tags/docs/README.md create mode 100644 plugins/format_performer_tags/docs/default_settings.jpg create mode 100644 plugins/format_performer_tags/options_format_performer_tags.ui create mode 100644 plugins/format_performer_tags/ui_options_format_performer_tags.py diff --git a/plugins/format_performer_tags/__init__.py b/plugins/format_performer_tags/__init__.py new file mode 100644 index 00000000..b4a27ea5 --- /dev/null +++ b/plugins/format_performer_tags/__init__.py @@ -0,0 +1,277 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2018 Bob Swift (rdswift) +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + +PLUGIN_NAME = 'Format Performer Tags' +PLUGIN_AUTHOR = 'Bob Swift (rdswift)' +PLUGIN_DESCRIPTION = ''' +This plugin provides options with respect to the formatting of performer +tags. It has been developed using the 'Standardise Performers' plugin by +Sophist as the basis for retrieving and processing the performer data for +each of the tracks. The format of the resulting tags can be customized +in the option settings page. +''' + +PLUGIN_VERSION = "0.05" +PLUGIN_API_VERSIONS = ["2.0"] +PLUGIN_LICENSE = "GPL-2.0 or later" +PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" + +PLUGIN_USER_GUIDE_URL = "https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/format_performer_tags/docs/README.md" + +DEV_TESTING = False + +import re +from picard import config, log +from picard.metadata import register_track_metadata_processor +from picard.plugin import PluginPriority +from picard.ui.options import register_options_page, OptionsPage +from picard.plugins.format_performer_tags.ui_options_format_performer_tags import Ui_FormatPerformerTagsOptionsPage + +performers_split = re.compile(r", | and ").split + +WORD_LIST = ['guest', 'solo', 'additional'] + +def format_performer_tags(album, metadata, *args): + word_dict = {} + for word in WORD_LIST: + word_dict[word] = config.setting['format_group_' + word] + for key, values in list(filter(lambda filter_tuple: filter_tuple[0].startswith('performer:') or filter_tuple[0].startswith('~performersort:'), metadata.rawitems())): + mainkey, subkey = key.split(':', 1) + if not subkey: + continue + log.debug("%s: Formatting Performer [%s: %s]", PLUGIN_NAME, subkey, values,) + instruments = performers_split(subkey) + for instrument in instruments: + if DEV_TESTING: + log.debug("%s: instrument (first pass): '%s'", PLUGIN_NAME, instrument,) + if instrument in WORD_LIST: + instruments[0] = "{0} {1}".format(instruments[0], instrument,) + instruments.remove(instrument) + groups = { 1: [], 2: [], 3: [], 4: [], } + words = instruments[0].split() + for word in words[:]: + if word in WORD_LIST: + groups[word_dict[word]].append(word) + words.remove(word) + instruments[0] = " ".join(words) + display_group = {} + for group_number in range(1, 5): + if groups[group_number]: + if DEV_TESTING: + log.debug("%s: groups[%s]: %s", PLUGIN_NAME, group_number, groups[group_number],) + group_separator = config.setting["format_group_{0}_sep_char".format(group_number)] + if not group_separator: + group_separator = " " + display_group[group_number] = config.setting["format_group_{0}_start_char".format(group_number)] \ + + group_separator.join(groups[group_number]) \ + + config.setting["format_group_{0}_end_char".format(group_number)] + else: + display_group[group_number] = "" + if DEV_TESTING: + log.debug("%s: display_group: %s", PLUGIN_NAME, display_group,) + del metadata[key] + for instrument in instruments: + if DEV_TESTING: + log.debug("%s: instrument (second pass): '%s'", PLUGIN_NAME, instrument,) + words = instrument.split() + if (len(words) > 1) and (words[-1] in ["vocal", "vocals",]): + vocals = " ".join(words[:-1]) + instrument = words[-1] + else: + vocals = "" + if vocals: + group_number = config.setting["format_group_vocals"] + temp_group = groups[group_number][:] + if group_number < 2: + temp_group.append(vocals) + else: + temp_group.insert(0, vocals) + group_separator = config.setting["format_group_{0}_sep_char".format(group_number)] + if not group_separator: + group_separator = " " + display_group[group_number] = config.setting["format_group_{0}_start_char".format(group_number)] \ + + group_separator.join(temp_group) \ + + config.setting["format_group_{0}_end_char".format(group_number)] + + newkey = ('%s:%s%s%s%s' % (mainkey, display_group[1], instrument, display_group[2], display_group[3],)) + log.debug("%s: newkey: %s", PLUGIN_NAME, newkey,) + for value in values: + metadata.add_unique(newkey, (value + display_group[4])) + + +class FormatPerformerTagsOptionsPage(OptionsPage): + + NAME = "format_performer_tags" + TITLE = "Format Performer Tags" + PARENT = "plugins" + + options = [ + config.IntOption("setting", "format_group_additional", 3), + config.IntOption("setting", "format_group_guest", 4), + config.IntOption("setting", "format_group_solo", 3), + config.IntOption("setting", "format_group_vocals", 2), + config.TextOption("setting", "format_group_1_start_char", ''), + config.TextOption("setting", "format_group_1_end_char", ''), + config.TextOption("setting", "format_group_1_sep_char", ''), + config.TextOption("setting", "format_group_2_start_char", ', '), + config.TextOption("setting", "format_group_2_end_char", ''), + config.TextOption("setting", "format_group_2_sep_char", ''), + config.TextOption("setting", "format_group_3_start_char", ' ('), + config.TextOption("setting", "format_group_3_end_char", ')'), + config.TextOption("setting", "format_group_3_sep_char", ''), + config.TextOption("setting", "format_group_4_start_char", ' ('), + config.TextOption("setting", "format_group_4_end_char", ')'), + config.TextOption("setting", "format_group_4_sep_char", ''), + ] + + def __init__(self, parent=None): + super(FormatPerformerTagsOptionsPage, self).__init__(parent) + self.ui = Ui_FormatPerformerTagsOptionsPage() + self.ui.setupUi(self) + + def load(self): + # Enable external link + self.ui.format_description.setOpenExternalLinks(True) + + # Settings for Keyword: additional + temp = config.setting["format_group_additional"] + if temp > 3: + self.ui.additional_rb_4.setChecked(True) + elif temp > 2: + self.ui.additional_rb_3.setChecked(True) + elif temp > 1: + self.ui.additional_rb_2.setChecked(True) + else: + self.ui.additional_rb_1.setChecked(True) + + # Settings for Keyword: guest + temp = config.setting["format_group_guest"] + if temp > 3: + self.ui.guest_rb_4.setChecked(True) + elif temp > 2: + self.ui.guest_rb_3.setChecked(True) + elif temp > 1: + self.ui.guest_rb_2.setChecked(True) + else: + self.ui.guest_rb_1.setChecked(True) + + # Settings for Keyword: solo + temp = config.setting["format_group_solo"] + if temp > 3: + self.ui.solo_rb_4.setChecked(True) + elif temp > 2: + self.ui.solo_rb_3.setChecked(True) + elif temp > 1: + self.ui.solo_rb_2.setChecked(True) + else: + self.ui.solo_rb_1.setChecked(True) + + # Settings for all vocal keywords + temp = config.setting["format_group_vocals"] + if temp > 3: + self.ui.vocals_rb_4.setChecked(True) + elif temp > 2: + self.ui.vocals_rb_3.setChecked(True) + elif temp > 1: + self.ui.vocals_rb_2.setChecked(True) + else: + self.ui.vocals_rb_1.setChecked(True) + + # Settings for word group 1 + self.ui.format_group_1_start_char.setText(config.setting["format_group_1_start_char"]) + self.ui.format_group_1_end_char.setText(config.setting["format_group_1_end_char"]) + self.ui.format_group_1_sep_char.setText(config.setting["format_group_1_sep_char"]) + + # Settings for word group 2 + self.ui.format_group_2_start_char.setText(config.setting["format_group_2_start_char"]) + self.ui.format_group_2_end_char.setText(config.setting["format_group_2_end_char"]) + self.ui.format_group_2_sep_char.setText(config.setting["format_group_2_sep_char"]) + + # Settings for word group 3 + self.ui.format_group_3_start_char.setText(config.setting["format_group_3_start_char"]) + self.ui.format_group_3_end_char.setText(config.setting["format_group_3_end_char"]) + self.ui.format_group_3_sep_char.setText(config.setting["format_group_3_sep_char"]) + + # Settings for word group 4 + self.ui.format_group_4_start_char.setText(config.setting["format_group_4_start_char"]) + self.ui.format_group_4_end_char.setText(config.setting["format_group_4_end_char"]) + self.ui.format_group_4_sep_char.setText(config.setting["format_group_4_sep_char"]) + + # TODO: Modify self.format_description in ui_options_format_performer_tags.py to include a placeholder + # such as {user_guide_url} so that the translated string can be formatted to include the value + # of PLUGIN_USER_GUIDE_URL to dynamically set the link while not requiring retranslation if the + # link changes. Preliminary code something like: + # + # temp = (self.ui.format_description.text).format(user_guide_url=PLUGIN_USER_GUIDE_URL,) + # self.ui.format_description.setText(temp) + + + def save(self): + # Process 'additional' keyword settings + temp = 1 + if self.ui.additional_rb_2.isChecked(): temp = 2 + if self.ui.additional_rb_3.isChecked(): temp = 3 + if self.ui.additional_rb_4.isChecked(): temp = 4 + config.setting["format_group_additional"] = temp + + # Process 'guest' keyword settings + temp = 1 + if self.ui.guest_rb_2.isChecked(): temp = 2 + if self.ui.guest_rb_3.isChecked(): temp = 3 + if self.ui.guest_rb_4.isChecked(): temp = 4 + config.setting["format_group_guest"] = temp + + # Process 'solo' keyword settings + temp = 1 + if self.ui.solo_rb_2.isChecked(): temp = 2 + if self.ui.solo_rb_3.isChecked(): temp = 3 + if self.ui.solo_rb_4.isChecked(): temp = 4 + config.setting["format_group_solo"] = temp + + # Process all vocal keyword settings + temp = 1 + if self.ui.vocals_rb_2.isChecked(): temp = 2 + if self.ui.vocals_rb_3.isChecked(): temp = 3 + if self.ui.vocals_rb_4.isChecked(): temp = 4 + config.setting["format_group_vocals"] = temp + + # Settings for word group 1 + config.setting["format_group_1_start_char"] = self.ui.format_group_1_start_char.text() + config.setting["format_group_1_end_char"] = self.ui.format_group_1_end_char.text() + config.setting["format_group_1_sep_char"] = self.ui.format_group_1_sep_char.text() + + # Settings for word group 2 + config.setting["format_group_2_start_char"] = self.ui.format_group_2_start_char.text() + config.setting["format_group_2_end_char"] = self.ui.format_group_2_end_char.text() + config.setting["format_group_2_sep_char"] = self.ui.format_group_2_sep_char.text() + + # Settings for word group 3 + config.setting["format_group_3_start_char"] = self.ui.format_group_3_start_char.text() + config.setting["format_group_3_end_char"] = self.ui.format_group_3_end_char.text() + config.setting["format_group_3_sep_char"] = self.ui.format_group_3_sep_char.text() + + # Settings for word group 4 + config.setting["format_group_4_start_char"] = self.ui.format_group_4_start_char.text() + config.setting["format_group_4_end_char"] = self.ui.format_group_4_end_char.text() + config.setting["format_group_4_sep_char"] = self.ui.format_group_4_sep_char.text() + + +# Register the plugin to run at a HIGH priority. +register_track_metadata_processor(format_performer_tags, priority=PluginPriority.HIGH) +register_options_page(FormatPerformerTagsOptionsPage) diff --git a/plugins/format_performer_tags/docs/README.md b/plugins/format_performer_tags/docs/README.md new file mode 100644 index 00000000..cafc5f7b --- /dev/null +++ b/plugins/format_performer_tags/docs/README.md @@ -0,0 +1,282 @@ +# Format Performer Tags \[[Download](https://github.com/rdswift/picard-plugins/raw/2.0_RDS_Plugins/plugins/format_performer_tags/format_performer_tags.zip)\] + +## Overview + +This plugin allows the user to configure the way that instrument and vocal performer tags are written. Once +installed a settings page will be added to Picard's options, which is where the plugin is configured. + +--- + +## What it Does + +This plugin serves two purposes. + +### First: + +Picard will by default try to order the performer/instrument credits by the name of the performers, summing up all instruments for that performer in one line. +This plugin will order the performer/instrument credits by instrument, summing up all performers that play them. + +So instead of this: + +``` +background vocals and drums: Wayne Gretzky +bass and background vocals: Leslie Nielsen +guitar, keyboard and synthesizer: Edward Hopper +guitar and vocals: Vladimir Nabokov +keyboard and lead vocals: Bianca Castafiore +``` + +It will be displayed like this: + +``` +background vocals: Wayne Gretzky, Leslie Nielsen +bass: Leslie Nielsen +drums: Wayne Gretzky +guitar: Edward Hopper, Vladimir Nabokov +keyboard: Edward Hopper, Bianca Castafiore +lead vocals: Bianca Castafiore +synthesizer: Edward Hopper +vocals: Vladimir Nabokov +``` + +### Second: + +This plugin allows fine-tuned organization of how instruments, performers and additional descriptions (keywords) are displayed in the instrument/performer tags. + +Here is some background information about these keywords: + +MusicBrainz' database allows to add and store keywords (attributes) that will refine or additionally describe the role of a performer for a recording. +For an artist performing music on an instrument, these are the three attributes (keywords) that MusicBrainz can store, and offer Picard: + +* additional +* guest +* solo + +For an artist performing with his/her voice, MusicBrainz has this restricted list of keywords describing the role or the register of the voice: + +* background vocals +* choir vocals +* lead vocals + * alto vocals + * baritone vocals + * bass vocals + * bass-baritone vocals + * contralto vocals + * countertenor vocals + * mezzo-soprano vocals + * soprano vocals + * tenor vocals + * treble vocals +* other vocals + * spoken vocals + + +Picard can retrieve and display these keywords and will list them all together in front of the performer. +The result will be something like this: + +``` +guitar and solo guitar: Bob 'Swift' Fingers +additional drums: Rob Reiner (guest) +additional baritone vocals: Hermann Rorschach +guest soprano vocals: Bianca Castafiore +``` + +The problem with this is that it is a bit indistinct if these keywords say something about the instrument, the artist and their performing role, the voice's register, or the persons relation to the group/orchestra. +For instance: + +* 'additional' is referring to instrumentation. For example 'additional percussion'. +* 'guest' is referring to the performer as a person. Indicating that they are a guest in that band/orchestra instead of a regular member. +* 'solo' is referring to a specific role a musician performs in a composition. For example a musician performing a guitar solo. +* 'soprano vocals' is saying something about the register of a performer's voice. + +So you might want to attach 'solo' to the instrument, 'baritone' to the vocals, and 'guest' to the performer. +This plugin allows you to do that, so you could have something like this as a result: + +``` +guitar [solo]: Bob 'Swift' Fingers +drums ‹additional› : Rob Reiner (guest) +vocals, baritone ‹additional› : Hermann Rorschach +vocals, soprano: Bianca Castafiore (guest) +``` + +--- + +## How it Works + +This is the concept behind the workings of this plugin: + +The basic structure of a performer tag such as Picard produces it is: + + [Keywords] Instrument / Vocals: Performer + +This plugin makes four different 'Sections' at fixed positions in these tags available. +Their positions are: + + [Section 1]Instrument / Vocals[Section 2][Section 3]: Performer[Section 4] + +In the settings panel you can define in what section (at what location) you want each of the available keywords to be displayed. +You can do that by simply selecting the section number for that (group of) keyword(s). + +You can also define what characters you want to use for delimiting or surrounding the keywords. +For some situations you might want to use the more common parenthesis brackets ( ), or maybe you prefer less common brackets such as \[ \] or ‹ ›. +Note that using default parenthesis might confuse possible subsequent tagging formulas in the music player/manager of your choice. +You can also just leave them blank, or use commas, spaces, etc. + +Note that the plugin does not add any spaces as separators by default, so you will need to define those to your personal liking. + +--- + +## Settings + +The first group of settings is the **Keyword Section Assignments**. This is where you select the section in +which each of the keywords will be displayed. Selection is made by clicking the radio button corresponding +to the desired section for each of the keywords. + +The second group of settings is the **Section Display Settings**. This is where you configure the text +included at the beginning and end of each section displayed, and the characters used to separate multiple +items within a section. Note that leading or trailing spaces must be included in the settings and will not +be automatically added. If no separator characters are entered, the items will be automatically separated +by a single space. + +The initial default settings are: + + +![default settings image](default_settings.jpg) + +These settings will produce tags such as: + +``` +rhodes piano (solo): Billy Preston (guest) +percussion: Steve Berlin (guest), Kris MacFarlane, Séan McCann +vocals, background: Jeen (guest) +``` + +--- + +## Examples + +The following are some examples using actual information from MusicBrainz: + +### Example 1: + +(add example) + +### Example 2: + +(add example) + +### Example 3: + +(add example) + +--- + +## Credits + +Special thank-you to [hiccup](https://musicbrainz.org/user/hiccup) for improvement suggestions and extensive testing during +the development of this plugin, and for providing the write-up and examples that formed the basis for this User Guide. + + + + diff --git a/plugins/format_performer_tags/docs/default_settings.jpg b/plugins/format_performer_tags/docs/default_settings.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0466e10ec2d30da17b66d099d6a547c28a2e7589 GIT binary patch literal 42308 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 literal 0 HcmV?d00001 diff --git a/plugins/format_performer_tags/options_format_performer_tags.ui b/plugins/format_performer_tags/options_format_performer_tags.ui new file mode 100644 index 00000000..dc31c1e4 --- /dev/null +++ b/plugins/format_performer_tags/options_format_performer_tags.ui @@ -0,0 +1,1229 @@ + + + FormatPerformerTagsOptionsPage + + + + 0 + 0 + 458 + 673 + + + + + 0 + 0 + + + + + 16 + + + + + + 0 + 0 + + + + + 440 + 655 + + + + QFrame::NoFrame + + + true + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + 0 + 0 + 440 + 655 + + + + + + + + 0 + 0 + + + + + 0 + 50 + + + + + 75 + true + + + + Format Performer Tags + + + + 9 + + + 9 + + + 9 + + + 1 + + + + + + 0 + 0 + + + + + 50 + false + + + + <html><head/><body><p>These settings will determine the format for any <span style=" font-weight:600;">Performer</span> tags prepared. The format is divided into six parts: the performer; the instrument or &quot;vocals&quot;; and four user selectable sections for the extra information. This is set out as:</p><p align="center"><span style=" font-weight:600;">[Section 1]</span>Instrument/Vocals<span style=" font-weight:600;">[Section 2][Section 3]</span>: Performer<span style=" font-weight:600;">[Section 4]</span></p><p>You can select the section in which each of the extra information words appears.</p><p>For each of the sections you can select the starting character(s), the character(s) separating entries, and the ending character(s). Note that leading or trailing spaces must be included in the settings and will not be automatically added. If no separator characters are entered, the items within a section will be automatically separated by a single space.</p><p>Please visit the repository on GitHub for <a href="https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/format_performer_tags/docs/README.md"><span style=" text-decoration: underline; color:#0000ff;">additional information</span></a>.</p></body></html> + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + + + + 75 + true + + + + Keyword Section Assignments + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + + 50 + false + + + + Keyword: additional + + + + 3 + + + 1 + + + 0 + + + 1 + + + 0 + + + + + + 0 + 0 + + + + + 40 + 20 + + + + + 50 + false + + + + 1 + + + false + + + + + + + + 0 + 0 + + + + + 40 + 20 + + + + + 50 + false + + + + 2 + + + + + + + + 0 + 0 + + + + + 40 + 20 + + + + + 50 + false + + + + 3 + + + true + + + + + + + + 40 + 20 + + + + + 50 + false + + + + 4 + + + + + + + + + + + 0 + 0 + + + + + 50 + false + + + + Keyword: guest + + + + 3 + + + 1 + + + 0 + + + 1 + + + 0 + + + + + + 0 + 0 + + + + + 40 + 20 + + + + + 50 + false + + + + 1 + + + false + + + + + + + + 0 + 0 + + + + + 40 + 20 + + + + + 50 + false + + + + 2 + + + + + + + + 0 + 0 + + + + + 40 + 20 + + + + + 50 + false + + + + 3 + + + + + + + + 40 + 20 + + + + + 50 + false + + + + 4 + + + true + + + + + + + + + + + 0 + 0 + + + + + 50 + false + + + + Keyword: solo + + + + 3 + + + 1 + + + 0 + + + 1 + + + 0 + + + + + + 0 + 0 + + + + + 40 + 20 + + + + + 50 + false + + + + 1 + + + false + + + + + + + + 0 + 0 + + + + + 40 + 20 + + + + + 50 + false + + + + 2 + + + + + + + + 0 + 0 + + + + + 40 + 20 + + + + + 50 + false + + + + 3 + + + true + + + + + + + + 40 + 20 + + + + + 50 + false + + + + 4 + + + + + + + + + + + 0 + 0 + + + + + 50 + false + + + + All vocal type keywords + + + + 1 + + + 1 + + + 1 + + + 1 + + + + + + 0 + 0 + + + + + 40 + 20 + + + + + 50 + false + + + + 1 + + + false + + + + + + + + 0 + 0 + + + + + 40 + 20 + + + + + 50 + false + + + + 2 + + + true + + + + + + + + 0 + 0 + + + + + 40 + 20 + + + + + 50 + false + + + + 3 + + + + + + + + 40 + 20 + + + + + 50 + false + + + + 4 + + + + + + + + + + + + + + 0 + 50 + + + + + 75 + true + + + + Section Display Settings + + + + + + 3 + + + 1 + + + + + + 0 + 0 + + + + + 50 + 16777215 + + + + + 50 + false + + + + (blank) + + + + + + + + 0 + 0 + + + + + 50 + 16777215 + + + + + 50 + false + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + (blank) + + + false + + + + + + + + 0 + 0 + + + + + 50 + 16777215 + + + + + 50 + false + + + + (blank) + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Section 3 + + + + + + + + 0 + 0 + + + + + 50 + 16777215 + + + + + 50 + false + + + + ( + + + (blank) + + + + + + + + 0 + 0 + + + + + 50 + 16777215 + + + + + 50 + false + + + + (blank) + + + + + + + + 0 + 0 + + + + + 50 + 16777215 + + + + + 50 + false + + + + ( + + + (blank) + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Section 2 + + + + + + + + 0 + 0 + + + + + 50 + 16777215 + + + + + 50 + false + + + + (blank) + + + + + + + + 75 + true + + + + End Char(s) + + + Qt::AlignCenter + + + true + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Section 4 + + + + + + + + 0 + 0 + + + + + 50 + 16777215 + + + + + 50 + false + + + + (blank) + + + + + + + + 75 + true + + + + Sep Char(s) + + + Qt::AlignCenter + + + true + + + + + + + + 0 + 0 + + + + + 50 + 16777215 + + + + + 50 + false + + + + ) + + + (blank) + + + + + + + + 75 + true + false + + + + Start Char(s) + + + Qt::AlignCenter + + + true + + + + + + + + 0 + 0 + + + + + 50 + 16777215 + + + + + 50 + false + + + + + + + (blank) + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Section 1 + + + + + + + + 0 + 0 + + + + + 50 + 16777215 + + + + + 50 + false + + + + ) + + + (blank) + + + + + + + + 0 + 0 + + + + + 50 + 16777215 + + + + + 50 + false + + + + , + + + (blank) + + + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + + + additional_rb_1 + additional_rb_2 + additional_rb_3 + additional_rb_4 + guest_rb_1 + guest_rb_2 + guest_rb_3 + guest_rb_4 + solo_rb_1 + solo_rb_2 + solo_rb_3 + solo_rb_4 + vocals_rb_1 + vocals_rb_2 + vocals_rb_3 + vocals_rb_4 + format_group_1_start_char + format_group_1_sep_char + format_group_1_end_char + format_group_2_start_char + format_group_2_sep_char + format_group_2_end_char + format_group_3_start_char + format_group_3_sep_char + format_group_3_end_char + format_group_4_start_char + format_group_4_sep_char + format_group_4_end_char + scrollArea + + + + diff --git a/plugins/format_performer_tags/ui_options_format_performer_tags.py b/plugins/format_performer_tags/ui_options_format_performer_tags.py new file mode 100644 index 00000000..6f0fd1ec --- /dev/null +++ b/plugins/format_performer_tags/ui_options_format_performer_tags.py @@ -0,0 +1,673 @@ +# -*- coding: utf-8 -*- + +# Automatically generated - don't edit. +# Use `python setup.py build_ui` to update it. + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_FormatPerformerTagsOptionsPage(object): + def setupUi(self, FormatPerformerTagsOptionsPage): + FormatPerformerTagsOptionsPage.setObjectName("FormatPerformerTagsOptionsPage") + FormatPerformerTagsOptionsPage.resize(458, 673) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(FormatPerformerTagsOptionsPage.sizePolicy().hasHeightForWidth()) + FormatPerformerTagsOptionsPage.setSizePolicy(sizePolicy) + self.vboxlayout = QtWidgets.QVBoxLayout(FormatPerformerTagsOptionsPage) + self.vboxlayout.setSpacing(16) + self.vboxlayout.setObjectName("vboxlayout") + self.scrollArea = QtWidgets.QScrollArea(FormatPerformerTagsOptionsPage) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.scrollArea.sizePolicy().hasHeightForWidth()) + self.scrollArea.setSizePolicy(sizePolicy) + self.scrollArea.setMinimumSize(QtCore.QSize(440, 655)) + self.scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame) + self.scrollArea.setWidgetResizable(True) + self.scrollArea.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) + self.scrollArea.setObjectName("scrollArea") + self.scrollAreaWidgetContents = QtWidgets.QWidget() + self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 440, 655)) + self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents) + self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.gb_description = QtWidgets.QGroupBox(self.scrollAreaWidgetContents) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.gb_description.sizePolicy().hasHeightForWidth()) + self.gb_description.setSizePolicy(sizePolicy) + self.gb_description.setMinimumSize(QtCore.QSize(0, 50)) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.gb_description.setFont(font) + self.gb_description.setObjectName("gb_description") + self.verticalLayout = QtWidgets.QVBoxLayout(self.gb_description) + self.verticalLayout.setContentsMargins(9, 9, 9, 1) + self.verticalLayout.setObjectName("verticalLayout") + self.format_description = QtWidgets.QLabel(self.gb_description) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.format_description.sizePolicy().hasHeightForWidth()) + self.format_description.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.format_description.setFont(font) + self.format_description.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) + self.format_description.setWordWrap(True) + self.format_description.setObjectName("format_description") + self.verticalLayout.addWidget(self.format_description) + self.verticalLayout_2.addWidget(self.gb_description) + self.gb_word_groups = QtWidgets.QGroupBox(self.scrollAreaWidgetContents) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.gb_word_groups.setFont(font) + self.gb_word_groups.setObjectName("gb_word_groups") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.gb_word_groups) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.group_additional = QtWidgets.QGroupBox(self.gb_word_groups) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.group_additional.sizePolicy().hasHeightForWidth()) + self.group_additional.setSizePolicy(sizePolicy) + self.group_additional.setMinimumSize(QtCore.QSize(0, 0)) + self.group_additional.setMaximumSize(QtCore.QSize(16777215, 16777215)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.group_additional.setFont(font) + self.group_additional.setObjectName("group_additional") + self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.group_additional) + self.horizontalLayout_2.setContentsMargins(1, 0, 1, 0) + self.horizontalLayout_2.setSpacing(3) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.additional_rb_1 = QtWidgets.QRadioButton(self.group_additional) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.additional_rb_1.sizePolicy().hasHeightForWidth()) + self.additional_rb_1.setSizePolicy(sizePolicy) + self.additional_rb_1.setMaximumSize(QtCore.QSize(40, 20)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.additional_rb_1.setFont(font) + self.additional_rb_1.setChecked(False) + self.additional_rb_1.setObjectName("additional_rb_1") + self.horizontalLayout_2.addWidget(self.additional_rb_1, 0, QtCore.Qt.AlignVCenter) + self.additional_rb_2 = QtWidgets.QRadioButton(self.group_additional) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.additional_rb_2.sizePolicy().hasHeightForWidth()) + self.additional_rb_2.setSizePolicy(sizePolicy) + self.additional_rb_2.setMaximumSize(QtCore.QSize(40, 20)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.additional_rb_2.setFont(font) + self.additional_rb_2.setObjectName("additional_rb_2") + self.horizontalLayout_2.addWidget(self.additional_rb_2, 0, QtCore.Qt.AlignVCenter) + self.additional_rb_3 = QtWidgets.QRadioButton(self.group_additional) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.additional_rb_3.sizePolicy().hasHeightForWidth()) + self.additional_rb_3.setSizePolicy(sizePolicy) + self.additional_rb_3.setMaximumSize(QtCore.QSize(40, 20)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.additional_rb_3.setFont(font) + self.additional_rb_3.setChecked(True) + self.additional_rb_3.setObjectName("additional_rb_3") + self.horizontalLayout_2.addWidget(self.additional_rb_3, 0, QtCore.Qt.AlignVCenter) + self.additional_rb_4 = QtWidgets.QRadioButton(self.group_additional) + self.additional_rb_4.setMaximumSize(QtCore.QSize(40, 20)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.additional_rb_4.setFont(font) + self.additional_rb_4.setObjectName("additional_rb_4") + self.horizontalLayout_2.addWidget(self.additional_rb_4, 0, QtCore.Qt.AlignVCenter) + self.verticalLayout_3.addWidget(self.group_additional) + self.group_guest = QtWidgets.QGroupBox(self.gb_word_groups) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.group_guest.sizePolicy().hasHeightForWidth()) + self.group_guest.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.group_guest.setFont(font) + self.group_guest.setObjectName("group_guest") + self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.group_guest) + self.horizontalLayout_3.setContentsMargins(1, 0, 1, 0) + self.horizontalLayout_3.setSpacing(3) + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.guest_rb_1 = QtWidgets.QRadioButton(self.group_guest) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.guest_rb_1.sizePolicy().hasHeightForWidth()) + self.guest_rb_1.setSizePolicy(sizePolicy) + self.guest_rb_1.setMaximumSize(QtCore.QSize(40, 20)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.guest_rb_1.setFont(font) + self.guest_rb_1.setChecked(False) + self.guest_rb_1.setObjectName("guest_rb_1") + self.horizontalLayout_3.addWidget(self.guest_rb_1, 0, QtCore.Qt.AlignVCenter) + self.guest_rb_2 = QtWidgets.QRadioButton(self.group_guest) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.guest_rb_2.sizePolicy().hasHeightForWidth()) + self.guest_rb_2.setSizePolicy(sizePolicy) + self.guest_rb_2.setMaximumSize(QtCore.QSize(40, 20)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.guest_rb_2.setFont(font) + self.guest_rb_2.setObjectName("guest_rb_2") + self.horizontalLayout_3.addWidget(self.guest_rb_2, 0, QtCore.Qt.AlignVCenter) + self.guest_rb_3 = QtWidgets.QRadioButton(self.group_guest) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.guest_rb_3.sizePolicy().hasHeightForWidth()) + self.guest_rb_3.setSizePolicy(sizePolicy) + self.guest_rb_3.setMaximumSize(QtCore.QSize(40, 20)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.guest_rb_3.setFont(font) + self.guest_rb_3.setObjectName("guest_rb_3") + self.horizontalLayout_3.addWidget(self.guest_rb_3, 0, QtCore.Qt.AlignVCenter) + self.guest_rb_4 = QtWidgets.QRadioButton(self.group_guest) + self.guest_rb_4.setMaximumSize(QtCore.QSize(40, 20)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.guest_rb_4.setFont(font) + self.guest_rb_4.setChecked(True) + self.guest_rb_4.setObjectName("guest_rb_4") + self.horizontalLayout_3.addWidget(self.guest_rb_4, 0, QtCore.Qt.AlignVCenter) + self.verticalLayout_3.addWidget(self.group_guest) + self.group_solo = QtWidgets.QGroupBox(self.gb_word_groups) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.group_solo.sizePolicy().hasHeightForWidth()) + self.group_solo.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.group_solo.setFont(font) + self.group_solo.setObjectName("group_solo") + self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.group_solo) + self.horizontalLayout_4.setContentsMargins(1, 0, 1, 0) + self.horizontalLayout_4.setSpacing(3) + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.solo_rb_1 = QtWidgets.QRadioButton(self.group_solo) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.solo_rb_1.sizePolicy().hasHeightForWidth()) + self.solo_rb_1.setSizePolicy(sizePolicy) + self.solo_rb_1.setMaximumSize(QtCore.QSize(40, 20)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.solo_rb_1.setFont(font) + self.solo_rb_1.setChecked(False) + self.solo_rb_1.setObjectName("solo_rb_1") + self.horizontalLayout_4.addWidget(self.solo_rb_1, 0, QtCore.Qt.AlignVCenter) + self.solo_rb_2 = QtWidgets.QRadioButton(self.group_solo) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.solo_rb_2.sizePolicy().hasHeightForWidth()) + self.solo_rb_2.setSizePolicy(sizePolicy) + self.solo_rb_2.setMaximumSize(QtCore.QSize(40, 20)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.solo_rb_2.setFont(font) + self.solo_rb_2.setObjectName("solo_rb_2") + self.horizontalLayout_4.addWidget(self.solo_rb_2, 0, QtCore.Qt.AlignVCenter) + self.solo_rb_3 = QtWidgets.QRadioButton(self.group_solo) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.solo_rb_3.sizePolicy().hasHeightForWidth()) + self.solo_rb_3.setSizePolicy(sizePolicy) + self.solo_rb_3.setMaximumSize(QtCore.QSize(40, 20)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.solo_rb_3.setFont(font) + self.solo_rb_3.setChecked(True) + self.solo_rb_3.setObjectName("solo_rb_3") + self.horizontalLayout_4.addWidget(self.solo_rb_3, 0, QtCore.Qt.AlignVCenter) + self.solo_rb_4 = QtWidgets.QRadioButton(self.group_solo) + self.solo_rb_4.setMaximumSize(QtCore.QSize(40, 20)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.solo_rb_4.setFont(font) + self.solo_rb_4.setObjectName("solo_rb_4") + self.horizontalLayout_4.addWidget(self.solo_rb_4, 0, QtCore.Qt.AlignVCenter) + self.verticalLayout_3.addWidget(self.group_solo) + self.group_vocals = QtWidgets.QGroupBox(self.gb_word_groups) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.group_vocals.sizePolicy().hasHeightForWidth()) + self.group_vocals.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.group_vocals.setFont(font) + self.group_vocals.setObjectName("group_vocals") + self.horizontalLayout_5 = QtWidgets.QHBoxLayout(self.group_vocals) + self.horizontalLayout_5.setContentsMargins(1, 1, 1, 1) + self.horizontalLayout_5.setObjectName("horizontalLayout_5") + self.vocals_rb_1 = QtWidgets.QRadioButton(self.group_vocals) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.vocals_rb_1.sizePolicy().hasHeightForWidth()) + self.vocals_rb_1.setSizePolicy(sizePolicy) + self.vocals_rb_1.setMaximumSize(QtCore.QSize(40, 20)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.vocals_rb_1.setFont(font) + self.vocals_rb_1.setChecked(False) + self.vocals_rb_1.setObjectName("vocals_rb_1") + self.horizontalLayout_5.addWidget(self.vocals_rb_1, 0, QtCore.Qt.AlignVCenter) + self.vocals_rb_2 = QtWidgets.QRadioButton(self.group_vocals) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.vocals_rb_2.sizePolicy().hasHeightForWidth()) + self.vocals_rb_2.setSizePolicy(sizePolicy) + self.vocals_rb_2.setMaximumSize(QtCore.QSize(40, 20)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.vocals_rb_2.setFont(font) + self.vocals_rb_2.setChecked(True) + self.vocals_rb_2.setObjectName("vocals_rb_2") + self.horizontalLayout_5.addWidget(self.vocals_rb_2, 0, QtCore.Qt.AlignVCenter) + self.vocals_rb_3 = QtWidgets.QRadioButton(self.group_vocals) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.vocals_rb_3.sizePolicy().hasHeightForWidth()) + self.vocals_rb_3.setSizePolicy(sizePolicy) + self.vocals_rb_3.setMaximumSize(QtCore.QSize(40, 20)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.vocals_rb_3.setFont(font) + self.vocals_rb_3.setObjectName("vocals_rb_3") + self.horizontalLayout_5.addWidget(self.vocals_rb_3, 0, QtCore.Qt.AlignVCenter) + self.vocals_rb_4 = QtWidgets.QRadioButton(self.group_vocals) + self.vocals_rb_4.setMaximumSize(QtCore.QSize(40, 20)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.vocals_rb_4.setFont(font) + self.vocals_rb_4.setObjectName("vocals_rb_4") + self.horizontalLayout_5.addWidget(self.vocals_rb_4, 0, QtCore.Qt.AlignVCenter) + self.verticalLayout_3.addWidget(self.group_vocals) + self.verticalLayout_2.addWidget(self.gb_word_groups, 0, QtCore.Qt.AlignTop) + self.gb_group_settings = QtWidgets.QGroupBox(self.scrollAreaWidgetContents) + self.gb_group_settings.setMinimumSize(QtCore.QSize(0, 50)) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.gb_group_settings.setFont(font) + self.gb_group_settings.setObjectName("gb_group_settings") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.gb_group_settings) + self.horizontalLayout.setObjectName("horizontalLayout") + self.gridLayout = QtWidgets.QGridLayout() + self.gridLayout.setHorizontalSpacing(3) + self.gridLayout.setVerticalSpacing(1) + self.gridLayout.setObjectName("gridLayout") + self.format_group_3_sep_char = QtWidgets.QLineEdit(self.gb_group_settings) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.format_group_3_sep_char.sizePolicy().hasHeightForWidth()) + self.format_group_3_sep_char.setSizePolicy(sizePolicy) + self.format_group_3_sep_char.setMaximumSize(QtCore.QSize(50, 16777215)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.format_group_3_sep_char.setFont(font) + self.format_group_3_sep_char.setObjectName("format_group_3_sep_char") + self.gridLayout.addWidget(self.format_group_3_sep_char, 3, 2, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) + self.format_group_1_start_char = QtWidgets.QLineEdit(self.gb_group_settings) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.format_group_1_start_char.sizePolicy().hasHeightForWidth()) + self.format_group_1_start_char.setSizePolicy(sizePolicy) + self.format_group_1_start_char.setMaximumSize(QtCore.QSize(50, 16777215)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.format_group_1_start_char.setFont(font) + self.format_group_1_start_char.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self.format_group_1_start_char.setClearButtonEnabled(False) + self.format_group_1_start_char.setObjectName("format_group_1_start_char") + self.gridLayout.addWidget(self.format_group_1_start_char, 1, 1, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) + self.format_group_1_sep_char = QtWidgets.QLineEdit(self.gb_group_settings) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.format_group_1_sep_char.sizePolicy().hasHeightForWidth()) + self.format_group_1_sep_char.setSizePolicy(sizePolicy) + self.format_group_1_sep_char.setMaximumSize(QtCore.QSize(50, 16777215)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.format_group_1_sep_char.setFont(font) + self.format_group_1_sep_char.setObjectName("format_group_1_sep_char") + self.gridLayout.addWidget(self.format_group_1_sep_char, 1, 2, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) + self.label_18 = QtWidgets.QLabel(self.gb_group_settings) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label_18.sizePolicy().hasHeightForWidth()) + self.label_18.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.label_18.setFont(font) + self.label_18.setObjectName("label_18") + self.gridLayout.addWidget(self.label_18, 3, 0, 1, 1, QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self.format_group_4_start_char = QtWidgets.QLineEdit(self.gb_group_settings) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.format_group_4_start_char.sizePolicy().hasHeightForWidth()) + self.format_group_4_start_char.setSizePolicy(sizePolicy) + self.format_group_4_start_char.setMaximumSize(QtCore.QSize(50, 16777215)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.format_group_4_start_char.setFont(font) + self.format_group_4_start_char.setObjectName("format_group_4_start_char") + self.gridLayout.addWidget(self.format_group_4_start_char, 4, 1, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) + self.format_group_1_end_char = QtWidgets.QLineEdit(self.gb_group_settings) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.format_group_1_end_char.sizePolicy().hasHeightForWidth()) + self.format_group_1_end_char.setSizePolicy(sizePolicy) + self.format_group_1_end_char.setMaximumSize(QtCore.QSize(50, 16777215)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.format_group_1_end_char.setFont(font) + self.format_group_1_end_char.setObjectName("format_group_1_end_char") + self.gridLayout.addWidget(self.format_group_1_end_char, 1, 3, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) + self.format_group_3_start_char = QtWidgets.QLineEdit(self.gb_group_settings) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.format_group_3_start_char.sizePolicy().hasHeightForWidth()) + self.format_group_3_start_char.setSizePolicy(sizePolicy) + self.format_group_3_start_char.setMaximumSize(QtCore.QSize(50, 16777215)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.format_group_3_start_char.setFont(font) + self.format_group_3_start_char.setObjectName("format_group_3_start_char") + self.gridLayout.addWidget(self.format_group_3_start_char, 3, 1, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) + self.label = QtWidgets.QLabel(self.gb_group_settings) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth()) + self.label.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.label.setFont(font) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 2, 0, 1, 1, QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self.format_group_4_sep_char = QtWidgets.QLineEdit(self.gb_group_settings) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.format_group_4_sep_char.sizePolicy().hasHeightForWidth()) + self.format_group_4_sep_char.setSizePolicy(sizePolicy) + self.format_group_4_sep_char.setMaximumSize(QtCore.QSize(50, 16777215)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.format_group_4_sep_char.setFont(font) + self.format_group_4_sep_char.setObjectName("format_group_4_sep_char") + self.gridLayout.addWidget(self.format_group_4_sep_char, 4, 2, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) + self.label_19 = QtWidgets.QLabel(self.gb_group_settings) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.label_19.setFont(font) + self.label_19.setAlignment(QtCore.Qt.AlignCenter) + self.label_19.setWordWrap(True) + self.label_19.setObjectName("label_19") + self.gridLayout.addWidget(self.label_19, 0, 3, 1, 1) + self.label_7 = QtWidgets.QLabel(self.gb_group_settings) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label_7.sizePolicy().hasHeightForWidth()) + self.label_7.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.label_7.setFont(font) + self.label_7.setObjectName("label_7") + self.gridLayout.addWidget(self.label_7, 4, 0, 1, 1, QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self.format_group_2_sep_char = QtWidgets.QLineEdit(self.gb_group_settings) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.format_group_2_sep_char.sizePolicy().hasHeightForWidth()) + self.format_group_2_sep_char.setSizePolicy(sizePolicy) + self.format_group_2_sep_char.setMaximumSize(QtCore.QSize(50, 16777215)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.format_group_2_sep_char.setFont(font) + self.format_group_2_sep_char.setObjectName("format_group_2_sep_char") + self.gridLayout.addWidget(self.format_group_2_sep_char, 2, 2, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) + self.label_20 = QtWidgets.QLabel(self.gb_group_settings) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.label_20.setFont(font) + self.label_20.setAlignment(QtCore.Qt.AlignCenter) + self.label_20.setWordWrap(True) + self.label_20.setObjectName("label_20") + self.gridLayout.addWidget(self.label_20, 0, 2, 1, 1) + self.format_group_4_end_char = QtWidgets.QLineEdit(self.gb_group_settings) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.format_group_4_end_char.sizePolicy().hasHeightForWidth()) + self.format_group_4_end_char.setSizePolicy(sizePolicy) + self.format_group_4_end_char.setMaximumSize(QtCore.QSize(50, 16777215)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.format_group_4_end_char.setFont(font) + self.format_group_4_end_char.setObjectName("format_group_4_end_char") + self.gridLayout.addWidget(self.format_group_4_end_char, 4, 3, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) + self.label_15 = QtWidgets.QLabel(self.gb_group_settings) + font = QtGui.QFont() + font.setBold(True) + font.setUnderline(False) + font.setWeight(75) + self.label_15.setFont(font) + self.label_15.setAlignment(QtCore.Qt.AlignCenter) + self.label_15.setWordWrap(True) + self.label_15.setObjectName("label_15") + self.gridLayout.addWidget(self.label_15, 0, 1, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) + self.format_group_2_end_char = QtWidgets.QLineEdit(self.gb_group_settings) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.format_group_2_end_char.sizePolicy().hasHeightForWidth()) + self.format_group_2_end_char.setSizePolicy(sizePolicy) + self.format_group_2_end_char.setMaximumSize(QtCore.QSize(50, 16777215)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.format_group_2_end_char.setFont(font) + self.format_group_2_end_char.setText("") + self.format_group_2_end_char.setObjectName("format_group_2_end_char") + self.gridLayout.addWidget(self.format_group_2_end_char, 2, 3, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) + self.label_5 = QtWidgets.QLabel(self.gb_group_settings) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.label_5.sizePolicy().hasHeightForWidth()) + self.label_5.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.label_5.setFont(font) + self.label_5.setObjectName("label_5") + self.gridLayout.addWidget(self.label_5, 1, 0, 1, 1, QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self.format_group_3_end_char = QtWidgets.QLineEdit(self.gb_group_settings) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.format_group_3_end_char.sizePolicy().hasHeightForWidth()) + self.format_group_3_end_char.setSizePolicy(sizePolicy) + self.format_group_3_end_char.setMaximumSize(QtCore.QSize(50, 16777215)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.format_group_3_end_char.setFont(font) + self.format_group_3_end_char.setObjectName("format_group_3_end_char") + self.gridLayout.addWidget(self.format_group_3_end_char, 3, 3, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) + self.format_group_2_start_char = QtWidgets.QLineEdit(self.gb_group_settings) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.format_group_2_start_char.sizePolicy().hasHeightForWidth()) + self.format_group_2_start_char.setSizePolicy(sizePolicy) + self.format_group_2_start_char.setMaximumSize(QtCore.QSize(50, 16777215)) + font = QtGui.QFont() + font.setBold(False) + font.setWeight(50) + self.format_group_2_start_char.setFont(font) + self.format_group_2_start_char.setObjectName("format_group_2_start_char") + self.gridLayout.addWidget(self.format_group_2_start_char, 2, 1, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) + self.horizontalLayout.addLayout(self.gridLayout) + self.verticalLayout_2.addWidget(self.gb_group_settings) + spacerItem = QtWidgets.QSpacerItem(20, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout_2.addItem(spacerItem) + self.scrollArea.setWidget(self.scrollAreaWidgetContents) + self.vboxlayout.addWidget(self.scrollArea, 0, QtCore.Qt.AlignTop) + + self.retranslateUi(FormatPerformerTagsOptionsPage) + QtCore.QMetaObject.connectSlotsByName(FormatPerformerTagsOptionsPage) + FormatPerformerTagsOptionsPage.setTabOrder(self.additional_rb_1, self.additional_rb_2) + FormatPerformerTagsOptionsPage.setTabOrder(self.additional_rb_2, self.additional_rb_3) + FormatPerformerTagsOptionsPage.setTabOrder(self.additional_rb_3, self.additional_rb_4) + FormatPerformerTagsOptionsPage.setTabOrder(self.additional_rb_4, self.guest_rb_1) + FormatPerformerTagsOptionsPage.setTabOrder(self.guest_rb_1, self.guest_rb_2) + FormatPerformerTagsOptionsPage.setTabOrder(self.guest_rb_2, self.guest_rb_3) + FormatPerformerTagsOptionsPage.setTabOrder(self.guest_rb_3, self.guest_rb_4) + FormatPerformerTagsOptionsPage.setTabOrder(self.guest_rb_4, self.solo_rb_1) + FormatPerformerTagsOptionsPage.setTabOrder(self.solo_rb_1, self.solo_rb_2) + FormatPerformerTagsOptionsPage.setTabOrder(self.solo_rb_2, self.solo_rb_3) + FormatPerformerTagsOptionsPage.setTabOrder(self.solo_rb_3, self.solo_rb_4) + FormatPerformerTagsOptionsPage.setTabOrder(self.solo_rb_4, self.vocals_rb_1) + FormatPerformerTagsOptionsPage.setTabOrder(self.vocals_rb_1, self.vocals_rb_2) + FormatPerformerTagsOptionsPage.setTabOrder(self.vocals_rb_2, self.vocals_rb_3) + FormatPerformerTagsOptionsPage.setTabOrder(self.vocals_rb_3, self.vocals_rb_4) + FormatPerformerTagsOptionsPage.setTabOrder(self.vocals_rb_4, self.format_group_1_start_char) + FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_1_start_char, self.format_group_1_sep_char) + FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_1_sep_char, self.format_group_1_end_char) + FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_1_end_char, self.format_group_2_start_char) + FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_2_start_char, self.format_group_2_sep_char) + FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_2_sep_char, self.format_group_2_end_char) + FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_2_end_char, self.format_group_3_start_char) + FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_3_start_char, self.format_group_3_sep_char) + FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_3_sep_char, self.format_group_3_end_char) + FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_3_end_char, self.format_group_4_start_char) + FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_4_start_char, self.format_group_4_sep_char) + FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_4_sep_char, self.format_group_4_end_char) + FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_4_end_char, self.scrollArea) + + def retranslateUi(self, FormatPerformerTagsOptionsPage): + _translate = QtCore.QCoreApplication.translate + self.gb_description.setTitle(_("Format Performer Tags")) + self.format_description.setText(_("

    These settings will determine the format for any Performer tags prepared. The format is divided into six parts: the performer; the instrument or "vocals"; and four user selectable sections for the extra information. This is set out as:

    [Section 1]Instrument/Vocals[Section 2][Section 3]: Performer[Section 4]

    You can select the section in which each of the extra information words appears.

    For each of the sections you can select the starting character(s), the character(s) separating entries, and the ending character(s). Note that leading or trailing spaces must be included in the settings and will not be automatically added. If no separator characters are entered, the items within a section will be automatically separated by a single space.

    Please visit the repository on GitHub for additional information.

    ")) + self.gb_word_groups.setTitle(_("Keyword Section Assignments")) + self.group_additional.setTitle(_("Keyword: additional")) + self.additional_rb_1.setText(_("1")) + self.additional_rb_2.setText(_("2")) + self.additional_rb_3.setText(_("3")) + self.additional_rb_4.setText(_("4")) + self.group_guest.setTitle(_("Keyword: guest")) + self.guest_rb_1.setText(_("1")) + self.guest_rb_2.setText(_("2")) + self.guest_rb_3.setText(_("3")) + self.guest_rb_4.setText(_("4")) + self.group_solo.setTitle(_("Keyword: solo")) + self.solo_rb_1.setText(_("1")) + self.solo_rb_2.setText(_("2")) + self.solo_rb_3.setText(_("3")) + self.solo_rb_4.setText(_("4")) + self.group_vocals.setTitle(_("All vocal type keywords")) + self.vocals_rb_1.setText(_("1")) + self.vocals_rb_2.setText(_("2")) + self.vocals_rb_3.setText(_("3")) + self.vocals_rb_4.setText(_("4")) + self.gb_group_settings.setTitle(_("Section Display Settings")) + self.format_group_3_sep_char.setPlaceholderText(_("(blank)")) + self.format_group_1_start_char.setPlaceholderText(_("(blank)")) + self.format_group_1_sep_char.setPlaceholderText(_("(blank)")) + self.label_18.setText(_("Section 3 ")) + self.format_group_4_start_char.setText(_(" (")) + self.format_group_4_start_char.setPlaceholderText(_("(blank)")) + self.format_group_1_end_char.setPlaceholderText(_("(blank)")) + self.format_group_3_start_char.setText(_(" (")) + self.format_group_3_start_char.setPlaceholderText(_("(blank)")) + self.label.setText(_("Section 2 ")) + self.format_group_4_sep_char.setPlaceholderText(_("(blank)")) + self.label_19.setText(_("End Char(s)")) + self.label_7.setText(_("Section 4 ")) + self.format_group_2_sep_char.setPlaceholderText(_("(blank)")) + self.label_20.setText(_("Sep Char(s)")) + self.format_group_4_end_char.setText(_(")")) + self.format_group_4_end_char.setPlaceholderText(_("(blank)")) + self.label_15.setText(_("Start Char(s)")) + self.format_group_2_end_char.setPlaceholderText(_("(blank)")) + self.label_5.setText(_("Section 1 ")) + self.format_group_3_end_char.setText(_(")")) + self.format_group_3_end_char.setPlaceholderText(_("(blank)")) + self.format_group_2_start_char.setText(_(", ")) + self.format_group_2_start_char.setPlaceholderText(_("(blank)")) + From f8501d742f4557697aac2f85cbd925b71e3f6289 Mon Sep 17 00:00:00 2001 From: Wieland Hoffmann Date: Sat, 15 Dec 2018 12:59:05 +0100 Subject: [PATCH 070/123] cuesheet: Only encode complete lines Previously, `str` and `bytes objects` were appended, which fails on Python 3. Now, all lines are encoded only when they're complete. --- plugins/cuesheet/cuesheet.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/cuesheet/cuesheet.py b/plugins/cuesheet/cuesheet.py index b7f73aa7..03cb9595 100644 --- a/plugins/cuesheet/cuesheet.py +++ b/plugins/cuesheet/cuesheet.py @@ -134,7 +134,8 @@ def write(self): elif line[0] != "FILE": indent = 4 line2 = " ".join([self.quote(s) for s in line]) - lines.append(" " * indent + line2.encode("UTF-8") + "\n") + line2 = " " * indent + line2 + "\n" + lines.append(line2.encode("UTF-8")) with open(encode_filename(self.filename), "wt") as f: f.writelines(lines) From 601bb2e361385409079bfb573d6a3174f86a2502 Mon Sep 17 00:00:00 2001 From: Wieland Hoffmann Date: Sat, 15 Dec 2018 13:00:09 +0100 Subject: [PATCH 071/123] cuesheet: Open the cuesheet file in binary mode Bytes are being written to it. --- plugins/cuesheet/cuesheet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cuesheet/cuesheet.py b/plugins/cuesheet/cuesheet.py index 03cb9595..60c6cdc9 100644 --- a/plugins/cuesheet/cuesheet.py +++ b/plugins/cuesheet/cuesheet.py @@ -136,7 +136,7 @@ def write(self): line2 = " ".join([self.quote(s) for s in line]) line2 = " " * indent + line2 + "\n" lines.append(line2.encode("UTF-8")) - with open(encode_filename(self.filename), "wt") as f: + with open(encode_filename(self.filename), "wb") as f: f.writelines(lines) From b1b2fbbeb38f62c74ff94273539ec4421b5b8fd1 Mon Sep 17 00:00:00 2001 From: Wieland Hoffmann Date: Mon, 17 Dec 2018 09:35:02 +0100 Subject: [PATCH 072/123] cuesheet: Bump the version number This is a follow-up to 601bb2e361385409079bfb573d6a3174f86a2502. --- plugins/cuesheet/cuesheet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cuesheet/cuesheet.py b/plugins/cuesheet/cuesheet.py index 60c6cdc9..c95351f2 100644 --- a/plugins/cuesheet/cuesheet.py +++ b/plugins/cuesheet/cuesheet.py @@ -3,7 +3,7 @@ PLUGIN_NAME = "Generate Cuesheet" PLUGIN_AUTHOR = "Lukáš Lalinský, Sambhav Kothari" PLUGIN_DESCRIPTION = "Generate cuesheet (.cue file) from an album." -PLUGIN_VERSION = "1.0" +PLUGIN_VERSION = "1.1" PLUGIN_API_VERSIONS = ["2.0"] From 7f982ac58bc9ad4253d4f5d1889939ce4fd47116 Mon Sep 17 00:00:00 2001 From: Bob Swift Date: Mon, 17 Dec 2018 15:02:47 -0700 Subject: [PATCH 073/123] Add ui changes by phw --- plugins/format_performer_tags/__init__.py | 246 +++-- .../options_format_performer_tags.ui | 871 ++++-------------- .../ui_options_format_performer_tags.py | 770 +++++----------- 3 files changed, 562 insertions(+), 1325 deletions(-) diff --git a/plugins/format_performer_tags/__init__.py b/plugins/format_performer_tags/__init__.py index b4a27ea5..2d4603bc 100644 --- a/plugins/format_performer_tags/__init__.py +++ b/plugins/format_performer_tags/__init__.py @@ -18,7 +18,7 @@ # 02110-1301, USA. PLUGIN_NAME = 'Format Performer Tags' -PLUGIN_AUTHOR = 'Bob Swift (rdswift)' +PLUGIN_AUTHOR = 'Bob Swift (rdswift), Philipp Wolfer' PLUGIN_DESCRIPTION = ''' This plugin provides options with respect to the formatting of performer tags. It has been developed using the 'Standardise Performers' plugin by @@ -27,9 +27,9 @@ in the option settings page. ''' -PLUGIN_VERSION = "0.05" +PLUGIN_VERSION = "0.6" PLUGIN_API_VERSIONS = ["2.0"] -PLUGIN_LICENSE = "GPL-2.0 or later" +PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" PLUGIN_USER_GUIDE_URL = "https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/format_performer_tags/docs/README.md" @@ -38,7 +38,7 @@ import re from picard import config, log -from picard.metadata import register_track_metadata_processor +from picard.metadata import Metadata, register_track_metadata_processor from picard.plugin import PluginPriority from picard.ui.options import register_options_page, OptionsPage from picard.plugins.format_performer_tags.ui_options_format_performer_tags import Ui_FormatPerformerTagsOptionsPage @@ -47,72 +47,82 @@ WORD_LIST = ['guest', 'solo', 'additional'] -def format_performer_tags(album, metadata, *args): + +def get_word_dict(settings): word_dict = {} for word in WORD_LIST: - word_dict[word] = config.setting['format_group_' + word] - for key, values in list(filter(lambda filter_tuple: filter_tuple[0].startswith('performer:') or filter_tuple[0].startswith('~performersort:'), metadata.rawitems())): - mainkey, subkey = key.split(':', 1) - if not subkey: - continue - log.debug("%s: Formatting Performer [%s: %s]", PLUGIN_NAME, subkey, values,) - instruments = performers_split(subkey) - for instrument in instruments: - if DEV_TESTING: - log.debug("%s: instrument (first pass): '%s'", PLUGIN_NAME, instrument,) - if instrument in WORD_LIST: - instruments[0] = "{0} {1}".format(instruments[0], instrument,) - instruments.remove(instrument) - groups = { 1: [], 2: [], 3: [], 4: [], } - words = instruments[0].split() - for word in words[:]: - if word in WORD_LIST: - groups[word_dict[word]].append(word) - words.remove(word) - instruments[0] = " ".join(words) - display_group = {} - for group_number in range(1, 5): - if groups[group_number]: - if DEV_TESTING: - log.debug("%s: groups[%s]: %s", PLUGIN_NAME, group_number, groups[group_number],) - group_separator = config.setting["format_group_{0}_sep_char".format(group_number)] - if not group_separator: - group_separator = " " - display_group[group_number] = config.setting["format_group_{0}_start_char".format(group_number)] \ - + group_separator.join(groups[group_number]) \ - + config.setting["format_group_{0}_end_char".format(group_number)] - else: - display_group[group_number] = "" + word_dict[word] = settings['format_group_' + word] + return word_dict + + +def rewrite_tag(key, values, metadata, word_dict, settings): + mainkey, subkey = key.split(':', 1) + if not subkey: + return + log.debug("%s: Formatting Performer [%s: %s]", PLUGIN_NAME, subkey, values,) + instruments = performers_split(subkey) + for instrument in instruments: if DEV_TESTING: - log.debug("%s: display_group: %s", PLUGIN_NAME, display_group,) - del metadata[key] - for instrument in instruments: + log.debug("%s: instrument (first pass): '%s'", PLUGIN_NAME, instrument,) + if instrument in WORD_LIST: + instruments[0] = "{0} {1}".format(instruments[0], instrument,) + instruments.remove(instrument) + groups = { 1: [], 2: [], 3: [], 4: [], } + words = instruments[0].split() + for word in words[:]: + if word in WORD_LIST: + groups[word_dict[word]].append(word) + words.remove(word) + instruments[0] = " ".join(words) + display_group = {} + for group_number in range(1, 5): + if groups[group_number]: if DEV_TESTING: - log.debug("%s: instrument (second pass): '%s'", PLUGIN_NAME, instrument,) - words = instrument.split() - if (len(words) > 1) and (words[-1] in ["vocal", "vocals",]): - vocals = " ".join(words[:-1]) - instrument = words[-1] + log.debug("%s: groups[%s]: %s", PLUGIN_NAME, group_number, groups[group_number],) + group_separator = settings["format_group_{0}_sep_char".format(group_number)] + if not group_separator: + group_separator = " " + display_group[group_number] = settings["format_group_{0}_start_char".format(group_number)] \ + + group_separator.join(groups[group_number]) \ + + settings["format_group_{0}_end_char".format(group_number)] + else: + display_group[group_number] = "" + if DEV_TESTING: + log.debug("%s: display_group: %s", PLUGIN_NAME, display_group,) + metadata.delete(key) + for instrument in instruments: + if DEV_TESTING: + log.debug("%s: instrument (second pass): '%s'", PLUGIN_NAME, instrument,) + words = instrument.split() + if (len(words) > 1) and (words[-1] in ["vocal", "vocals",]): + vocals = " ".join(words[:-1]) + instrument = words[-1] + else: + vocals = "" + if vocals: + group_number = settings["format_group_vocals"] + temp_group = groups[group_number][:] + if group_number < 2: + temp_group.append(vocals) else: - vocals = "" - if vocals: - group_number = config.setting["format_group_vocals"] - temp_group = groups[group_number][:] - if group_number < 2: - temp_group.append(vocals) - else: - temp_group.insert(0, vocals) - group_separator = config.setting["format_group_{0}_sep_char".format(group_number)] - if not group_separator: - group_separator = " " - display_group[group_number] = config.setting["format_group_{0}_start_char".format(group_number)] \ - + group_separator.join(temp_group) \ - + config.setting["format_group_{0}_end_char".format(group_number)] - - newkey = ('%s:%s%s%s%s' % (mainkey, display_group[1], instrument, display_group[2], display_group[3],)) - log.debug("%s: newkey: %s", PLUGIN_NAME, newkey,) - for value in values: - metadata.add_unique(newkey, (value + display_group[4])) + temp_group.insert(0, vocals) + group_separator = settings["format_group_{0}_sep_char".format(group_number)] + if not group_separator: + group_separator = " " + display_group[group_number] = settings["format_group_{0}_start_char".format(group_number)] \ + + group_separator.join(temp_group) \ + + settings["format_group_{0}_end_char".format(group_number)] + + newkey = ('%s:%s%s%s%s' % (mainkey, display_group[1], instrument, display_group[2], display_group[3],)) + log.debug("%s: newkey: %s", PLUGIN_NAME, newkey,) + for value in values: + metadata.add_unique(newkey, (value + display_group[4])) + + +def format_performer_tags(album, metadata, *args): + word_dict = get_word_dict(config.setting) + for key, values in list(filter(lambda filter_tuple: filter_tuple[0].startswith('performer:') or filter_tuple[0].startswith('~performersort:'), metadata.rawitems())): + rewrite_tag(key, values, metadata, word_dict, config.setting) class FormatPerformerTagsOptionsPage(OptionsPage): @@ -127,7 +137,7 @@ class FormatPerformerTagsOptionsPage(OptionsPage): config.IntOption("setting", "format_group_solo", 3), config.IntOption("setting", "format_group_vocals", 2), config.TextOption("setting", "format_group_1_start_char", ''), - config.TextOption("setting", "format_group_1_end_char", ''), + config.TextOption("setting", "format_group_1_end_char", ' '), config.TextOption("setting", "format_group_1_sep_char", ''), config.TextOption("setting", "format_group_2_start_char", ', '), config.TextOption("setting", "format_group_2_end_char", ''), @@ -144,6 +154,34 @@ def __init__(self, parent=None): super(FormatPerformerTagsOptionsPage, self).__init__(parent) self.ui = Ui_FormatPerformerTagsOptionsPage() self.ui.setupUi(self) + self.ui.additional_rb_1.clicked.connect(self.update_examples) + self.ui.additional_rb_2.clicked.connect(self.update_examples) + self.ui.additional_rb_3.clicked.connect(self.update_examples) + self.ui.additional_rb_4.clicked.connect(self.update_examples) + self.ui.guest_rb_1.clicked.connect(self.update_examples) + self.ui.guest_rb_2.clicked.connect(self.update_examples) + self.ui.guest_rb_3.clicked.connect(self.update_examples) + self.ui.guest_rb_4.clicked.connect(self.update_examples) + self.ui.solo_rb_1.clicked.connect(self.update_examples) + self.ui.solo_rb_2.clicked.connect(self.update_examples) + self.ui.solo_rb_3.clicked.connect(self.update_examples) + self.ui.solo_rb_4.clicked.connect(self.update_examples) + self.ui.vocals_rb_1.clicked.connect(self.update_examples) + self.ui.vocals_rb_2.clicked.connect(self.update_examples) + self.ui.vocals_rb_3.clicked.connect(self.update_examples) + self.ui.vocals_rb_4.clicked.connect(self.update_examples) + self.ui.format_group_1_start_char.editingFinished.connect(self.update_examples) + self.ui.format_group_2_start_char.editingFinished.connect(self.update_examples) + self.ui.format_group_3_start_char.editingFinished.connect(self.update_examples) + self.ui.format_group_4_start_char.editingFinished.connect(self.update_examples) + self.ui.format_group_1_sep_char.editingFinished.connect(self.update_examples) + self.ui.format_group_2_sep_char.editingFinished.connect(self.update_examples) + self.ui.format_group_3_sep_char.editingFinished.connect(self.update_examples) + self.ui.format_group_4_sep_char.editingFinished.connect(self.update_examples) + self.ui.format_group_1_end_char.editingFinished.connect(self.update_examples) + self.ui.format_group_2_end_char.editingFinished.connect(self.update_examples) + self.ui.format_group_3_end_char.editingFinished.connect(self.update_examples) + self.ui.format_group_4_end_char.editingFinished.connect(self.update_examples) def load(self): # Enable external link @@ -212,6 +250,7 @@ def load(self): self.ui.format_group_4_start_char.setText(config.setting["format_group_4_start_char"]) self.ui.format_group_4_end_char.setText(config.setting["format_group_4_end_char"]) self.ui.format_group_4_sep_char.setText(config.setting["format_group_4_sep_char"]) + self.update_examples() # TODO: Modify self.format_description in ui_options_format_performer_tags.py to include a placeholder # such as {user_guide_url} so that the translated string can be formatted to include the value @@ -223,53 +262,96 @@ def load(self): def save(self): + self._set_settings(config.setting) + + def restore_defaults(self): + super().restore_defaults() + self.update_examples() + + def _set_settings(self, settings): + # Process 'additional' keyword settings temp = 1 if self.ui.additional_rb_2.isChecked(): temp = 2 if self.ui.additional_rb_3.isChecked(): temp = 3 if self.ui.additional_rb_4.isChecked(): temp = 4 - config.setting["format_group_additional"] = temp + settings["format_group_additional"] = temp # Process 'guest' keyword settings temp = 1 if self.ui.guest_rb_2.isChecked(): temp = 2 if self.ui.guest_rb_3.isChecked(): temp = 3 if self.ui.guest_rb_4.isChecked(): temp = 4 - config.setting["format_group_guest"] = temp + settings["format_group_guest"] = temp # Process 'solo' keyword settings temp = 1 if self.ui.solo_rb_2.isChecked(): temp = 2 if self.ui.solo_rb_3.isChecked(): temp = 3 if self.ui.solo_rb_4.isChecked(): temp = 4 - config.setting["format_group_solo"] = temp + settings["format_group_solo"] = temp # Process all vocal keyword settings temp = 1 if self.ui.vocals_rb_2.isChecked(): temp = 2 if self.ui.vocals_rb_3.isChecked(): temp = 3 if self.ui.vocals_rb_4.isChecked(): temp = 4 - config.setting["format_group_vocals"] = temp + settings["format_group_vocals"] = temp # Settings for word group 1 - config.setting["format_group_1_start_char"] = self.ui.format_group_1_start_char.text() - config.setting["format_group_1_end_char"] = self.ui.format_group_1_end_char.text() - config.setting["format_group_1_sep_char"] = self.ui.format_group_1_sep_char.text() + settings["format_group_1_start_char"] = self.ui.format_group_1_start_char.text() + settings["format_group_1_end_char"] = self.ui.format_group_1_end_char.text() + settings["format_group_1_sep_char"] = self.ui.format_group_1_sep_char.text() # Settings for word group 2 - config.setting["format_group_2_start_char"] = self.ui.format_group_2_start_char.text() - config.setting["format_group_2_end_char"] = self.ui.format_group_2_end_char.text() - config.setting["format_group_2_sep_char"] = self.ui.format_group_2_sep_char.text() + settings["format_group_2_start_char"] = self.ui.format_group_2_start_char.text() + settings["format_group_2_end_char"] = self.ui.format_group_2_end_char.text() + settings["format_group_2_sep_char"] = self.ui.format_group_2_sep_char.text() # Settings for word group 3 - config.setting["format_group_3_start_char"] = self.ui.format_group_3_start_char.text() - config.setting["format_group_3_end_char"] = self.ui.format_group_3_end_char.text() - config.setting["format_group_3_sep_char"] = self.ui.format_group_3_sep_char.text() + settings["format_group_3_start_char"] = self.ui.format_group_3_start_char.text() + settings["format_group_3_end_char"] = self.ui.format_group_3_end_char.text() + settings["format_group_3_sep_char"] = self.ui.format_group_3_sep_char.text() # Settings for word group 4 - config.setting["format_group_4_start_char"] = self.ui.format_group_4_start_char.text() - config.setting["format_group_4_end_char"] = self.ui.format_group_4_end_char.text() - config.setting["format_group_4_sep_char"] = self.ui.format_group_4_sep_char.text() + settings["format_group_4_start_char"] = self.ui.format_group_4_start_char.text() + settings["format_group_4_end_char"] = self.ui.format_group_4_end_char.text() + settings["format_group_4_sep_char"] = self.ui.format_group_4_sep_char.text() + + def update_examples(self): + settings = {} + self._set_settings(settings) + word_dict = get_word_dict(settings) + + instruments_credits = { + "guitar": ["Johnny Flux", "John Watson"], + "guest guitar": ["Jimmy Page"], + "additional guest solo guitar": ["Jimmy Page"], + } + instruments_example = self.build_example(instruments_credits, word_dict, settings) + self.ui.example_instruments.setText(instruments_example) + + vocals_credits = { + "additional solo lead vocals": ["Robert Plant"], + "additional solo guest lead vocals": ["Sandy Denny"], + } + vocals_example = self.build_example(vocals_credits, word_dict, settings) + self.ui.example_vocals.setText(vocals_example) + + @staticmethod + def build_example(credits, word_dict, settings): + prefix = "performer:" + metadata = Metadata() + for key, values in credits.items(): + rewrite_tag(prefix + key, values, metadata, word_dict, settings) + + examples = [] + for key, values in metadata.rawitems(): + credit = "%s: %s" % (key, ", ".join(values)) + if credit.startswith(prefix): + credit = credit[len(prefix):] + examples.append(credit) + return "\n".join(examples) # Register the plugin to run at a HIGH priority. diff --git a/plugins/format_performer_tags/options_format_performer_tags.ui b/plugins/format_performer_tags/options_format_performer_tags.ui index dc31c1e4..6b24524e 100644 --- a/plugins/format_performer_tags/options_format_performer_tags.ui +++ b/plugins/format_performer_tags/options_format_performer_tags.ui @@ -6,89 +6,44 @@ 0 0 - 458 - 673 + 561 + 802
    - - - 0 - 0 - + + + 360 + 0 + - - - 16 - - + + Form + + + - - - 0 - 0 - - - - - 440 - 655 - - QFrame::NoFrame true - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - 0 - 0 - 440 - 655 + -13 + 529 + 848 - - - 0 - 0 - - - - - 0 - 50 - - - - - 75 - true - - Format Performer Tags - - - 9 - - - 9 - - - 9 - - - 1 - + @@ -97,18 +52,9 @@ 0 - - - 50 - false - - <html><head/><body><p>These settings will determine the format for any <span style=" font-weight:600;">Performer</span> tags prepared. The format is divided into six parts: the performer; the instrument or &quot;vocals&quot;; and four user selectable sections for the extra information. This is set out as:</p><p align="center"><span style=" font-weight:600;">[Section 1]</span>Instrument/Vocals<span style=" font-weight:600;">[Section 2][Section 3]</span>: Performer<span style=" font-weight:600;">[Section 4]</span></p><p>You can select the section in which each of the extra information words appears.</p><p>For each of the sections you can select the starting character(s), the character(s) separating entries, and the ending character(s). Note that leading or trailing spaces must be included in the settings and will not be automatically added. If no separator characters are entered, the items within a section will be automatically separated by a single space.</p><p>Please visit the repository on GitHub for <a href="https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/format_performer_tags/docs/README.md"><span style=" text-decoration: underline; color:#0000ff;">additional information</span></a>.</p></body></html> - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - true @@ -117,158 +63,77 @@ - + - - - 75 - true - - - Keyword Section Assignments + Keyword Sections Assignment - + - - - - 0 - 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - - 50 - false - - + Keyword: additional - - - 3 - + 1 - 0 + 1 1 - 0 + 1 - + - - - 0 - 0 - - 40 - 20 + 16777215 - - - 50 - false - - 1 - - false - - + - - - 0 - 0 - - 40 - 20 + 16777215 - - - 50 - false - - 2 - + - - - 0 - 0 - - 40 - 20 + 16777215 - - - 50 - false - - 3 - - true - - + 40 - 20 + 16777215 - - - 50 - false - - 4 @@ -279,135 +144,72 @@ - - - 0 - 0 - - - - - 50 - false - - Keyword: guest - - - 3 - + 1 - 0 + 1 1 - 0 + 1 - + - - - 0 - 0 - - 40 - 20 + 16777215 - - - 50 - false - - 1 - - false - - + - - - 0 - 0 - - 40 - 20 + 16777215 - - - 50 - false - - 2 - + - - - 0 - 0 - - 40 - 20 + 16777215 - - - 50 - false - - 3 - + 40 - 20 + 16777215 - - - 50 - false - - 4 - - true - @@ -415,132 +217,69 @@ - - - 0 - 0 - - - - - 50 - false - - Keyword: solo - - - 3 - + 1 - 0 + 1 1 - 0 + 1 - + - - - 0 - 0 - - 40 - 20 + 16777215 - - - 50 - false - - 1 - - false - - + - - - 0 - 0 - - 40 - 20 + 16777215 - - - 50 - false - - 2 - + - - - 0 - 0 - - 40 - 20 + 16777215 - - - 50 - false - - 3 - - true - - + 40 - 20 + 16777215 - - - 50 - false - - 4 @@ -551,22 +290,10 @@ - - - 0 - 0 - - - - - 50 - false - - All vocal type keywords - + 1 @@ -579,101 +306,59 @@ 1 - + - - - 0 - 0 - - - + 40 20 - - - 50 - false - + + + 40 + 16777215 + 1 - - false - - + - - - 0 - 0 - - 40 - 20 + 16777215 - - - 50 - false - - 2 - - true - - + - - - 0 - 0 - - 40 - 20 + 16777215 - - - 50 - false - - 3 - + 40 - 20 + 16777215 - - - 50 - false - - 4 @@ -687,113 +372,68 @@ - - - 0 - 50 - - - - - 75 - true - - Section Display Settings - + - - 3 - 1 - - + + - + 0 0 - - - 50 - 16777215 - - - 50 - false + 75 + true - - (blank) + + Section 2 - - + + - + 0 0 - - - 50 - 16777215 - - - 50 - false + 75 + true - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - (blank) - - - false + + Section 3 - - - - - 0 - 0 - - + + 50 16777215 - - - 50 - false - - (blank) - - + + 0 @@ -807,99 +447,60 @@ - Section 3 + Section 1 - - + + - + 0 0 - - - 50 - 16777215 - - - 50 - false + 75 + true - ( - - - (blank) + Section 4 - - - - - 0 - 0 - - + + 50 16777215 - - - 50 - false - - (blank) - - - - - 0 - 0 - - + + 50 16777215 - - - 50 - false - - - ( + (blank) - - - - - 0 - 0 - - + + 75 @@ -907,37 +508,12 @@ - Section 2 - - - - - - - - 0 - 0 - - - - - 50 - 16777215 - - - - - 50 - false - - - - (blank) + Start Char(s) - - + + 75 @@ -945,24 +521,12 @@ - End Char(s) - - - Qt::AlignCenter - - - true + Sep Char(s) - + - - - 0 - 0 - - 75 @@ -970,169 +534,118 @@ - Section 4 + End Char(s) - - - - - 0 - 0 - - + + 50 16777215 - - - 50 - false - + + , (blank) - - - - - 75 - true - + + + + + 50 + 16777215 + - Sep Char(s) - - - Qt::AlignCenter + ( - - true + + (blank) - - - - - 0 - 0 - - + + 50 16777215 - - - 50 - false - - - ) + ( (blank) - - - - - 75 - true - false - - - - Start Char(s) - - - Qt::AlignCenter + + + + + 50 + 16777215 + - - true + + (blank) - - - - - 0 - 0 - - + + 50 16777215 - - - 50 - false - + + (blank) - - + + + + + + + 50 + 16777215 + (blank) - - - - - 0 - 0 - - - - - 75 - true - + + + + + 50 + 16777215 + - - Section 1 + + (blank) - + - - - 0 - 0 - - 50 16777215 - - - 50 - false - - ) @@ -1141,28 +654,16 @@ - - - - - 0 - 0 - - + + 50 16777215 - - - 50 - false - - - , + ) (blank) @@ -1174,6 +675,29 @@ + + + + Examples + + + + + + + + + + + + + + + + + + + @@ -1182,7 +706,7 @@ 20 - 0 + 40 @@ -1193,37 +717,6 @@ - - additional_rb_1 - additional_rb_2 - additional_rb_3 - additional_rb_4 - guest_rb_1 - guest_rb_2 - guest_rb_3 - guest_rb_4 - solo_rb_1 - solo_rb_2 - solo_rb_3 - solo_rb_4 - vocals_rb_1 - vocals_rb_2 - vocals_rb_3 - vocals_rb_4 - format_group_1_start_char - format_group_1_sep_char - format_group_1_end_char - format_group_2_start_char - format_group_2_sep_char - format_group_2_end_char - format_group_3_start_char - format_group_3_sep_char - format_group_3_end_char - format_group_4_start_char - format_group_4_sep_char - format_group_4_end_char - scrollArea -
    diff --git a/plugins/format_performer_tags/ui_options_format_performer_tags.py b/plugins/format_performer_tags/ui_options_format_performer_tags.py index 6f0fd1ec..c8a9dd69 100644 --- a/plugins/format_performer_tags/ui_options_format_performer_tags.py +++ b/plugins/format_performer_tags/ui_options_format_performer_tags.py @@ -1,444 +1,172 @@ # -*- coding: utf-8 -*- -# Automatically generated - don't edit. -# Use `python setup.py build_ui` to update it. +# Form implementation generated from reading ui file 'options_format_performer_tags_2.ui' +# +# Created by: PyQt5 UI code generator 5.11.3 +# +# WARNING! All changes made in this file will be lost! from PyQt5 import QtCore, QtGui, QtWidgets class Ui_FormatPerformerTagsOptionsPage(object): def setupUi(self, FormatPerformerTagsOptionsPage): FormatPerformerTagsOptionsPage.setObjectName("FormatPerformerTagsOptionsPage") - FormatPerformerTagsOptionsPage.resize(458, 673) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(FormatPerformerTagsOptionsPage.sizePolicy().hasHeightForWidth()) - FormatPerformerTagsOptionsPage.setSizePolicy(sizePolicy) - self.vboxlayout = QtWidgets.QVBoxLayout(FormatPerformerTagsOptionsPage) - self.vboxlayout.setSpacing(16) - self.vboxlayout.setObjectName("vboxlayout") + FormatPerformerTagsOptionsPage.resize(561, 802) + FormatPerformerTagsOptionsPage.setMinimumSize(QtCore.QSize(360, 0)) + self.verticalLayout = QtWidgets.QVBoxLayout(FormatPerformerTagsOptionsPage) + self.verticalLayout.setObjectName("verticalLayout") self.scrollArea = QtWidgets.QScrollArea(FormatPerformerTagsOptionsPage) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.scrollArea.sizePolicy().hasHeightForWidth()) - self.scrollArea.setSizePolicy(sizePolicy) - self.scrollArea.setMinimumSize(QtCore.QSize(440, 655)) self.scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame) self.scrollArea.setWidgetResizable(True) - self.scrollArea.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) self.scrollArea.setObjectName("scrollArea") self.scrollAreaWidgetContents = QtWidgets.QWidget() - self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 440, 655)) + self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, -13, 529, 848)) self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents) - self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) self.verticalLayout_2.setObjectName("verticalLayout_2") self.gb_description = QtWidgets.QGroupBox(self.scrollAreaWidgetContents) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.gb_description.sizePolicy().hasHeightForWidth()) - self.gb_description.setSizePolicy(sizePolicy) - self.gb_description.setMinimumSize(QtCore.QSize(0, 50)) - font = QtGui.QFont() - font.setBold(True) - font.setWeight(75) - self.gb_description.setFont(font) self.gb_description.setObjectName("gb_description") - self.verticalLayout = QtWidgets.QVBoxLayout(self.gb_description) - self.verticalLayout.setContentsMargins(9, 9, 9, 1) - self.verticalLayout.setObjectName("verticalLayout") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.gb_description) + self.verticalLayout_3.setObjectName("verticalLayout_3") self.format_description = QtWidgets.QLabel(self.gb_description) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.format_description.sizePolicy().hasHeightForWidth()) self.format_description.setSizePolicy(sizePolicy) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.format_description.setFont(font) - self.format_description.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) self.format_description.setWordWrap(True) self.format_description.setObjectName("format_description") - self.verticalLayout.addWidget(self.format_description) + self.verticalLayout_3.addWidget(self.format_description) self.verticalLayout_2.addWidget(self.gb_description) self.gb_word_groups = QtWidgets.QGroupBox(self.scrollAreaWidgetContents) - font = QtGui.QFont() - font.setBold(True) - font.setWeight(75) - self.gb_word_groups.setFont(font) self.gb_word_groups.setObjectName("gb_word_groups") - self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.gb_word_groups) - self.verticalLayout_3.setObjectName("verticalLayout_3") - self.group_additional = QtWidgets.QGroupBox(self.gb_word_groups) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.group_additional.sizePolicy().hasHeightForWidth()) - self.group_additional.setSizePolicy(sizePolicy) - self.group_additional.setMinimumSize(QtCore.QSize(0, 0)) - self.group_additional.setMaximumSize(QtCore.QSize(16777215, 16777215)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.group_additional.setFont(font) - self.group_additional.setObjectName("group_additional") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.group_additional) - self.horizontalLayout_2.setContentsMargins(1, 0, 1, 0) - self.horizontalLayout_2.setSpacing(3) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.additional_rb_1 = QtWidgets.QRadioButton(self.group_additional) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.additional_rb_1.sizePolicy().hasHeightForWidth()) - self.additional_rb_1.setSizePolicy(sizePolicy) - self.additional_rb_1.setMaximumSize(QtCore.QSize(40, 20)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.additional_rb_1.setFont(font) - self.additional_rb_1.setChecked(False) + self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.gb_word_groups) + self.verticalLayout_5.setObjectName("verticalLayout_5") + self.group_additonal = QtWidgets.QGroupBox(self.gb_word_groups) + self.group_additonal.setObjectName("group_additonal") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.group_additonal) + self.horizontalLayout.setContentsMargins(1, 1, 1, 1) + self.horizontalLayout.setObjectName("horizontalLayout") + self.additional_rb_1 = QtWidgets.QRadioButton(self.group_additonal) + self.additional_rb_1.setMaximumSize(QtCore.QSize(40, 16777215)) self.additional_rb_1.setObjectName("additional_rb_1") - self.horizontalLayout_2.addWidget(self.additional_rb_1, 0, QtCore.Qt.AlignVCenter) - self.additional_rb_2 = QtWidgets.QRadioButton(self.group_additional) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.additional_rb_2.sizePolicy().hasHeightForWidth()) - self.additional_rb_2.setSizePolicy(sizePolicy) - self.additional_rb_2.setMaximumSize(QtCore.QSize(40, 20)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.additional_rb_2.setFont(font) + self.horizontalLayout.addWidget(self.additional_rb_1) + self.additional_rb_2 = QtWidgets.QRadioButton(self.group_additonal) + self.additional_rb_2.setMaximumSize(QtCore.QSize(40, 16777215)) self.additional_rb_2.setObjectName("additional_rb_2") - self.horizontalLayout_2.addWidget(self.additional_rb_2, 0, QtCore.Qt.AlignVCenter) - self.additional_rb_3 = QtWidgets.QRadioButton(self.group_additional) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.additional_rb_3.sizePolicy().hasHeightForWidth()) - self.additional_rb_3.setSizePolicy(sizePolicy) - self.additional_rb_3.setMaximumSize(QtCore.QSize(40, 20)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.additional_rb_3.setFont(font) - self.additional_rb_3.setChecked(True) + self.horizontalLayout.addWidget(self.additional_rb_2) + self.additional_rb_3 = QtWidgets.QRadioButton(self.group_additonal) + self.additional_rb_3.setMaximumSize(QtCore.QSize(40, 16777215)) self.additional_rb_3.setObjectName("additional_rb_3") - self.horizontalLayout_2.addWidget(self.additional_rb_3, 0, QtCore.Qt.AlignVCenter) - self.additional_rb_4 = QtWidgets.QRadioButton(self.group_additional) - self.additional_rb_4.setMaximumSize(QtCore.QSize(40, 20)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.additional_rb_4.setFont(font) + self.horizontalLayout.addWidget(self.additional_rb_3) + self.additional_rb_4 = QtWidgets.QRadioButton(self.group_additonal) + self.additional_rb_4.setMaximumSize(QtCore.QSize(40, 16777215)) self.additional_rb_4.setObjectName("additional_rb_4") - self.horizontalLayout_2.addWidget(self.additional_rb_4, 0, QtCore.Qt.AlignVCenter) - self.verticalLayout_3.addWidget(self.group_additional) + self.horizontalLayout.addWidget(self.additional_rb_4) + self.verticalLayout_5.addWidget(self.group_additonal) self.group_guest = QtWidgets.QGroupBox(self.gb_word_groups) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.group_guest.sizePolicy().hasHeightForWidth()) - self.group_guest.setSizePolicy(sizePolicy) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.group_guest.setFont(font) self.group_guest.setObjectName("group_guest") - self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.group_guest) - self.horizontalLayout_3.setContentsMargins(1, 0, 1, 0) - self.horizontalLayout_3.setSpacing(3) - self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.group_guest) + self.horizontalLayout_2.setContentsMargins(1, 1, 1, 1) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") self.guest_rb_1 = QtWidgets.QRadioButton(self.group_guest) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.guest_rb_1.sizePolicy().hasHeightForWidth()) - self.guest_rb_1.setSizePolicy(sizePolicy) - self.guest_rb_1.setMaximumSize(QtCore.QSize(40, 20)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.guest_rb_1.setFont(font) - self.guest_rb_1.setChecked(False) + self.guest_rb_1.setMaximumSize(QtCore.QSize(40, 16777215)) self.guest_rb_1.setObjectName("guest_rb_1") - self.horizontalLayout_3.addWidget(self.guest_rb_1, 0, QtCore.Qt.AlignVCenter) + self.horizontalLayout_2.addWidget(self.guest_rb_1) self.guest_rb_2 = QtWidgets.QRadioButton(self.group_guest) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.guest_rb_2.sizePolicy().hasHeightForWidth()) - self.guest_rb_2.setSizePolicy(sizePolicy) - self.guest_rb_2.setMaximumSize(QtCore.QSize(40, 20)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.guest_rb_2.setFont(font) + self.guest_rb_2.setMaximumSize(QtCore.QSize(40, 16777215)) self.guest_rb_2.setObjectName("guest_rb_2") - self.horizontalLayout_3.addWidget(self.guest_rb_2, 0, QtCore.Qt.AlignVCenter) + self.horizontalLayout_2.addWidget(self.guest_rb_2) self.guest_rb_3 = QtWidgets.QRadioButton(self.group_guest) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.guest_rb_3.sizePolicy().hasHeightForWidth()) - self.guest_rb_3.setSizePolicy(sizePolicy) - self.guest_rb_3.setMaximumSize(QtCore.QSize(40, 20)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.guest_rb_3.setFont(font) + self.guest_rb_3.setMaximumSize(QtCore.QSize(40, 16777215)) self.guest_rb_3.setObjectName("guest_rb_3") - self.horizontalLayout_3.addWidget(self.guest_rb_3, 0, QtCore.Qt.AlignVCenter) + self.horizontalLayout_2.addWidget(self.guest_rb_3) self.guest_rb_4 = QtWidgets.QRadioButton(self.group_guest) - self.guest_rb_4.setMaximumSize(QtCore.QSize(40, 20)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.guest_rb_4.setFont(font) - self.guest_rb_4.setChecked(True) + self.guest_rb_4.setMaximumSize(QtCore.QSize(40, 16777215)) self.guest_rb_4.setObjectName("guest_rb_4") - self.horizontalLayout_3.addWidget(self.guest_rb_4, 0, QtCore.Qt.AlignVCenter) - self.verticalLayout_3.addWidget(self.group_guest) + self.horizontalLayout_2.addWidget(self.guest_rb_4) + self.verticalLayout_5.addWidget(self.group_guest) self.group_solo = QtWidgets.QGroupBox(self.gb_word_groups) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.group_solo.sizePolicy().hasHeightForWidth()) - self.group_solo.setSizePolicy(sizePolicy) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.group_solo.setFont(font) self.group_solo.setObjectName("group_solo") - self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.group_solo) - self.horizontalLayout_4.setContentsMargins(1, 0, 1, 0) - self.horizontalLayout_4.setSpacing(3) - self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.group_solo) + self.horizontalLayout_3.setContentsMargins(1, 1, 1, 1) + self.horizontalLayout_3.setObjectName("horizontalLayout_3") self.solo_rb_1 = QtWidgets.QRadioButton(self.group_solo) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.solo_rb_1.sizePolicy().hasHeightForWidth()) - self.solo_rb_1.setSizePolicy(sizePolicy) - self.solo_rb_1.setMaximumSize(QtCore.QSize(40, 20)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.solo_rb_1.setFont(font) - self.solo_rb_1.setChecked(False) + self.solo_rb_1.setMaximumSize(QtCore.QSize(40, 16777215)) self.solo_rb_1.setObjectName("solo_rb_1") - self.horizontalLayout_4.addWidget(self.solo_rb_1, 0, QtCore.Qt.AlignVCenter) + self.horizontalLayout_3.addWidget(self.solo_rb_1) self.solo_rb_2 = QtWidgets.QRadioButton(self.group_solo) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.solo_rb_2.sizePolicy().hasHeightForWidth()) - self.solo_rb_2.setSizePolicy(sizePolicy) - self.solo_rb_2.setMaximumSize(QtCore.QSize(40, 20)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.solo_rb_2.setFont(font) + self.solo_rb_2.setMaximumSize(QtCore.QSize(40, 16777215)) self.solo_rb_2.setObjectName("solo_rb_2") - self.horizontalLayout_4.addWidget(self.solo_rb_2, 0, QtCore.Qt.AlignVCenter) + self.horizontalLayout_3.addWidget(self.solo_rb_2) self.solo_rb_3 = QtWidgets.QRadioButton(self.group_solo) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.solo_rb_3.sizePolicy().hasHeightForWidth()) - self.solo_rb_3.setSizePolicy(sizePolicy) - self.solo_rb_3.setMaximumSize(QtCore.QSize(40, 20)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.solo_rb_3.setFont(font) - self.solo_rb_3.setChecked(True) + self.solo_rb_3.setMaximumSize(QtCore.QSize(40, 16777215)) self.solo_rb_3.setObjectName("solo_rb_3") - self.horizontalLayout_4.addWidget(self.solo_rb_3, 0, QtCore.Qt.AlignVCenter) + self.horizontalLayout_3.addWidget(self.solo_rb_3) self.solo_rb_4 = QtWidgets.QRadioButton(self.group_solo) - self.solo_rb_4.setMaximumSize(QtCore.QSize(40, 20)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.solo_rb_4.setFont(font) + self.solo_rb_4.setMaximumSize(QtCore.QSize(40, 16777215)) self.solo_rb_4.setObjectName("solo_rb_4") - self.horizontalLayout_4.addWidget(self.solo_rb_4, 0, QtCore.Qt.AlignVCenter) - self.verticalLayout_3.addWidget(self.group_solo) + self.horizontalLayout_3.addWidget(self.solo_rb_4) + self.verticalLayout_5.addWidget(self.group_solo) self.group_vocals = QtWidgets.QGroupBox(self.gb_word_groups) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.group_vocals.sizePolicy().hasHeightForWidth()) - self.group_vocals.setSizePolicy(sizePolicy) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.group_vocals.setFont(font) self.group_vocals.setObjectName("group_vocals") - self.horizontalLayout_5 = QtWidgets.QHBoxLayout(self.group_vocals) - self.horizontalLayout_5.setContentsMargins(1, 1, 1, 1) - self.horizontalLayout_5.setObjectName("horizontalLayout_5") + self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.group_vocals) + self.horizontalLayout_4.setContentsMargins(1, 1, 1, 1) + self.horizontalLayout_4.setObjectName("horizontalLayout_4") self.vocals_rb_1 = QtWidgets.QRadioButton(self.group_vocals) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.vocals_rb_1.sizePolicy().hasHeightForWidth()) - self.vocals_rb_1.setSizePolicy(sizePolicy) - self.vocals_rb_1.setMaximumSize(QtCore.QSize(40, 20)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.vocals_rb_1.setFont(font) - self.vocals_rb_1.setChecked(False) + self.vocals_rb_1.setMinimumSize(QtCore.QSize(40, 20)) + self.vocals_rb_1.setMaximumSize(QtCore.QSize(40, 16777215)) self.vocals_rb_1.setObjectName("vocals_rb_1") - self.horizontalLayout_5.addWidget(self.vocals_rb_1, 0, QtCore.Qt.AlignVCenter) + self.horizontalLayout_4.addWidget(self.vocals_rb_1) self.vocals_rb_2 = QtWidgets.QRadioButton(self.group_vocals) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.vocals_rb_2.sizePolicy().hasHeightForWidth()) - self.vocals_rb_2.setSizePolicy(sizePolicy) - self.vocals_rb_2.setMaximumSize(QtCore.QSize(40, 20)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.vocals_rb_2.setFont(font) - self.vocals_rb_2.setChecked(True) + self.vocals_rb_2.setMaximumSize(QtCore.QSize(40, 16777215)) self.vocals_rb_2.setObjectName("vocals_rb_2") - self.horizontalLayout_5.addWidget(self.vocals_rb_2, 0, QtCore.Qt.AlignVCenter) + self.horizontalLayout_4.addWidget(self.vocals_rb_2) self.vocals_rb_3 = QtWidgets.QRadioButton(self.group_vocals) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.vocals_rb_3.sizePolicy().hasHeightForWidth()) - self.vocals_rb_3.setSizePolicy(sizePolicy) - self.vocals_rb_3.setMaximumSize(QtCore.QSize(40, 20)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.vocals_rb_3.setFont(font) + self.vocals_rb_3.setMaximumSize(QtCore.QSize(40, 16777215)) self.vocals_rb_3.setObjectName("vocals_rb_3") - self.horizontalLayout_5.addWidget(self.vocals_rb_3, 0, QtCore.Qt.AlignVCenter) + self.horizontalLayout_4.addWidget(self.vocals_rb_3) self.vocals_rb_4 = QtWidgets.QRadioButton(self.group_vocals) - self.vocals_rb_4.setMaximumSize(QtCore.QSize(40, 20)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.vocals_rb_4.setFont(font) + self.vocals_rb_4.setMaximumSize(QtCore.QSize(40, 16777215)) self.vocals_rb_4.setObjectName("vocals_rb_4") - self.horizontalLayout_5.addWidget(self.vocals_rb_4, 0, QtCore.Qt.AlignVCenter) - self.verticalLayout_3.addWidget(self.group_vocals) - self.verticalLayout_2.addWidget(self.gb_word_groups, 0, QtCore.Qt.AlignTop) + self.horizontalLayout_4.addWidget(self.vocals_rb_4) + self.verticalLayout_5.addWidget(self.group_vocals) + self.verticalLayout_2.addWidget(self.gb_word_groups) self.gb_group_settings = QtWidgets.QGroupBox(self.scrollAreaWidgetContents) - self.gb_group_settings.setMinimumSize(QtCore.QSize(0, 50)) - font = QtGui.QFont() - font.setBold(True) - font.setWeight(75) - self.gb_group_settings.setFont(font) self.gb_group_settings.setObjectName("gb_group_settings") - self.horizontalLayout = QtWidgets.QHBoxLayout(self.gb_group_settings) - self.horizontalLayout.setObjectName("horizontalLayout") + self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.gb_group_settings) + self.verticalLayout_6.setObjectName("verticalLayout_6") self.gridLayout = QtWidgets.QGridLayout() - self.gridLayout.setHorizontalSpacing(3) self.gridLayout.setVerticalSpacing(1) self.gridLayout.setObjectName("gridLayout") - self.format_group_3_sep_char = QtWidgets.QLineEdit(self.gb_group_settings) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.format_group_3_sep_char.sizePolicy().hasHeightForWidth()) - self.format_group_3_sep_char.setSizePolicy(sizePolicy) - self.format_group_3_sep_char.setMaximumSize(QtCore.QSize(50, 16777215)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.format_group_3_sep_char.setFont(font) - self.format_group_3_sep_char.setObjectName("format_group_3_sep_char") - self.gridLayout.addWidget(self.format_group_3_sep_char, 3, 2, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) - self.format_group_1_start_char = QtWidgets.QLineEdit(self.gb_group_settings) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.format_group_1_start_char.sizePolicy().hasHeightForWidth()) - self.format_group_1_start_char.setSizePolicy(sizePolicy) - self.format_group_1_start_char.setMaximumSize(QtCore.QSize(50, 16777215)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.format_group_1_start_char.setFont(font) - self.format_group_1_start_char.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) - self.format_group_1_start_char.setClearButtonEnabled(False) - self.format_group_1_start_char.setObjectName("format_group_1_start_char") - self.gridLayout.addWidget(self.format_group_1_start_char, 1, 1, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) - self.format_group_1_sep_char = QtWidgets.QLineEdit(self.gb_group_settings) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.format_group_1_sep_char.sizePolicy().hasHeightForWidth()) - self.format_group_1_sep_char.setSizePolicy(sizePolicy) - self.format_group_1_sep_char.setMaximumSize(QtCore.QSize(50, 16777215)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.format_group_1_sep_char.setFont(font) - self.format_group_1_sep_char.setObjectName("format_group_1_sep_char") - self.gridLayout.addWidget(self.format_group_1_sep_char, 1, 2, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) - self.label_18 = QtWidgets.QLabel(self.gb_group_settings) + self.label_2 = QtWidgets.QLabel(self.gb_group_settings) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.label_18.sizePolicy().hasHeightForWidth()) - self.label_18.setSizePolicy(sizePolicy) + sizePolicy.setHeightForWidth(self.label_2.sizePolicy().hasHeightForWidth()) + self.label_2.setSizePolicy(sizePolicy) font = QtGui.QFont() font.setBold(True) font.setWeight(75) - self.label_18.setFont(font) - self.label_18.setObjectName("label_18") - self.gridLayout.addWidget(self.label_18, 3, 0, 1, 1, QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) - self.format_group_4_start_char = QtWidgets.QLineEdit(self.gb_group_settings) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.format_group_4_start_char.sizePolicy().hasHeightForWidth()) - self.format_group_4_start_char.setSizePolicy(sizePolicy) - self.format_group_4_start_char.setMaximumSize(QtCore.QSize(50, 16777215)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.format_group_4_start_char.setFont(font) - self.format_group_4_start_char.setObjectName("format_group_4_start_char") - self.gridLayout.addWidget(self.format_group_4_start_char, 4, 1, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) - self.format_group_1_end_char = QtWidgets.QLineEdit(self.gb_group_settings) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.format_group_1_end_char.sizePolicy().hasHeightForWidth()) - self.format_group_1_end_char.setSizePolicy(sizePolicy) - self.format_group_1_end_char.setMaximumSize(QtCore.QSize(50, 16777215)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.format_group_1_end_char.setFont(font) - self.format_group_1_end_char.setObjectName("format_group_1_end_char") - self.gridLayout.addWidget(self.format_group_1_end_char, 1, 3, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) - self.format_group_3_start_char = QtWidgets.QLineEdit(self.gb_group_settings) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + self.label_2.setFont(font) + self.label_2.setObjectName("label_2") + self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1, QtCore.Qt.AlignLeft) + self.label_3 = QtWidgets.QLabel(self.gb_group_settings) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.format_group_3_start_char.sizePolicy().hasHeightForWidth()) - self.format_group_3_start_char.setSizePolicy(sizePolicy) - self.format_group_3_start_char.setMaximumSize(QtCore.QSize(50, 16777215)) + sizePolicy.setHeightForWidth(self.label_3.sizePolicy().hasHeightForWidth()) + self.label_3.setSizePolicy(sizePolicy) font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.format_group_3_start_char.setFont(font) - self.format_group_3_start_char.setObjectName("format_group_3_start_char") - self.gridLayout.addWidget(self.format_group_3_start_char, 3, 1, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) + font.setBold(True) + font.setWeight(75) + self.label_3.setFont(font) + self.label_3.setObjectName("label_3") + self.gridLayout.addWidget(self.label_3, 3, 0, 1, 1, QtCore.Qt.AlignLeft) + self.format_group_1_start_char = QtWidgets.QLineEdit(self.gb_group_settings) + self.format_group_1_start_char.setMaximumSize(QtCore.QSize(50, 16777215)) + self.format_group_1_start_char.setObjectName("format_group_1_start_char") + self.gridLayout.addWidget(self.format_group_1_start_char, 1, 1, 1, 1, QtCore.Qt.AlignHCenter) self.label = QtWidgets.QLabel(self.gb_group_settings) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) @@ -450,224 +178,158 @@ def setupUi(self, FormatPerformerTagsOptionsPage): font.setWeight(75) self.label.setFont(font) self.label.setObjectName("label") - self.gridLayout.addWidget(self.label, 2, 0, 1, 1, QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) - self.format_group_4_sep_char = QtWidgets.QLineEdit(self.gb_group_settings) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + self.gridLayout.addWidget(self.label, 1, 0, 1, 1, QtCore.Qt.AlignLeft) + self.label_4 = QtWidgets.QLabel(self.gb_group_settings) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.format_group_4_sep_char.sizePolicy().hasHeightForWidth()) - self.format_group_4_sep_char.setSizePolicy(sizePolicy) - self.format_group_4_sep_char.setMaximumSize(QtCore.QSize(50, 16777215)) + sizePolicy.setHeightForWidth(self.label_4.sizePolicy().hasHeightForWidth()) + self.label_4.setSizePolicy(sizePolicy) font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.format_group_4_sep_char.setFont(font) - self.format_group_4_sep_char.setObjectName("format_group_4_sep_char") - self.gridLayout.addWidget(self.format_group_4_sep_char, 4, 2, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) - self.label_19 = QtWidgets.QLabel(self.gb_group_settings) + font.setBold(True) + font.setWeight(75) + self.label_4.setFont(font) + self.label_4.setObjectName("label_4") + self.gridLayout.addWidget(self.label_4, 4, 0, 1, 1, QtCore.Qt.AlignLeft) + self.format_group_1_sep_char = QtWidgets.QLineEdit(self.gb_group_settings) + self.format_group_1_sep_char.setMaximumSize(QtCore.QSize(50, 16777215)) + self.format_group_1_sep_char.setObjectName("format_group_1_sep_char") + self.gridLayout.addWidget(self.format_group_1_sep_char, 1, 2, 1, 1, QtCore.Qt.AlignHCenter) + self.format_group_1_end_char = QtWidgets.QLineEdit(self.gb_group_settings) + self.format_group_1_end_char.setMaximumSize(QtCore.QSize(50, 16777215)) + self.format_group_1_end_char.setObjectName("format_group_1_end_char") + self.gridLayout.addWidget(self.format_group_1_end_char, 1, 3, 1, 1, QtCore.Qt.AlignHCenter) + self.label_5 = QtWidgets.QLabel(self.gb_group_settings) font = QtGui.QFont() font.setBold(True) font.setWeight(75) - self.label_19.setFont(font) - self.label_19.setAlignment(QtCore.Qt.AlignCenter) - self.label_19.setWordWrap(True) - self.label_19.setObjectName("label_19") - self.gridLayout.addWidget(self.label_19, 0, 3, 1, 1) + self.label_5.setFont(font) + self.label_5.setObjectName("label_5") + self.gridLayout.addWidget(self.label_5, 0, 1, 1, 1, QtCore.Qt.AlignHCenter) + self.label_6 = QtWidgets.QLabel(self.gb_group_settings) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.label_6.setFont(font) + self.label_6.setObjectName("label_6") + self.gridLayout.addWidget(self.label_6, 0, 2, 1, 1, QtCore.Qt.AlignHCenter) self.label_7 = QtWidgets.QLabel(self.gb_group_settings) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.label_7.sizePolicy().hasHeightForWidth()) - self.label_7.setSizePolicy(sizePolicy) font = QtGui.QFont() font.setBold(True) font.setWeight(75) self.label_7.setFont(font) self.label_7.setObjectName("label_7") - self.gridLayout.addWidget(self.label_7, 4, 0, 1, 1, QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self.gridLayout.addWidget(self.label_7, 0, 3, 1, 1, QtCore.Qt.AlignHCenter) + self.format_group_2_start_char = QtWidgets.QLineEdit(self.gb_group_settings) + self.format_group_2_start_char.setMaximumSize(QtCore.QSize(50, 16777215)) + self.format_group_2_start_char.setObjectName("format_group_2_start_char") + self.gridLayout.addWidget(self.format_group_2_start_char, 2, 1, 1, 1, QtCore.Qt.AlignHCenter) + self.format_group_3_start_char = QtWidgets.QLineEdit(self.gb_group_settings) + self.format_group_3_start_char.setMaximumSize(QtCore.QSize(50, 16777215)) + self.format_group_3_start_char.setObjectName("format_group_3_start_char") + self.gridLayout.addWidget(self.format_group_3_start_char, 3, 1, 1, 1, QtCore.Qt.AlignHCenter) + self.format_group_4_start_char = QtWidgets.QLineEdit(self.gb_group_settings) + self.format_group_4_start_char.setMaximumSize(QtCore.QSize(50, 16777215)) + self.format_group_4_start_char.setObjectName("format_group_4_start_char") + self.gridLayout.addWidget(self.format_group_4_start_char, 4, 1, 1, 1, QtCore.Qt.AlignHCenter) self.format_group_2_sep_char = QtWidgets.QLineEdit(self.gb_group_settings) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.format_group_2_sep_char.sizePolicy().hasHeightForWidth()) - self.format_group_2_sep_char.setSizePolicy(sizePolicy) self.format_group_2_sep_char.setMaximumSize(QtCore.QSize(50, 16777215)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.format_group_2_sep_char.setFont(font) self.format_group_2_sep_char.setObjectName("format_group_2_sep_char") - self.gridLayout.addWidget(self.format_group_2_sep_char, 2, 2, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) - self.label_20 = QtWidgets.QLabel(self.gb_group_settings) - font = QtGui.QFont() - font.setBold(True) - font.setWeight(75) - self.label_20.setFont(font) - self.label_20.setAlignment(QtCore.Qt.AlignCenter) - self.label_20.setWordWrap(True) - self.label_20.setObjectName("label_20") - self.gridLayout.addWidget(self.label_20, 0, 2, 1, 1) - self.format_group_4_end_char = QtWidgets.QLineEdit(self.gb_group_settings) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.format_group_4_end_char.sizePolicy().hasHeightForWidth()) - self.format_group_4_end_char.setSizePolicy(sizePolicy) - self.format_group_4_end_char.setMaximumSize(QtCore.QSize(50, 16777215)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.format_group_4_end_char.setFont(font) - self.format_group_4_end_char.setObjectName("format_group_4_end_char") - self.gridLayout.addWidget(self.format_group_4_end_char, 4, 3, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) - self.label_15 = QtWidgets.QLabel(self.gb_group_settings) - font = QtGui.QFont() - font.setBold(True) - font.setUnderline(False) - font.setWeight(75) - self.label_15.setFont(font) - self.label_15.setAlignment(QtCore.Qt.AlignCenter) - self.label_15.setWordWrap(True) - self.label_15.setObjectName("label_15") - self.gridLayout.addWidget(self.label_15, 0, 1, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) + self.gridLayout.addWidget(self.format_group_2_sep_char, 2, 2, 1, 1, QtCore.Qt.AlignHCenter) + self.format_group_3_sep_char = QtWidgets.QLineEdit(self.gb_group_settings) + self.format_group_3_sep_char.setMaximumSize(QtCore.QSize(50, 16777215)) + self.format_group_3_sep_char.setObjectName("format_group_3_sep_char") + self.gridLayout.addWidget(self.format_group_3_sep_char, 3, 2, 1, 1, QtCore.Qt.AlignHCenter) + self.format_group_4_sep_char = QtWidgets.QLineEdit(self.gb_group_settings) + self.format_group_4_sep_char.setMaximumSize(QtCore.QSize(50, 16777215)) + self.format_group_4_sep_char.setObjectName("format_group_4_sep_char") + self.gridLayout.addWidget(self.format_group_4_sep_char, 4, 2, 1, 1, QtCore.Qt.AlignHCenter) self.format_group_2_end_char = QtWidgets.QLineEdit(self.gb_group_settings) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.format_group_2_end_char.sizePolicy().hasHeightForWidth()) - self.format_group_2_end_char.setSizePolicy(sizePolicy) self.format_group_2_end_char.setMaximumSize(QtCore.QSize(50, 16777215)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.format_group_2_end_char.setFont(font) - self.format_group_2_end_char.setText("") self.format_group_2_end_char.setObjectName("format_group_2_end_char") - self.gridLayout.addWidget(self.format_group_2_end_char, 2, 3, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) - self.label_5 = QtWidgets.QLabel(self.gb_group_settings) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.label_5.sizePolicy().hasHeightForWidth()) - self.label_5.setSizePolicy(sizePolicy) - font = QtGui.QFont() - font.setBold(True) - font.setWeight(75) - self.label_5.setFont(font) - self.label_5.setObjectName("label_5") - self.gridLayout.addWidget(self.label_5, 1, 0, 1, 1, QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self.gridLayout.addWidget(self.format_group_2_end_char, 2, 3, 1, 1, QtCore.Qt.AlignHCenter) self.format_group_3_end_char = QtWidgets.QLineEdit(self.gb_group_settings) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.format_group_3_end_char.sizePolicy().hasHeightForWidth()) - self.format_group_3_end_char.setSizePolicy(sizePolicy) self.format_group_3_end_char.setMaximumSize(QtCore.QSize(50, 16777215)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.format_group_3_end_char.setFont(font) self.format_group_3_end_char.setObjectName("format_group_3_end_char") - self.gridLayout.addWidget(self.format_group_3_end_char, 3, 3, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) - self.format_group_2_start_char = QtWidgets.QLineEdit(self.gb_group_settings) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.format_group_2_start_char.sizePolicy().hasHeightForWidth()) - self.format_group_2_start_char.setSizePolicy(sizePolicy) - self.format_group_2_start_char.setMaximumSize(QtCore.QSize(50, 16777215)) - font = QtGui.QFont() - font.setBold(False) - font.setWeight(50) - self.format_group_2_start_char.setFont(font) - self.format_group_2_start_char.setObjectName("format_group_2_start_char") - self.gridLayout.addWidget(self.format_group_2_start_char, 2, 1, 1, 1, QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter) - self.horizontalLayout.addLayout(self.gridLayout) + self.gridLayout.addWidget(self.format_group_3_end_char, 3, 3, 1, 1, QtCore.Qt.AlignHCenter) + self.format_group_4_end_char = QtWidgets.QLineEdit(self.gb_group_settings) + self.format_group_4_end_char.setMaximumSize(QtCore.QSize(50, 16777215)) + self.format_group_4_end_char.setObjectName("format_group_4_end_char") + self.gridLayout.addWidget(self.format_group_4_end_char, 4, 3, 1, 1, QtCore.Qt.AlignHCenter) + self.verticalLayout_6.addLayout(self.gridLayout) self.verticalLayout_2.addWidget(self.gb_group_settings) - spacerItem = QtWidgets.QSpacerItem(20, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gb_examples = QtWidgets.QGroupBox(self.scrollAreaWidgetContents) + self.gb_examples.setObjectName("gb_examples") + self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.gb_examples) + self.verticalLayout_4.setObjectName("verticalLayout_4") + self.example_instruments = QtWidgets.QLabel(self.gb_examples) + self.example_instruments.setText("") + self.example_instruments.setObjectName("example_instruments") + self.verticalLayout_4.addWidget(self.example_instruments) + self.example_vocals = QtWidgets.QLabel(self.gb_examples) + self.example_vocals.setText("") + self.example_vocals.setObjectName("example_vocals") + self.verticalLayout_4.addWidget(self.example_vocals) + self.verticalLayout_2.addWidget(self.gb_examples) + spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) self.verticalLayout_2.addItem(spacerItem) self.scrollArea.setWidget(self.scrollAreaWidgetContents) - self.vboxlayout.addWidget(self.scrollArea, 0, QtCore.Qt.AlignTop) + self.verticalLayout.addWidget(self.scrollArea) self.retranslateUi(FormatPerformerTagsOptionsPage) QtCore.QMetaObject.connectSlotsByName(FormatPerformerTagsOptionsPage) - FormatPerformerTagsOptionsPage.setTabOrder(self.additional_rb_1, self.additional_rb_2) - FormatPerformerTagsOptionsPage.setTabOrder(self.additional_rb_2, self.additional_rb_3) - FormatPerformerTagsOptionsPage.setTabOrder(self.additional_rb_3, self.additional_rb_4) - FormatPerformerTagsOptionsPage.setTabOrder(self.additional_rb_4, self.guest_rb_1) - FormatPerformerTagsOptionsPage.setTabOrder(self.guest_rb_1, self.guest_rb_2) - FormatPerformerTagsOptionsPage.setTabOrder(self.guest_rb_2, self.guest_rb_3) - FormatPerformerTagsOptionsPage.setTabOrder(self.guest_rb_3, self.guest_rb_4) - FormatPerformerTagsOptionsPage.setTabOrder(self.guest_rb_4, self.solo_rb_1) - FormatPerformerTagsOptionsPage.setTabOrder(self.solo_rb_1, self.solo_rb_2) - FormatPerformerTagsOptionsPage.setTabOrder(self.solo_rb_2, self.solo_rb_3) - FormatPerformerTagsOptionsPage.setTabOrder(self.solo_rb_3, self.solo_rb_4) - FormatPerformerTagsOptionsPage.setTabOrder(self.solo_rb_4, self.vocals_rb_1) - FormatPerformerTagsOptionsPage.setTabOrder(self.vocals_rb_1, self.vocals_rb_2) - FormatPerformerTagsOptionsPage.setTabOrder(self.vocals_rb_2, self.vocals_rb_3) - FormatPerformerTagsOptionsPage.setTabOrder(self.vocals_rb_3, self.vocals_rb_4) - FormatPerformerTagsOptionsPage.setTabOrder(self.vocals_rb_4, self.format_group_1_start_char) - FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_1_start_char, self.format_group_1_sep_char) - FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_1_sep_char, self.format_group_1_end_char) - FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_1_end_char, self.format_group_2_start_char) - FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_2_start_char, self.format_group_2_sep_char) - FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_2_sep_char, self.format_group_2_end_char) - FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_2_end_char, self.format_group_3_start_char) - FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_3_start_char, self.format_group_3_sep_char) - FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_3_sep_char, self.format_group_3_end_char) - FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_3_end_char, self.format_group_4_start_char) - FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_4_start_char, self.format_group_4_sep_char) - FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_4_sep_char, self.format_group_4_end_char) - FormatPerformerTagsOptionsPage.setTabOrder(self.format_group_4_end_char, self.scrollArea) def retranslateUi(self, FormatPerformerTagsOptionsPage): _translate = QtCore.QCoreApplication.translate - self.gb_description.setTitle(_("Format Performer Tags")) - self.format_description.setText(_("

    These settings will determine the format for any Performer tags prepared. The format is divided into six parts: the performer; the instrument or "vocals"; and four user selectable sections for the extra information. This is set out as:

    [Section 1]Instrument/Vocals[Section 2][Section 3]: Performer[Section 4]

    You can select the section in which each of the extra information words appears.

    For each of the sections you can select the starting character(s), the character(s) separating entries, and the ending character(s). Note that leading or trailing spaces must be included in the settings and will not be automatically added. If no separator characters are entered, the items within a section will be automatically separated by a single space.

    Please visit the repository on GitHub for additional information.

    ")) - self.gb_word_groups.setTitle(_("Keyword Section Assignments")) - self.group_additional.setTitle(_("Keyword: additional")) - self.additional_rb_1.setText(_("1")) - self.additional_rb_2.setText(_("2")) - self.additional_rb_3.setText(_("3")) - self.additional_rb_4.setText(_("4")) - self.group_guest.setTitle(_("Keyword: guest")) - self.guest_rb_1.setText(_("1")) - self.guest_rb_2.setText(_("2")) - self.guest_rb_3.setText(_("3")) - self.guest_rb_4.setText(_("4")) - self.group_solo.setTitle(_("Keyword: solo")) - self.solo_rb_1.setText(_("1")) - self.solo_rb_2.setText(_("2")) - self.solo_rb_3.setText(_("3")) - self.solo_rb_4.setText(_("4")) - self.group_vocals.setTitle(_("All vocal type keywords")) - self.vocals_rb_1.setText(_("1")) - self.vocals_rb_2.setText(_("2")) - self.vocals_rb_3.setText(_("3")) - self.vocals_rb_4.setText(_("4")) - self.gb_group_settings.setTitle(_("Section Display Settings")) - self.format_group_3_sep_char.setPlaceholderText(_("(blank)")) - self.format_group_1_start_char.setPlaceholderText(_("(blank)")) - self.format_group_1_sep_char.setPlaceholderText(_("(blank)")) - self.label_18.setText(_("Section 3 ")) - self.format_group_4_start_char.setText(_(" (")) - self.format_group_4_start_char.setPlaceholderText(_("(blank)")) - self.format_group_1_end_char.setPlaceholderText(_("(blank)")) - self.format_group_3_start_char.setText(_(" (")) - self.format_group_3_start_char.setPlaceholderText(_("(blank)")) - self.label.setText(_("Section 2 ")) - self.format_group_4_sep_char.setPlaceholderText(_("(blank)")) - self.label_19.setText(_("End Char(s)")) - self.label_7.setText(_("Section 4 ")) - self.format_group_2_sep_char.setPlaceholderText(_("(blank)")) - self.label_20.setText(_("Sep Char(s)")) - self.format_group_4_end_char.setText(_(")")) - self.format_group_4_end_char.setPlaceholderText(_("(blank)")) - self.label_15.setText(_("Start Char(s)")) - self.format_group_2_end_char.setPlaceholderText(_("(blank)")) - self.label_5.setText(_("Section 1 ")) - self.format_group_3_end_char.setText(_(")")) - self.format_group_3_end_char.setPlaceholderText(_("(blank)")) - self.format_group_2_start_char.setText(_(", ")) - self.format_group_2_start_char.setPlaceholderText(_("(blank)")) + FormatPerformerTagsOptionsPage.setWindowTitle(_translate("FormatPerformerTagsOptionsPage", "Form")) + self.gb_description.setTitle(_translate("FormatPerformerTagsOptionsPage", "Format Performer Tags")) + self.format_description.setText(_translate("FormatPerformerTagsOptionsPage", "

    These settings will determine the format for any Performer tags prepared. The format is divided into six parts: the performer; the instrument or "vocals"; and four user selectable sections for the extra information. This is set out as:

    [Section 1]Instrument/Vocals[Section 2][Section 3]: Performer[Section 4]

    You can select the section in which each of the extra information words appears.

    For each of the sections you can select the starting character(s), the character(s) separating entries, and the ending character(s). Note that leading or trailing spaces must be included in the settings and will not be automatically added. If no separator characters are entered, the items within a section will be automatically separated by a single space.

    Please visit the repository on GitHub for additional information.

    ")) + self.gb_word_groups.setTitle(_translate("FormatPerformerTagsOptionsPage", "Keyword Sections Assignment")) + self.group_additonal.setTitle(_translate("FormatPerformerTagsOptionsPage", "Keyword: additional")) + self.additional_rb_1.setText(_translate("FormatPerformerTagsOptionsPage", "1")) + self.additional_rb_2.setText(_translate("FormatPerformerTagsOptionsPage", "2")) + self.additional_rb_3.setText(_translate("FormatPerformerTagsOptionsPage", "3")) + self.additional_rb_4.setText(_translate("FormatPerformerTagsOptionsPage", "4")) + self.group_guest.setTitle(_translate("FormatPerformerTagsOptionsPage", "Keyword: guest")) + self.guest_rb_1.setText(_translate("FormatPerformerTagsOptionsPage", "1")) + self.guest_rb_2.setText(_translate("FormatPerformerTagsOptionsPage", "2")) + self.guest_rb_3.setText(_translate("FormatPerformerTagsOptionsPage", "3")) + self.guest_rb_4.setText(_translate("FormatPerformerTagsOptionsPage", "4")) + self.group_solo.setTitle(_translate("FormatPerformerTagsOptionsPage", "Keyword: solo")) + self.solo_rb_1.setText(_translate("FormatPerformerTagsOptionsPage", "1")) + self.solo_rb_2.setText(_translate("FormatPerformerTagsOptionsPage", "2")) + self.solo_rb_3.setText(_translate("FormatPerformerTagsOptionsPage", "3")) + self.solo_rb_4.setText(_translate("FormatPerformerTagsOptionsPage", "4")) + self.group_vocals.setTitle(_translate("FormatPerformerTagsOptionsPage", "All vocal type keywords")) + self.vocals_rb_1.setText(_translate("FormatPerformerTagsOptionsPage", "1")) + self.vocals_rb_2.setText(_translate("FormatPerformerTagsOptionsPage", "2")) + self.vocals_rb_3.setText(_translate("FormatPerformerTagsOptionsPage", "3")) + self.vocals_rb_4.setText(_translate("FormatPerformerTagsOptionsPage", "4")) + self.gb_group_settings.setTitle(_translate("FormatPerformerTagsOptionsPage", "Section Display Settings")) + self.label_2.setText(_translate("FormatPerformerTagsOptionsPage", "Section 2")) + self.label_3.setText(_translate("FormatPerformerTagsOptionsPage", "Section 3")) + self.format_group_1_start_char.setPlaceholderText(_translate("FormatPerformerTagsOptionsPage", "(blank)")) + self.label.setText(_translate("FormatPerformerTagsOptionsPage", "Section 1")) + self.label_4.setText(_translate("FormatPerformerTagsOptionsPage", "Section 4")) + self.format_group_1_sep_char.setPlaceholderText(_translate("FormatPerformerTagsOptionsPage", "(blank)")) + self.format_group_1_end_char.setText(_translate("FormatPerformerTagsOptionsPage", " ")) + self.format_group_1_end_char.setPlaceholderText(_translate("FormatPerformerTagsOptionsPage", "(blank)")) + self.label_5.setText(_translate("FormatPerformerTagsOptionsPage", "Start Char(s)")) + self.label_6.setText(_translate("FormatPerformerTagsOptionsPage", "Sep Char(s)")) + self.label_7.setText(_translate("FormatPerformerTagsOptionsPage", "End Char(s)")) + self.format_group_2_start_char.setText(_translate("FormatPerformerTagsOptionsPage", ", ")) + self.format_group_2_start_char.setPlaceholderText(_translate("FormatPerformerTagsOptionsPage", "(blank)")) + self.format_group_3_start_char.setText(_translate("FormatPerformerTagsOptionsPage", " (")) + self.format_group_3_start_char.setPlaceholderText(_translate("FormatPerformerTagsOptionsPage", "(blank)")) + self.format_group_4_start_char.setText(_translate("FormatPerformerTagsOptionsPage", " (")) + self.format_group_4_start_char.setPlaceholderText(_translate("FormatPerformerTagsOptionsPage", "(blank)")) + self.format_group_2_sep_char.setPlaceholderText(_translate("FormatPerformerTagsOptionsPage", "(blank)")) + self.format_group_3_sep_char.setPlaceholderText(_translate("FormatPerformerTagsOptionsPage", "(blank)")) + self.format_group_4_sep_char.setPlaceholderText(_translate("FormatPerformerTagsOptionsPage", "(blank)")) + self.format_group_2_end_char.setPlaceholderText(_translate("FormatPerformerTagsOptionsPage", "(blank)")) + self.format_group_3_end_char.setText(_translate("FormatPerformerTagsOptionsPage", ")")) + self.format_group_3_end_char.setPlaceholderText(_translate("FormatPerformerTagsOptionsPage", "(blank)")) + self.format_group_4_end_char.setText(_translate("FormatPerformerTagsOptionsPage", ")")) + self.format_group_4_end_char.setPlaceholderText(_translate("FormatPerformerTagsOptionsPage", "(blank)")) + self.gb_examples.setTitle(_translate("FormatPerformerTagsOptionsPage", "Examples")) From 6e670eb4ee39dc1fa2d569188a1bb77d69813d32 Mon Sep 17 00:00:00 2001 From: Bob Swift Date: Mon, 17 Dec 2018 16:40:07 -0700 Subject: [PATCH 074/123] Add contributor list and revision history --- plugins/format_performer_tags/docs/HISTORY.md | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 plugins/format_performer_tags/docs/HISTORY.md diff --git a/plugins/format_performer_tags/docs/HISTORY.md b/plugins/format_performer_tags/docs/HISTORY.md new file mode 100644 index 00000000..60ea8cbb --- /dev/null +++ b/plugins/format_performer_tags/docs/HISTORY.md @@ -0,0 +1,45 @@ +# Format Performer Tags + +## Contributors + +The following people have contributed to the development of this plugin. + +* Bob Swift ([rdswift](https://github.com/rdswift/)) +* Philipp Wolfer ([phw](https://github.com/phw/)) + +--- + +## Revision History + +The following identifies the development history of the plugin, in reverse chronological order. Each version lists the changes made for that version, along with the author of each change. + +### Version 0.6 + +* Update the user interface. Add live examples when settings are changed. \[phw\] +* Update plugin metadata. \[phw\] +* Add `HISTORY.md` file containing the contributors list and revision history. \[rdswift\] + +### Version 0.5 + +* Reformat long lines. \[rdswift\] +* Add TODO note about language translation. \[rdswift\] + +### Version 0.4 + +* Remove code to strip extra whitespace from key and value strings. \[rdswift\] + +### Version 0.3 + +* Update to use four user-defined sections. \[rdswift\] +* Add user guide. \[rdswift\] + +### Version 0.2 + +* Fix bug that caused some performer records to be missed in the processing. \[rdswift\] +* Add vocals performer processing. \[rdswift\] + +### Version 0.1 + +* Initial testing release. \[rdswift\] + +--- From 6c85b44d76c41384535da6ebce1453c23fc51e05 Mon Sep 17 00:00:00 2001 From: Paul Brackin Date: Tue, 18 Dec 2018 11:01:33 -0800 Subject: [PATCH 075/123] Adding UI for wikidata-genre plugin. wikidata.py -> __init__.py, adding QT UI files. Bump version to 1.3. --- plugins/wikidata/{wikidata.py => __init__.py} | 150 ++++++++-- plugins/wikidata/options_wikidata.ui | 281 ++++++++++++++++++ plugins/wikidata/ui_options_wikidata.py | 146 +++++++++ 3 files changed, 558 insertions(+), 19 deletions(-) rename plugins/wikidata/{wikidata.py => __init__.py} (61%) create mode 100644 plugins/wikidata/options_wikidata.ui create mode 100644 plugins/wikidata/ui_options_wikidata.py diff --git a/plugins/wikidata/wikidata.py b/plugins/wikidata/__init__.py similarity index 61% rename from plugins/wikidata/wikidata.py rename to plugins/wikidata/__init__.py index 932a1fbe..1d2047e2 100644 --- a/plugins/wikidata/wikidata.py +++ b/plugins/wikidata/__init__.py @@ -8,18 +8,52 @@ PLUGIN_NAME = 'wikidata-genre' PLUGIN_AUTHOR = 'Daniel Sobey, Sambhav Kothari' PLUGIN_DESCRIPTION = 'query wikidata to get genre tags' -PLUGIN_VERSION = '1.2' +PLUGIN_VERSION = '1.3' PLUGIN_API_VERSIONS = ["2.0"] PLUGIN_LICENSE = 'WTFPL' PLUGIN_LICENSE_URL = 'http://www.wtfpl.net/' +import re from functools import partial from picard import config, log from picard.metadata import register_track_metadata_processor +from picard.plugins.wikidata.ui_options_wikidata import Ui_WikidataOptionsPage +from picard.ui.options import register_options_page, OptionsPage + + +def parse_ignored_tags(ignore_tags_setting): + ignore_tags = [] + for tag in ignore_tags_setting.lower().split(','): + tag = tag.strip() + if tag.startswith('/') and tag.endswith('/'): + try: + tag = re.compile(tag[1:-1]) + except re.error: + log.error( + 'Error parsing ignored tag "%s"', tag, exc_info=True) + ignore_tags.append(tag) + return ignore_tags + + +def matches_ignored(ignore_tags, tag): + tag = tag.lower().strip() + for pattern in ignore_tags: + if hasattr(pattern, 'match'): + match = pattern.match(tag) + else: + match = pattern == tag + if match: + return True + return False class Wikidata: + RELEASE_GROUP = 1 + ARTIST = 2 + WORK = 3 + + def __init__(self): # Key: mbid, value: List of metadata entries to be updated when we have parsed everything self.requests = {} @@ -39,6 +73,14 @@ def __init__(self): self.ws = None self.log = None + # settings from options, options + self.use_release_group_genres = False + self.use_artist_genres = False + self.use_artist_only_if_no_release = False + self.use_work_genres = True + self.ignore_these_genres = '' + self.genre_delimiter = '' + # not used def process_release(self, album, metadata, release): self.ws = album.tagger.webservice @@ -69,7 +111,7 @@ def process_request(self, metadata, album, item_id, type): new_genre = set(metadata.getall("genre")) new_genre.update(genre_list) #sort the new genre list so that they don't appear as new entries (not a change) next time - metadata["genre"] = sorted(new_genre) + metadata["genre"] = self.genre_delimiter.join(sorted(new_genre)) return else: # pending requests are handled by adding the metadata object to a @@ -100,27 +142,27 @@ def musicbrainz_release_lookup(self, item_id, metadata, response, reply, error): log.error('WIKIDATA: Error retrieving release group info') else: if 'metadata' in response.children: - if 'release_group' in response.metadata[0].children: + if 'release_group' in response.metadata[0].children and self.use_release_group_genres: if 'relation_list' in response.metadata[0].release_group[0].children: for relation in response.metadata[0].release_group[0].relation_list[0].relation: if relation.type == 'wikidata' and 'target' in relation.children: found = True wikidata_url = relation.target[0].text - self.process_wikidata(wikidata_url, item_id) - if 'artist' in response.metadata[0].children: + self.process_wikidata(Wikidata.RELEASE_GROUP, wikidata_url, item_id) + if 'artist' in response.metadata[0].children and self.use_artist_genres: if 'relation_list' in response.metadata[0].artist[0].children: for relation in response.metadata[0].artist[0].relation_list[0].relation: if relation.type == 'wikidata' and 'target' in relation.children: found = True wikidata_url = relation.target[0].text - self.process_wikidata(wikidata_url, item_id) - if 'work' in response.metadata[0].children: + self.process_wikidata(Wikidata.ARTIST, wikidata_url, item_id) + if 'work' in response.metadata[0].children and self.use_work_genres: if 'relation_list' in response.metadata[0].work[0].children: for relation in response.metadata[0].work[0].relation_list[0].relation: if relation.type == 'wikidata' and 'target' in relation.children: found = True wikidata_url = relation.target[0].text - self.process_wikidata(wikidata_url, item_id) + self.process_wikidata(Wikidata.WORK, wikidata_url, item_id) if not found: log.debug('WIKIDATA: No wikidata url found for item_id: %s ', item_id) @@ -134,17 +176,17 @@ def musicbrainz_release_lookup(self, item_id, metadata, response, reply, error): self.requests.clear() log.info('WIKIDATA: Finished.') - def process_wikidata(self, wikidata_url, item_id): + def process_wikidata(self, genre_source_type, wikidata_url, item_id): album = self.itemAlbums[item_id] album._requests += 1 item = wikidata_url.split('/')[4] path = "/wiki/Special:EntityData/" + item + ".rdf" log.debug('WIKIDATA: Fetching from wikidata.org%s' % path) self.ws.get('www.wikidata.org', 443, path, - partial(self.parse_wikidata_response, item, item_id), + partial(self.parse_wikidata_response, item, item_id, genre_source_type), parse_response_type="xml", priority=False, important=False) - def parse_wikidata_response(self, item, item_id, response, reply, error): + def parse_wikidata_response(self, item, item_id, genre_source_type, response, reply, error): genre_entries = [] genre_list = [] if error: @@ -172,8 +214,11 @@ def parse_wikidata_response(self, item, item_id, response, reply, error): for node2 in list1: if node2.attribs.get('lang') == 'en': genre = node2.text.title() - genre_list.append(genre) - log.debug('Our genre is: %s' % genre) + if not matches_ignored(self.ignore_these_genres, genre): + genre_list.append(genre) + log.debug('New genre has been found and ALLOWED: %s' % genre) + else: + log.debug('New genre has been found, but IGNORED: %s' % genre) if len(genre_list) > 0: log.debug('WIKIDATA: item_id: %s' % item_id) @@ -181,30 +226,52 @@ def parse_wikidata_response(self, item, item_id, response, reply, error): log.debug('WIKIDATA: Final list of genre: %s' % genre_list) log.info('WIKIDATA: Total items to update: %d ' % len(self.requests[item_id])) for metadata in self.requests[item_id]: + if genre_source_type == Wikidata.RELEASE_GROUP: + metadata['release_group_genre_sourced'] = True + elif genre_source_type == Wikidata.ARTIST: + if self.use_artist_only_if_no_release and metadata['release_group_genre_sourced']: + if item_id not in self.cache: + self.cache[item_id] = [] + log.debug('wikidata: NOT setting Artist-sourced genre: %s ' % genre_list) + continue new_genre = set(metadata.getall("genre")) new_genre.update(genre_list) # sort the new genre list so that they don't appear as new entries (not a change) next time - metadata["genre"] = sorted(new_genre) + metadata["genre"] = self.genre_delimiter.join(sorted(genre_list)) self.cache[item_id] = genre_list - log.debug('WIKIDATA: Setting genre: %s ' % genre_list) + log.debug('wikidata: setting genre: %s ' % genre_list) else: - log.debug('WIKIDATA: Genre not found in wikidata') + log.debug('wikidata: genre not found in wikidata') - log.debug('WIKIDATA: Seeing if we can finalize tags...') + log.debug('wikidata: seeing if we can finalize tags...') album = self.itemAlbums[item_id] album._requests -= 1 if not album._requests: self.itemAlbums = {k: v for k, v in self.itemAlbums.items() if v != album} album._finalize_loading(None) - log.info('WIKIDATA: Total remaining requests: %s' % album._requests) + log.info('wikidata: total remaining requests: %s' % album._requests) if not self.itemAlbums: self.requests.clear() - log.info('WIKIDATA: Finished.') + log.info('wikidata: finished.') + def process_track(self, album, metadata, trackXmlNode, releaseXmlNode): self.mb_host = config.setting["server_host"] self.mb_port = config.setting["server_port"] + self.use_release_group_genres = config.setting["wikidata_use_release_group_genres"] + self.use_work_genres = config.setting["wikidata_use_work_genres"] + # Some changed settings could invalidate the cache, so clear it to be safe + if self.use_artist_genres != config.setting["wikidata_use_artist_genres"]: + self.use_artist_genres = config.setting["wikidata_use_artist_genres"] + self.cache.clear() + if self.use_artist_only_if_no_release != config.setting["wikidata_use_artist_only_if_no_release"]: + self.use_artist_only_if_no_release = config.setting["wikidata_use_artist_only_if_no_release"] + self.cache.clear() + if self.ignore_these_genres != parse_ignored_tags(config.setting["wikidata_ignore_these_genres"]): + self.ignore_these_genres = parse_ignored_tags(config.setting["wikidata_ignore_these_genres"]) + self.cache.clear() + self.genre_delimiter = config.setting["wikidata_genre_delimiter"] self.ws = album.tagger.webservice self.log = album.log @@ -227,5 +294,50 @@ def process_track(self, album, metadata, trackXmlNode, releaseXmlNode): self.process_request(metadata, album, workid, type='work') +class WikidataOptionsPage(OptionsPage): + NAME = "wikidata" + TITLE = "wikidata-genre" + PARENT = "plugins" + + options = [ + config.BoolOption("setting", "wikidata_use_release_group_genres", True), + config.BoolOption("setting", "wikidata_use_artist_genres", True), + config.BoolOption("setting", "wikidata_use_artist_only_if_no_release", True), + config.BoolOption("setting", "wikidata_use_work_genres", True), + config.TextOption("setting", "wikidata_ignore_these_genres", "seen live, favorites, /\\d+ of \\d+ stars/"), + config.TextOption("setting", "wikidata_genre_delimiter", "; "), + ] + + def __init__(self, parent=None): + super(WikidataOptionsPage, self).__init__(parent) + self.ui = Ui_WikidataOptionsPage() + self.ui.setupUi(self) + + def info(self): + pass + + def load(self): + setting = config.setting + self.ui.use_release_group_genres.setChecked(setting["wikidata_use_release_group_genres"]) + self.ui.use_artist_genres.setChecked(setting["wikidata_use_artist_genres"]) + self.ui.use_artist_only_if_no_release.setChecked(setting["wikidata_use_artist_only_if_no_release"]) + self.ui.use_work_genres.setChecked(setting["wikidata_use_work_genres"]) + self.ui.ignore_these_genres.setText(setting["wikidata_ignore_these_genres"]) + self.ui.genre_delimiter.setEditText(setting["wikidata_genre_delimiter"]) + + def save(self): + global _cache + setting = config.setting + if setting["wikidata_ignore_these_genres"] != str(self.ui.ignore_these_genres.text()): + _cache = {} + setting["wikidata_use_release_group_genres"] = self.ui.use_release_group_genres.isChecked() + setting["wikidata_use_artist_genres"] = self.ui.use_artist_genres.isChecked() + setting["wikidata_use_artist_only_if_no_release"] = self.ui.use_artist_only_if_no_release.isChecked() + setting["wikidata_use_work_genres"] = self.ui.use_work_genres.isChecked() + setting["wikidata_ignore_these_genres"] = str(self.ui.ignore_these_genres.text()) + setting["wikidata_genre_delimiter"] = str(self.ui.genre_delimiter.currentText()) + + wikidata = Wikidata() register_track_metadata_processor(wikidata.process_track) +register_options_page(WikidataOptionsPage) diff --git a/plugins/wikidata/options_wikidata.ui b/plugins/wikidata/options_wikidata.ui new file mode 100644 index 00000000..ade52bed --- /dev/null +++ b/plugins/wikidata/options_wikidata.ui @@ -0,0 +1,281 @@ + + + WikidataOptionsPage + + + + 0 + 0 + 518 + 289 + + + + + QLayout::SetDefaultConstraint + + + 9 + + + 9 + + + 9 + + + 9 + + + + + true + + + + 0 + 0 + + + + + + + Use Track genres + + + true + + + + + + + + + + 0 + 0 + + + + Use Release genres + + + false + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + false + + + + 1 + 0 + + + + Use Release genres only if no track tags exist + + + + + + + + + + + + + Use Artist genres + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + false + + + + 0 + 0 + + + + Use Artist genres only if no track tags and no release tags exist + + + false + + + + + + + + + + + + + + 0 + 0 + + + + Genre Delimiter (in case of multiple genres): + + + + + + + + 0 + 0 + + + + true + + + + / + + + + + , + + + + + ; + + + + + + + + + + + + + Qt::Horizontal + + + + 100 + 20 + + + + + + + + + + + + Ignore these genres: + + + + + + + + 0 + 0 + + + + + + + + + + Qt::Vertical + + + QSizePolicy::MinimumExpanding + + + + 263 + 34 + + + + + + use_track_genres + use_release_genres + use_artist_genres + use_release_only_if_no_track + ignore_tags_4 + genre_delimiter_label + genre_delimiter + ignore_these_genres + ignore_genres_label + + spacer_label + horizontalSpacer_2 + horizontalSpacer_3 + use_artist_only_if_no_track_n_no_release + + + + + + use_track_genres + + + + diff --git a/plugins/wikidata/ui_options_wikidata.py b/plugins/wikidata/ui_options_wikidata.py new file mode 100644 index 00000000..64d97f1c --- /dev/null +++ b/plugins/wikidata/ui_options_wikidata.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'options_wikidata.ui' +# +# Created by: PyQt5 UI code generator 5.11.3 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_WikidataOptionsPage(object): + def setupUi(self, WikidataOptionsPage): + WikidataOptionsPage.setObjectName("WikidataOptionsPage") + WikidataOptionsPage.resize(513, 310) + self.verticalLayout_5 = QtWidgets.QVBoxLayout(WikidataOptionsPage) + self.verticalLayout_5.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) + self.verticalLayout_5.setContentsMargins(9, 9, 9, 9) + self.verticalLayout_5.setObjectName("verticalLayout_5") + self.wikidata_vert_layout = QtWidgets.QWidget(WikidataOptionsPage) + self.wikidata_vert_layout.setEnabled(True) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.MinimumExpanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.wikidata_vert_layout.sizePolicy().hasHeightForWidth()) + self.wikidata_vert_layout.setSizePolicy(sizePolicy) + self.wikidata_vert_layout.setObjectName("wikidata_vert_layout") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.wikidata_vert_layout) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.use_release_group_genres = QtWidgets.QCheckBox(self.wikidata_vert_layout) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.use_release_group_genres.sizePolicy().hasHeightForWidth()) + self.use_release_group_genres.setSizePolicy(sizePolicy) + self.use_release_group_genres.setChecked(True) + self.use_release_group_genres.setObjectName("use_release_group_genres") + self.verticalLayout_2.addWidget(self.use_release_group_genres) + self.verticalLayout_3 = QtWidgets.QVBoxLayout() + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.use_artist_genres = QtWidgets.QCheckBox(self.wikidata_vert_layout) + self.use_artist_genres.setObjectName("use_artist_genres") + self.verticalLayout_3.addWidget(self.use_artist_genres) + self.horizontalLayout_4 = QtWidgets.QHBoxLayout() + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + spacerItem = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_4.addItem(spacerItem) + self.use_artist_only_if_no_release = QtWidgets.QCheckBox(self.wikidata_vert_layout) + self.use_artist_only_if_no_release.setEnabled(False) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.use_artist_only_if_no_release.sizePolicy().hasHeightForWidth()) + self.use_artist_only_if_no_release.setSizePolicy(sizePolicy) + self.use_artist_only_if_no_release.setCheckable(True) + self.use_artist_only_if_no_release.setChecked(False) + self.use_artist_only_if_no_release.setObjectName("use_artist_only_if_no_release") + self.horizontalLayout_4.addWidget(self.use_artist_only_if_no_release) + self.verticalLayout_3.addLayout(self.horizontalLayout_4) + self.verticalLayout_2.addLayout(self.verticalLayout_3) + self.use_work_genres = QtWidgets.QCheckBox(self.wikidata_vert_layout) + self.use_work_genres.setChecked(True) + self.use_work_genres.setObjectName("use_work_genres") + self.verticalLayout_2.addWidget(self.use_work_genres) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.genre_delimiter_label = QtWidgets.QLabel(self.wikidata_vert_layout) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.genre_delimiter_label.sizePolicy().hasHeightForWidth()) + self.genre_delimiter_label.setSizePolicy(sizePolicy) + self.genre_delimiter_label.setObjectName("genre_delimiter_label") + self.horizontalLayout.addWidget(self.genre_delimiter_label) + self.genre_delimiter = QtWidgets.QComboBox(self.wikidata_vert_layout) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.genre_delimiter.sizePolicy().hasHeightForWidth()) + self.genre_delimiter.setSizePolicy(sizePolicy) + self.genre_delimiter.setEditable(True) + self.genre_delimiter.setObjectName("genre_delimiter") + self.genre_delimiter.addItem("") + self.genre_delimiter.setItemText(0, " / ") + self.genre_delimiter.addItem("") + self.genre_delimiter.addItem("") + self.genre_delimiter.setItemText(2, ";") + self.genre_delimiter.addItem("") + self.genre_delimiter.addItem("") + self.genre_delimiter.setItemText(4, ", ") + self.genre_delimiter.addItem("") + self.horizontalLayout.addWidget(self.genre_delimiter) + spacerItem1 = QtWidgets.QSpacerItem(100, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout.addItem(spacerItem1) + self.verticalLayout_2.addLayout(self.horizontalLayout) + self.verticalLayout = QtWidgets.QVBoxLayout() + self.verticalLayout.setObjectName("verticalLayout") + self.ignore_genres_label = QtWidgets.QLabel(self.wikidata_vert_layout) + self.ignore_genres_label.setObjectName("ignore_genres_label") + self.verticalLayout.addWidget(self.ignore_genres_label) + self.ignore_these_genres = QtWidgets.QLineEdit(self.wikidata_vert_layout) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.ignore_these_genres.sizePolicy().hasHeightForWidth()) + self.ignore_these_genres.setSizePolicy(sizePolicy) + self.ignore_these_genres.setObjectName("ignore_these_genres") + self.verticalLayout.addWidget(self.ignore_these_genres) + self.verticalLayout_2.addLayout(self.verticalLayout) + spacerItem2 = QtWidgets.QSpacerItem(263, 34, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.MinimumExpanding) + self.verticalLayout_2.addItem(spacerItem2) + self.use_release_group_genres.raise_() + self.use_artist_genres.raise_() + self.genre_delimiter_label.raise_() + self.genre_delimiter.raise_() + self.ignore_these_genres.raise_() + self.ignore_genres_label.raise_() + self.use_artist_only_if_no_release.raise_() + self.use_work_genres.raise_() + self.verticalLayout_5.addWidget(self.wikidata_vert_layout) + + self.retranslateUi(WikidataOptionsPage) + self.use_artist_genres.toggled['bool'].connect(self.use_artist_only_if_no_release.setEnabled) + QtCore.QMetaObject.connectSlotsByName(WikidataOptionsPage) + + def retranslateUi(self, WikidataOptionsPage): + _translate = QtCore.QCoreApplication.translate + self.use_release_group_genres.setText(_translate("WikidataOptionsPage", "Use Release Group genres")) + self.use_artist_genres.setText(_translate("WikidataOptionsPage", "Use Artist genres")) + self.use_artist_only_if_no_release.setText(_translate("WikidataOptionsPage", "Use Artist genres only if no Release Group genres exist")) + self.use_work_genres.setText(_translate("WikidataOptionsPage", "Use Work genres, when applicable")) + self.genre_delimiter_label.setText(_translate("WikidataOptionsPage", "Genre Delimiter (in case of multiple genres):")) + self.genre_delimiter.setItemText(1, _translate("WikidataOptionsPage", "; ")) + self.genre_delimiter.setItemText(3, _translate("WikidataOptionsPage", " ; ")) + self.genre_delimiter.setItemText(5, _translate("WikidataOptionsPage", " ")) + self.ignore_genres_label.setText(_translate("WikidataOptionsPage", "Ignore these genres:")) + + +if __name__ == "__main__": + import sys + app = QtWidgets.QApplication(sys.argv) + WikidataOptionsPage = QtWidgets.QWidget() + ui = Ui_WikidataOptionsPage() + ui.setupUi(WikidataOptionsPage) + WikidataOptionsPage.show() + sys.exit(app.exec_()) + From 0cfac8f9d163ed66dce2654e55c8a57289aa4c16 Mon Sep 17 00:00:00 2001 From: Paul Brackin Date: Wed, 19 Dec 2018 23:25:10 -0800 Subject: [PATCH 076/123] Update __init__.py add handler for delimiter parsing when expanding the genre list & use the updated list (not the old one!) --- plugins/wikidata/__init__.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/plugins/wikidata/__init__.py b/plugins/wikidata/__init__.py index 1d2047e2..c3718e42 100644 --- a/plugins/wikidata/__init__.py +++ b/plugins/wikidata/__init__.py @@ -148,6 +148,7 @@ def musicbrainz_release_lookup(self, item_id, metadata, response, reply, error): if relation.type == 'wikidata' and 'target' in relation.children: found = True wikidata_url = relation.target[0].text + log.debug('WIKIDATA: wikidata url found for RELEASE_GROUP: %s ', wikidata_url) self.process_wikidata(Wikidata.RELEASE_GROUP, wikidata_url, item_id) if 'artist' in response.metadata[0].children and self.use_artist_genres: if 'relation_list' in response.metadata[0].artist[0].children: @@ -156,12 +157,14 @@ def musicbrainz_release_lookup(self, item_id, metadata, response, reply, error): found = True wikidata_url = relation.target[0].text self.process_wikidata(Wikidata.ARTIST, wikidata_url, item_id) + log.debug('WIKIDATA: wikidata url found for ARTIST: %s ', wikidata_url) if 'work' in response.metadata[0].children and self.use_work_genres: if 'relation_list' in response.metadata[0].work[0].children: for relation in response.metadata[0].work[0].relation_list[0].relation: if relation.type == 'wikidata' and 'target' in relation.children: found = True wikidata_url = relation.target[0].text + log.debug('WIKIDATA: wikidata url found for WORK: %s ', wikidata_url) self.process_wikidata(Wikidata.WORK, wikidata_url, item_id) if not found: log.debug('WIKIDATA: No wikidata url found for item_id: %s ', item_id) @@ -174,7 +177,7 @@ def musicbrainz_release_lookup(self, item_id, metadata, response, reply, error): log.info('WIKIDATA: Total remaining requests: %s' % album._requests) if not self.itemAlbums: self.requests.clear() - log.info('WIKIDATA: Finished.') + log.info('WIKIDATA: Finished (A)') def process_wikidata(self, genre_source_type, wikidata_url, item_id): album = self.itemAlbums[item_id] @@ -225,6 +228,7 @@ def parse_wikidata_response(self, item, item_id, genre_source_type, response, re log.debug('WIKIDATA: Final list of wikidata id found: %s' % genre_entries) log.debug('WIKIDATA: Final list of genre: %s' % genre_list) log.info('WIKIDATA: Total items to update: %d ' % len(self.requests[item_id])) + for metadata in self.requests[item_id]: if genre_source_type == Wikidata.RELEASE_GROUP: metadata['release_group_genre_sourced'] = True @@ -234,10 +238,22 @@ def parse_wikidata_response(self, item, item_id, genre_source_type, response, re self.cache[item_id] = [] log.debug('wikidata: NOT setting Artist-sourced genre: %s ' % genre_list) continue - new_genre = set(metadata.getall("genre")) + else: + log.debug('wikidata: Setting Artist-sourced genre: %s ' % genre_list) + + # getall doesn't handle delimiters so we need to check-n-parse here + old_genre_metadata = metadata.getall("genre") + old_genre_list = [] + for genre in old_genre_metadata: + if self.genre_delimiter in genre: + old_genre_list.extend(genre.split(self.genre_delimiter)) + else: + old_genre_list.append(genre) + + new_genre = set(old_genre_list) new_genre.update(genre_list) # sort the new genre list so that they don't appear as new entries (not a change) next time - metadata["genre"] = self.genre_delimiter.join(sorted(genre_list)) + metadata["genre"] = self.genre_delimiter.join(sorted(new_genre)) self.cache[item_id] = genre_list log.debug('wikidata: setting genre: %s ' % genre_list) else: @@ -253,21 +269,26 @@ def parse_wikidata_response(self, item, item_id, genre_source_type, response, re log.info('wikidata: total remaining requests: %s' % album._requests) if not self.itemAlbums: self.requests.clear() - log.info('wikidata: finished.') - + log.info('WIKIDATA: Finished (B)') def process_track(self, album, metadata, trackXmlNode, releaseXmlNode): self.mb_host = config.setting["server_host"] self.mb_port = config.setting["server_port"] - self.use_release_group_genres = config.setting["wikidata_use_release_group_genres"] + self.use_release_group_genres = config.setting[""] self.use_work_genres = config.setting["wikidata_use_work_genres"] # Some changed settings could invalidate the cache, so clear it to be safe + if self.use_release_group_genres != config.setting["wikidata_use_release_group_genres"]: + self.use_release_group_genres = config.setting["wikidata_use_release_group_genres"] + self.cache.clear() if self.use_artist_genres != config.setting["wikidata_use_artist_genres"]: self.use_artist_genres = config.setting["wikidata_use_artist_genres"] self.cache.clear() if self.use_artist_only_if_no_release != config.setting["wikidata_use_artist_only_if_no_release"]: self.use_artist_only_if_no_release = config.setting["wikidata_use_artist_only_if_no_release"] self.cache.clear() + if self.use_work_genres != config.setting["wikidata_use_work_genres"]: + self.use_work_genres = config.setting["wikidata_use_work_genres"] + self.cache.clear() if self.ignore_these_genres != parse_ignored_tags(config.setting["wikidata_ignore_these_genres"]): self.ignore_these_genres = parse_ignored_tags(config.setting["wikidata_ignore_these_genres"]) self.cache.clear() From 0de74661ea4e0ea6cee0cecf85c7efb9f5e3ebdc Mon Sep 17 00:00:00 2001 From: Paul Brackin Date: Thu, 20 Dec 2018 13:57:25 -0800 Subject: [PATCH 077/123] add ability to exclude certain artists from genre pull, clean-up & optimize New feature: ability to selectively exclude artist-genre-sourcing according to artist. Also removed reference to global cache, which was a C&P artifact from lastfm plugin, which I absconded some code from. Also I optimized the RegEx parsers a bit to keep max speed. --- plugins/wikidata/__init__.py | 53 ++-- plugins/wikidata/options_wikidata.ui | 385 +++++++++++++++--------- plugins/wikidata/ui_options_wikidata.py | 109 +++++-- 3 files changed, 352 insertions(+), 195 deletions(-) diff --git a/plugins/wikidata/__init__.py b/plugins/wikidata/__init__.py index c3718e42..bcb5c7c5 100644 --- a/plugins/wikidata/__init__.py +++ b/plugins/wikidata/__init__.py @@ -24,6 +24,8 @@ def parse_ignored_tags(ignore_tags_setting): ignore_tags = [] for tag in ignore_tags_setting.lower().split(','): + if not tag: + break tag = tag.strip() if tag.startswith('/') and tag.endswith('/'): try: @@ -36,14 +38,15 @@ def parse_ignored_tags(ignore_tags_setting): def matches_ignored(ignore_tags, tag): - tag = tag.lower().strip() - for pattern in ignore_tags: - if hasattr(pattern, 'match'): - match = pattern.match(tag) - else: - match = pattern == tag - if match: - return True + if ignore_tags: + tag = tag.lower().strip() + for pattern in ignore_tags: + if hasattr(pattern, 'match'): + match = pattern.match(tag) + else: + match = pattern == tag + if match: + return True return False @@ -77,8 +80,11 @@ def __init__(self): self.use_release_group_genres = False self.use_artist_genres = False self.use_artist_only_if_no_release = False + self.ignore_genres_from_these_artists = '' + self.ignore_genres_from_these_artists_list = [] self.use_work_genres = True self.ignore_these_genres = '' + self.ignore_these_genres_list = [] self.genre_delimiter = '' # not used @@ -217,7 +223,7 @@ def parse_wikidata_response(self, item, item_id, genre_source_type, response, re for node2 in list1: if node2.attribs.get('lang') == 'en': genre = node2.text.title() - if not matches_ignored(self.ignore_these_genres, genre): + if not matches_ignored(self.ignore_these_genres_list, genre): genre_list.append(genre) log.debug('New genre has been found and ALLOWED: %s' % genre) else: @@ -233,13 +239,14 @@ def parse_wikidata_response(self, item, item_id, genre_source_type, response, re if genre_source_type == Wikidata.RELEASE_GROUP: metadata['release_group_genre_sourced'] = True elif genre_source_type == Wikidata.ARTIST: - if self.use_artist_only_if_no_release and metadata['release_group_genre_sourced']: + if self.use_artist_only_if_no_release and metadata['release_group_genre_sourced'] or \ + matches_ignored(self.ignore_genres_from_these_artists_list, metadata.get("artist")): if item_id not in self.cache: self.cache[item_id] = [] - log.debug('wikidata: NOT setting Artist-sourced genre: %s ' % genre_list) + log.debug('WIKIDATA: NOT setting Artist-sourced genre: %s ' % genre_list) continue else: - log.debug('wikidata: Setting Artist-sourced genre: %s ' % genre_list) + log.debug('WIKIDATA: Setting Artist-sourced genre: %s ' % genre_list) # getall doesn't handle delimiters so we need to check-n-parse here old_genre_metadata = metadata.getall("genre") @@ -253,20 +260,21 @@ def parse_wikidata_response(self, item, item_id, genre_source_type, response, re new_genre = set(old_genre_list) new_genre.update(genre_list) # sort the new genre list so that they don't appear as new entries (not a change) next time + log.debug('WIKIDATA: setting metadata genre to : %s ' % new_genre) metadata["genre"] = self.genre_delimiter.join(sorted(new_genre)) + log.debug('WIKIDATA: setting cache genre to : %s ' % genre_list) self.cache[item_id] = genre_list - log.debug('wikidata: setting genre: %s ' % genre_list) else: - log.debug('wikidata: genre not found in wikidata') + log.debug('WIKIDATA: genre not found in wikidata') - log.debug('wikidata: seeing if we can finalize tags...') + log.debug('WIKIDATA: seeing if we can finalize tags...') album = self.itemAlbums[item_id] album._requests -= 1 if not album._requests: self.itemAlbums = {k: v for k, v in self.itemAlbums.items() if v != album} album._finalize_loading(None) - log.info('wikidata: total remaining requests: %s' % album._requests) + log.info('WIKIDATA: total remaining requests: %s' % album._requests) if not self.itemAlbums: self.requests.clear() log.info('WIKIDATA: Finished (B)') @@ -286,11 +294,18 @@ def process_track(self, album, metadata, trackXmlNode, releaseXmlNode): if self.use_artist_only_if_no_release != config.setting["wikidata_use_artist_only_if_no_release"]: self.use_artist_only_if_no_release = config.setting["wikidata_use_artist_only_if_no_release"] self.cache.clear() + if self.ignore_genres_from_these_artists != parse_ignored_tags( + config.setting["wikidata_ignore_genres_from_these_artists"]): + self.ignore_genres_from_these_artists_list = parse_ignored_tags( + config.setting["wikidata_ignore_genres_from_these_artists"]) + self.cache.clear() if self.use_work_genres != config.setting["wikidata_use_work_genres"]: self.use_work_genres = config.setting["wikidata_use_work_genres"] self.cache.clear() if self.ignore_these_genres != parse_ignored_tags(config.setting["wikidata_ignore_these_genres"]): self.ignore_these_genres = parse_ignored_tags(config.setting["wikidata_ignore_these_genres"]) + self.ignore_these_genres_list = parse_ignored_tags( + config.setting["wikidata_ignore_these_genres"]) self.cache.clear() self.genre_delimiter = config.setting["wikidata_genre_delimiter"] self.ws = album.tagger.webservice @@ -324,6 +339,7 @@ class WikidataOptionsPage(OptionsPage): config.BoolOption("setting", "wikidata_use_release_group_genres", True), config.BoolOption("setting", "wikidata_use_artist_genres", True), config.BoolOption("setting", "wikidata_use_artist_only_if_no_release", True), + config.TextOption("setting", "wikidata_ignore_genres_from_these_artists", ""), config.BoolOption("setting", "wikidata_use_work_genres", True), config.TextOption("setting", "wikidata_ignore_these_genres", "seen live, favorites, /\\d+ of \\d+ stars/"), config.TextOption("setting", "wikidata_genre_delimiter", "; "), @@ -342,18 +358,17 @@ def load(self): self.ui.use_release_group_genres.setChecked(setting["wikidata_use_release_group_genres"]) self.ui.use_artist_genres.setChecked(setting["wikidata_use_artist_genres"]) self.ui.use_artist_only_if_no_release.setChecked(setting["wikidata_use_artist_only_if_no_release"]) + self.ui.ignore_genres_from_these_artists.setText(setting["wikidata_ignore_genres_from_these_artists"]) self.ui.use_work_genres.setChecked(setting["wikidata_use_work_genres"]) self.ui.ignore_these_genres.setText(setting["wikidata_ignore_these_genres"]) self.ui.genre_delimiter.setEditText(setting["wikidata_genre_delimiter"]) def save(self): - global _cache setting = config.setting - if setting["wikidata_ignore_these_genres"] != str(self.ui.ignore_these_genres.text()): - _cache = {} setting["wikidata_use_release_group_genres"] = self.ui.use_release_group_genres.isChecked() setting["wikidata_use_artist_genres"] = self.ui.use_artist_genres.isChecked() setting["wikidata_use_artist_only_if_no_release"] = self.ui.use_artist_only_if_no_release.isChecked() + setting["wikidata_ignore_genres_from_these_artists"] = str(self.ui.ignore_genres_from_these_artists.text()) setting["wikidata_use_work_genres"] = self.ui.use_work_genres.isChecked() setting["wikidata_ignore_these_genres"] = str(self.ui.ignore_these_genres.text()) setting["wikidata_genre_delimiter"] = str(self.ui.genre_delimiter.currentText()) diff --git a/plugins/wikidata/options_wikidata.ui b/plugins/wikidata/options_wikidata.ui index ade52bed..53ddbf90 100644 --- a/plugins/wikidata/options_wikidata.ui +++ b/plugins/wikidata/options_wikidata.ui @@ -6,42 +6,33 @@ 0 0 - 518 - 289 + 602 + 497 - - - QLayout::SetDefaultConstraint - - - 9 - - - 9 - - - 9 - - - 9 - + - + true - + 0 0 - + - + + + + 0 + 0 + + - Use Track genres + Use Release Group genres true @@ -49,130 +40,176 @@ - + + + Qt::Horizontal + + + + + + + Use Artist genres + + + + + - + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + false + - + 0 0 - Use Release genres + Use Artist genres only if no Release Group genres exist + + + true false - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 20 - 20 - - - - - - - - false - - - - 1 - 0 - - - - Use Release genres only if no track tags exist - - - - - - + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + - + + + false + + + + 0 + 0 + + - Use Artist genres + Ignore genres from these Artist: (comma separated regular expressions) + + + + - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 20 - 20 - - - - - - - - false - - - - 0 - 0 - - - - Use Artist genres only if no track tags and no release tags exist - - - false - - - - + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + false + + - + + + Qt::Horizontal + + + + + + + Use Work genres, when applicable + + + true + + + + + + + Qt::Horizontal + + + + + + + true + - + 0 0 - Genre Delimiter (in case of multiple genres): + Genre Delimiter: + + + false + + true + - + 0 0 @@ -187,7 +224,7 @@ - , + ; @@ -195,6 +232,16 @@ ; + + + ; + + + + + , + + @@ -207,9 +254,12 @@ Qt::Horizontal + + QSizePolicy::Expanding + - 100 + 150 20 @@ -219,10 +269,17 @@ + + + + Qt::Horizontal + + + - Ignore these genres: + Ignore these genres: (comma separated regular expressions) @@ -238,44 +295,86 @@ - - - - Qt::Vertical - - - QSizePolicy::MinimumExpanding - - - - 263 - 34 - - - - - use_track_genres - use_release_genres - use_artist_genres - use_release_only_if_no_track - ignore_tags_4 - genre_delimiter_label - genre_delimiter - ignore_these_genres - ignore_genres_label + use_release_group_genres + + use_work_genres + line_2 + line_3 + horizontalLayoutWidget + horizontalLayoutWidget_2 - spacer_label - horizontalSpacer_2 - horizontalSpacer_3 - use_artist_only_if_no_track_n_no_release + verticalLayoutWidget + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + - - use_track_genres - - + + + use_artist_genres + toggled(bool) + ignore_genres_from_these_artists_label + setEnabled(bool) + + + 300 + 58 + + + 313 + 111 + + + + + use_artist_genres + toggled(bool) + use_artist_only_if_no_release + setEnabled(bool) + + + 300 + 58 + + + 313 + 83 + + + + + use_artist_genres + toggled(bool) + ignore_genres_from_these_artists + setEnabled(bool) + + + 300 + 58 + + + 313 + 139 + + + +
    diff --git a/plugins/wikidata/ui_options_wikidata.py b/plugins/wikidata/ui_options_wikidata.py index 64d97f1c..a755b3e4 100644 --- a/plugins/wikidata/ui_options_wikidata.py +++ b/plugins/wikidata/ui_options_wikidata.py @@ -11,14 +11,12 @@ class Ui_WikidataOptionsPage(object): def setupUi(self, WikidataOptionsPage): WikidataOptionsPage.setObjectName("WikidataOptionsPage") - WikidataOptionsPage.resize(513, 310) - self.verticalLayout_5 = QtWidgets.QVBoxLayout(WikidataOptionsPage) - self.verticalLayout_5.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) - self.verticalLayout_5.setContentsMargins(9, 9, 9, 9) - self.verticalLayout_5.setObjectName("verticalLayout_5") - self.wikidata_vert_layout = QtWidgets.QWidget(WikidataOptionsPage) + WikidataOptionsPage.resize(602, 497) + self.verticalLayout_4 = QtWidgets.QVBoxLayout(WikidataOptionsPage) + self.verticalLayout_4.setObjectName("verticalLayout_4") + self.wikidata_vert_layout = QtWidgets.QFrame(WikidataOptionsPage) self.wikidata_vert_layout.setEnabled(True) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.MinimumExpanding) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.wikidata_vert_layout.sizePolicy().hasHeightForWidth()) @@ -35,11 +33,14 @@ def setupUi(self, WikidataOptionsPage): self.use_release_group_genres.setChecked(True) self.use_release_group_genres.setObjectName("use_release_group_genres") self.verticalLayout_2.addWidget(self.use_release_group_genres) - self.verticalLayout_3 = QtWidgets.QVBoxLayout() - self.verticalLayout_3.setObjectName("verticalLayout_3") + self.line = QtWidgets.QFrame(self.wikidata_vert_layout) + self.line.setFrameShape(QtWidgets.QFrame.HLine) + self.line.setFrameShadow(QtWidgets.QFrame.Sunken) + self.line.setObjectName("line") + self.verticalLayout_2.addWidget(self.line) self.use_artist_genres = QtWidgets.QCheckBox(self.wikidata_vert_layout) self.use_artist_genres.setObjectName("use_artist_genres") - self.verticalLayout_3.addWidget(self.use_artist_genres) + self.verticalLayout_2.addWidget(self.use_artist_genres) self.horizontalLayout_4 = QtWidgets.QHBoxLayout() self.horizontalLayout_4.setObjectName("horizontalLayout_4") spacerItem = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum) @@ -55,24 +56,59 @@ def setupUi(self, WikidataOptionsPage): self.use_artist_only_if_no_release.setChecked(False) self.use_artist_only_if_no_release.setObjectName("use_artist_only_if_no_release") self.horizontalLayout_4.addWidget(self.use_artist_only_if_no_release) - self.verticalLayout_3.addLayout(self.horizontalLayout_4) - self.verticalLayout_2.addLayout(self.verticalLayout_3) + self.verticalLayout_2.addLayout(self.horizontalLayout_4) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + spacerItem1 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_2.addItem(spacerItem1) + self.ignore_genres_from_these_artists_label = QtWidgets.QLabel(self.wikidata_vert_layout) + self.ignore_genres_from_these_artists_label.setEnabled(False) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.ignore_genres_from_these_artists_label.sizePolicy().hasHeightForWidth()) + self.ignore_genres_from_these_artists_label.setSizePolicy(sizePolicy) + self.ignore_genres_from_these_artists_label.setObjectName("ignore_genres_from_these_artists_label") + self.horizontalLayout_2.addWidget(self.ignore_genres_from_these_artists_label) + self.verticalLayout_2.addLayout(self.horizontalLayout_2) + self.horizontalLayout_3 = QtWidgets.QHBoxLayout() + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + spacerItem2 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_3.addItem(spacerItem2) + self.ignore_genres_from_these_artists = QtWidgets.QLineEdit(self.wikidata_vert_layout) + self.ignore_genres_from_these_artists.setEnabled(False) + self.ignore_genres_from_these_artists.setObjectName("ignore_genres_from_these_artists") + self.horizontalLayout_3.addWidget(self.ignore_genres_from_these_artists) + self.verticalLayout_2.addLayout(self.horizontalLayout_3) + self.line_2 = QtWidgets.QFrame(self.wikidata_vert_layout) + self.line_2.setFrameShape(QtWidgets.QFrame.HLine) + self.line_2.setFrameShadow(QtWidgets.QFrame.Sunken) + self.line_2.setObjectName("line_2") + self.verticalLayout_2.addWidget(self.line_2) self.use_work_genres = QtWidgets.QCheckBox(self.wikidata_vert_layout) self.use_work_genres.setChecked(True) self.use_work_genres.setObjectName("use_work_genres") self.verticalLayout_2.addWidget(self.use_work_genres) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") + self.line_3 = QtWidgets.QFrame(self.wikidata_vert_layout) + self.line_3.setFrameShape(QtWidgets.QFrame.HLine) + self.line_3.setFrameShadow(QtWidgets.QFrame.Sunken) + self.line_3.setObjectName("line_3") + self.verticalLayout_2.addWidget(self.line_3) + self.horizontalLayout_5 = QtWidgets.QHBoxLayout() + self.horizontalLayout_5.setObjectName("horizontalLayout_5") self.genre_delimiter_label = QtWidgets.QLabel(self.wikidata_vert_layout) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) + self.genre_delimiter_label.setEnabled(True) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.genre_delimiter_label.sizePolicy().hasHeightForWidth()) self.genre_delimiter_label.setSizePolicy(sizePolicy) + self.genre_delimiter_label.setWordWrap(False) self.genre_delimiter_label.setObjectName("genre_delimiter_label") - self.horizontalLayout.addWidget(self.genre_delimiter_label) + self.horizontalLayout_5.addWidget(self.genre_delimiter_label) self.genre_delimiter = QtWidgets.QComboBox(self.wikidata_vert_layout) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + self.genre_delimiter.setEnabled(True) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.genre_delimiter.sizePolicy().hasHeightForWidth()) @@ -88,12 +124,18 @@ def setupUi(self, WikidataOptionsPage): self.genre_delimiter.addItem("") self.genre_delimiter.setItemText(4, ", ") self.genre_delimiter.addItem("") - self.horizontalLayout.addWidget(self.genre_delimiter) - spacerItem1 = QtWidgets.QSpacerItem(100, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout.addItem(spacerItem1) - self.verticalLayout_2.addLayout(self.horizontalLayout) + self.genre_delimiter.setItemText(5, "") + self.horizontalLayout_5.addWidget(self.genre_delimiter) + spacerItem3 = QtWidgets.QSpacerItem(150, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_5.addItem(spacerItem3) + self.verticalLayout_2.addLayout(self.horizontalLayout_5) self.verticalLayout = QtWidgets.QVBoxLayout() self.verticalLayout.setObjectName("verticalLayout") + self.line_4 = QtWidgets.QFrame(self.wikidata_vert_layout) + self.line_4.setFrameShape(QtWidgets.QFrame.HLine) + self.line_4.setFrameShadow(QtWidgets.QFrame.Sunken) + self.line_4.setObjectName("line_4") + self.verticalLayout.addWidget(self.line_4) self.ignore_genres_label = QtWidgets.QLabel(self.wikidata_vert_layout) self.ignore_genres_label.setObjectName("ignore_genres_label") self.verticalLayout.addWidget(self.ignore_genres_label) @@ -106,20 +148,21 @@ def setupUi(self, WikidataOptionsPage): self.ignore_these_genres.setObjectName("ignore_these_genres") self.verticalLayout.addWidget(self.ignore_these_genres) self.verticalLayout_2.addLayout(self.verticalLayout) - spacerItem2 = QtWidgets.QSpacerItem(263, 34, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.MinimumExpanding) - self.verticalLayout_2.addItem(spacerItem2) self.use_release_group_genres.raise_() - self.use_artist_genres.raise_() - self.genre_delimiter_label.raise_() - self.genre_delimiter.raise_() - self.ignore_these_genres.raise_() - self.ignore_genres_label.raise_() - self.use_artist_only_if_no_release.raise_() self.use_work_genres.raise_() - self.verticalLayout_5.addWidget(self.wikidata_vert_layout) + self.line_2.raise_() + self.line_3.raise_() + self.verticalLayout_4.addWidget(self.wikidata_vert_layout) + self.verticalLayout_3 = QtWidgets.QVBoxLayout() + self.verticalLayout_3.setObjectName("verticalLayout_3") + spacerItem4 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout_3.addItem(spacerItem4) + self.verticalLayout_4.addLayout(self.verticalLayout_3) self.retranslateUi(WikidataOptionsPage) + self.use_artist_genres.toggled['bool'].connect(self.ignore_genres_from_these_artists_label.setEnabled) self.use_artist_genres.toggled['bool'].connect(self.use_artist_only_if_no_release.setEnabled) + self.use_artist_genres.toggled['bool'].connect(self.ignore_genres_from_these_artists.setEnabled) QtCore.QMetaObject.connectSlotsByName(WikidataOptionsPage) def retranslateUi(self, WikidataOptionsPage): @@ -127,12 +170,12 @@ def retranslateUi(self, WikidataOptionsPage): self.use_release_group_genres.setText(_translate("WikidataOptionsPage", "Use Release Group genres")) self.use_artist_genres.setText(_translate("WikidataOptionsPage", "Use Artist genres")) self.use_artist_only_if_no_release.setText(_translate("WikidataOptionsPage", "Use Artist genres only if no Release Group genres exist")) + self.ignore_genres_from_these_artists_label.setText(_translate("WikidataOptionsPage", "Ignore genres from these Artist: (comma separated regular expressions)")) self.use_work_genres.setText(_translate("WikidataOptionsPage", "Use Work genres, when applicable")) - self.genre_delimiter_label.setText(_translate("WikidataOptionsPage", "Genre Delimiter (in case of multiple genres):")) + self.genre_delimiter_label.setText(_translate("WikidataOptionsPage", "Genre Delimiter:")) self.genre_delimiter.setItemText(1, _translate("WikidataOptionsPage", "; ")) self.genre_delimiter.setItemText(3, _translate("WikidataOptionsPage", " ; ")) - self.genre_delimiter.setItemText(5, _translate("WikidataOptionsPage", " ")) - self.ignore_genres_label.setText(_translate("WikidataOptionsPage", "Ignore these genres:")) + self.ignore_genres_label.setText(_translate("WikidataOptionsPage", "Ignore these genres: (comma separated regular expressions)")) if __name__ == "__main__": From fa637435714ebdf12e4c9e8bcc1cdc0277f2be34 Mon Sep 17 00:00:00 2001 From: evandrocoan Date: Sun, 23 Dec 2018 14:49:13 -0200 Subject: [PATCH 078/123] Created Add Column album plugin --- plugins/add_album_column/__init__.py | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 plugins/add_album_column/__init__.py diff --git a/plugins/add_album_column/__init__.py b/plugins/add_album_column/__init__.py new file mode 100644 index 00000000..f4fbecbd --- /dev/null +++ b/plugins/add_album_column/__init__.py @@ -0,0 +1,33 @@ +# -*- coding: UTF-8 -*- + +# +# Licensing +# +# Channel Manager Main, Create and maintain channel files +# Copyright (C) 2017 Evandro Coan +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 3 of the License, or ( at +# your option ) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +PLUGIN_NAME = u"Add Album Column" +PLUGIN_AUTHOR = u"Evandro Coan" +PLUGIN_DESCRIPTION = "Add the Album column to the main window panel." + +PLUGIN_VERSION = "1.0" +PLUGIN_API_VERSIONS = ["2.0"] +PLUGIN_LICENSE = "GPLv3" +PLUGIN_LICENSE_URL = "http://www.gnu.org/licenses/" + +from picard.ui.itemviews import MainPanel +MainPanel.columns.append((N_('Album'), 'album')) From ad05698ecc3f271bc31e37d7378fbd061fdf194e Mon Sep 17 00:00:00 2001 From: Paul Brackin Date: Sun, 23 Dec 2018 12:47:49 -0800 Subject: [PATCH 079/123] Disable genre delimiter setting if Tags setting is ID3v24, handle & remove blank delimiter, clean up layout, add cutoffs based on settings Disable genre delimiter setting if Tags setting is ID3v24 Handle blank delimiters Remove blank delimiter as an option Clean up options page layout Add cutoffs to process_track based on settings, to prevent uneeded MB API calls --- plugins/wikidata/__init__.py | 66 +++-- plugins/wikidata/options_wikidata.ui | 331 ++++++++++++++---------- plugins/wikidata/ui_options_wikidata.py | 175 +++++++------ 3 files changed, 335 insertions(+), 237 deletions(-) diff --git a/plugins/wikidata/__init__.py b/plugins/wikidata/__init__.py index bcb5c7c5..43d99f1e 100644 --- a/plugins/wikidata/__init__.py +++ b/plugins/wikidata/__init__.py @@ -252,7 +252,7 @@ def parse_wikidata_response(self, item, item_id, genre_source_type, response, re old_genre_metadata = metadata.getall("genre") old_genre_list = [] for genre in old_genre_metadata: - if self.genre_delimiter in genre: + if self.genre_delimiter and self.genre_delimiter in genre: old_genre_list.extend(genre.split(self.genre_delimiter)) else: old_genre_list.append(genre) @@ -261,7 +261,11 @@ def parse_wikidata_response(self, item, item_id, genre_source_type, response, re new_genre.update(genre_list) # sort the new genre list so that they don't appear as new entries (not a change) next time log.debug('WIKIDATA: setting metadata genre to : %s ' % new_genre) - metadata["genre"] = self.genre_delimiter.join(sorted(new_genre)) + if self.genre_delimiter: + metadata["genre"] = self.genre_delimiter.join(sorted(new_genre)) + else: + metadata["genre"] = sorted(new_genre) + log.debug('WIKIDATA: setting cache genre to : %s ' % genre_list) self.cache[item_id] = genre_list else: @@ -280,6 +284,32 @@ def parse_wikidata_response(self, item, item_id, genre_source_type, response, re log.info('WIKIDATA: Finished (B)') def process_track(self, album, metadata, trackXmlNode, releaseXmlNode): + self.update_settings() + self.ws = album.tagger.webservice + self.log = album.log + + log.info('WIKIDATA: Processing Track...') + if self.use_release_group_genres: + for release_group in metadata.getall('musicbrainz_releasegroupid'): + log.debug('WIKIDATA: Looking up release group metadata for %s ' % release_group) + self.process_request(metadata, album, release_group, type='release-group') + + if self.use_artist_genres: + for artist in metadata.getall('musicbrainz_albumartistid'): + log.debug('WIKIDATA: Processing release artist %s' % artist) + self.process_request(metadata, album, artist, type='artist') + + if self.use_artist_genres: + for artist in metadata.getall('musicbrainz_artistid'): + log.debug('WIKIDATA: Processing track artist %s' % artist) + self.process_request(metadata, album, artist, type='artist') + + if self.use_work_genres: + for workid in metadata.getall('musicbrainz_workid'): + log.debug('WIKIDATA: Processing track artist %s' % workid) + self.process_request(metadata, album, workid, type='work') + + def update_settings(self): self.mb_host = config.setting["server_host"] self.mb_port = config.setting["server_port"] self.use_release_group_genres = config.setting[""] @@ -307,27 +337,8 @@ def process_track(self, album, metadata, trackXmlNode, releaseXmlNode): self.ignore_these_genres_list = parse_ignored_tags( config.setting["wikidata_ignore_these_genres"]) self.cache.clear() - self.genre_delimiter = config.setting["wikidata_genre_delimiter"] - self.ws = album.tagger.webservice - self.log = album.log - - log.info('WIKIDATA: Processing Track...') - for release_group in metadata.getall('musicbrainz_releasegroupid'): - log.debug('WIKIDATA: Looking up release group metadata for %s ' % release_group) - self.process_request(metadata, album, release_group, type='release-group') - - for artist in metadata.getall('musicbrainz_albumartistid'): - log.debug('WIKIDATA: Processing release artist %s' % artist) - self.process_request(metadata, album, artist, type='artist') - - for artist in metadata.getall('musicbrainz_artistid'): - log.debug('WIKIDATA: Processing track artist %s' % artist) - self.process_request(metadata, album, artist, type='artist') - - if 'musicbrainz_workid' in metadata: - for workid in metadata.getall('musicbrainz_workid'): - log.debug('WIKIDATA: Processing track artist %s' % workid) - self.process_request(metadata, album, workid, type='work') + if config.setting["write_id3v23"]: + self.genre_delimiter = config.setting["wikidata_genre_delimiter"] class WikidataOptionsPage(OptionsPage): @@ -349,6 +360,9 @@ def __init__(self, parent=None): super(WikidataOptionsPage, self).__init__(parent) self.ui = Ui_WikidataOptionsPage() self.ui.setupUi(self) + if not config.setting["write_id3v23"]: + self.ui.genre_delimiter.setEnabled(False); + self.ui.genre_delimiter_label.setEnabled(False); def info(self): pass @@ -361,7 +375,8 @@ def load(self): self.ui.ignore_genres_from_these_artists.setText(setting["wikidata_ignore_genres_from_these_artists"]) self.ui.use_work_genres.setChecked(setting["wikidata_use_work_genres"]) self.ui.ignore_these_genres.setText(setting["wikidata_ignore_these_genres"]) - self.ui.genre_delimiter.setEditText(setting["wikidata_genre_delimiter"]) + if config.setting["write_id3v23"]: + self.ui.genre_delimiter.setEditText(setting["wikidata_genre_delimiter"]) def save(self): setting = config.setting @@ -371,7 +386,8 @@ def save(self): setting["wikidata_ignore_genres_from_these_artists"] = str(self.ui.ignore_genres_from_these_artists.text()) setting["wikidata_use_work_genres"] = self.ui.use_work_genres.isChecked() setting["wikidata_ignore_these_genres"] = str(self.ui.ignore_these_genres.text()) - setting["wikidata_genre_delimiter"] = str(self.ui.genre_delimiter.currentText()) + if config.setting["write_id3v23"]: + setting["wikidata_genre_delimiter"] = str(self.ui.genre_delimiter.currentText()) wikidata = Wikidata() diff --git a/plugins/wikidata/options_wikidata.ui b/plugins/wikidata/options_wikidata.ui index 53ddbf90..debcf569 100644 --- a/plugins/wikidata/options_wikidata.ui +++ b/plugins/wikidata/options_wikidata.ui @@ -7,24 +7,36 @@ 0 0 602 - 497 + 512 - + - + true - + 0 0 - + + + 0 + 0 + + + + Release Group Genre Settings + + + + true + 0 @@ -39,13 +51,30 @@ - - - - Qt::Horizontal - - - + + + + + + + true + + + + 0 + 0 + + + + + 0 + 0 + + + + Artist Genre Settings + + @@ -125,7 +154,7 @@ - Ignore genres from these Artist: (comma separated regular expressions) + Ignore Artist genres from these Artist(s): (comma separated regular expressions) @@ -158,13 +187,24 @@ - - - - Qt::Horizontal - - - + + + + + + + true + + + + 0 + 0 + + + + Work Genre Settings + + @@ -175,98 +215,27 @@ - - - - Qt::Horizontal - - - - - - - - - true - - - - 0 - 0 - - - - Genre Delimiter: - - - false - - - - - - - true - - - - 0 - 0 - - - - true - - - - / - - - - - ; - - - - - ; - - - - - ; - - - - - , - - - - - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Expanding - - - - 150 - 20 - - - - - - + + + + + + + + 0 + 0 + + + + + 0 + 120 + + + + General Settings + + @@ -293,36 +262,126 @@ + + + + + + false + + + + 0 + 0 + + + + Genre Delimiter: + + + false + + + + + + + false + + + + 0 + 0 + + + + true + + + + / + + + + + ; + + + + + ; + + + + + ; + + + + + , + + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 10 + 20 + + + + + + + + false + + + (only applicable to ID3v2.3 tags) + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + - use_release_group_genres - - use_work_genres - line_2 - line_3 - horizontalLayoutWidget - horizontalLayoutWidget_2 - - verticalLayoutWidget - - - - - Qt::Vertical - - - - 20 - 40 - - - - - + + + Qt::Vertical + + + + 20 + 40 + + + diff --git a/plugins/wikidata/ui_options_wikidata.py b/plugins/wikidata/ui_options_wikidata.py index a755b3e4..c16bb22f 100644 --- a/plugins/wikidata/ui_options_wikidata.py +++ b/plugins/wikidata/ui_options_wikidata.py @@ -11,20 +11,22 @@ class Ui_WikidataOptionsPage(object): def setupUi(self, WikidataOptionsPage): WikidataOptionsPage.setObjectName("WikidataOptionsPage") - WikidataOptionsPage.resize(602, 497) - self.verticalLayout_4 = QtWidgets.QVBoxLayout(WikidataOptionsPage) - self.verticalLayout_4.setObjectName("verticalLayout_4") - self.wikidata_vert_layout = QtWidgets.QFrame(WikidataOptionsPage) - self.wikidata_vert_layout.setEnabled(True) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.MinimumExpanding) + WikidataOptionsPage.resize(602, 512) + self.verticalLayout_2 = QtWidgets.QVBoxLayout(WikidataOptionsPage) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.groupBox = QtWidgets.QGroupBox(WikidataOptionsPage) + self.groupBox.setEnabled(True) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.wikidata_vert_layout.sizePolicy().hasHeightForWidth()) - self.wikidata_vert_layout.setSizePolicy(sizePolicy) - self.wikidata_vert_layout.setObjectName("wikidata_vert_layout") - self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.wikidata_vert_layout) - self.verticalLayout_2.setObjectName("verticalLayout_2") - self.use_release_group_genres = QtWidgets.QCheckBox(self.wikidata_vert_layout) + sizePolicy.setHeightForWidth(self.groupBox.sizePolicy().hasHeightForWidth()) + self.groupBox.setSizePolicy(sizePolicy) + self.groupBox.setMinimumSize(QtCore.QSize(0, 0)) + self.groupBox.setObjectName("groupBox") + self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.groupBox) + self.verticalLayout_4.setObjectName("verticalLayout_4") + self.use_release_group_genres = QtWidgets.QCheckBox(self.groupBox) + self.use_release_group_genres.setEnabled(True) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -32,20 +34,27 @@ def setupUi(self, WikidataOptionsPage): self.use_release_group_genres.setSizePolicy(sizePolicy) self.use_release_group_genres.setChecked(True) self.use_release_group_genres.setObjectName("use_release_group_genres") - self.verticalLayout_2.addWidget(self.use_release_group_genres) - self.line = QtWidgets.QFrame(self.wikidata_vert_layout) - self.line.setFrameShape(QtWidgets.QFrame.HLine) - self.line.setFrameShadow(QtWidgets.QFrame.Sunken) - self.line.setObjectName("line") - self.verticalLayout_2.addWidget(self.line) - self.use_artist_genres = QtWidgets.QCheckBox(self.wikidata_vert_layout) + self.verticalLayout_4.addWidget(self.use_release_group_genres) + self.verticalLayout_2.addWidget(self.groupBox) + self.groupBox_2 = QtWidgets.QGroupBox(WikidataOptionsPage) + self.groupBox_2.setEnabled(True) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.groupBox_2.sizePolicy().hasHeightForWidth()) + self.groupBox_2.setSizePolicy(sizePolicy) + self.groupBox_2.setMinimumSize(QtCore.QSize(0, 0)) + self.groupBox_2.setObjectName("groupBox_2") + self.verticalLayout_8 = QtWidgets.QVBoxLayout(self.groupBox_2) + self.verticalLayout_8.setObjectName("verticalLayout_8") + self.use_artist_genres = QtWidgets.QCheckBox(self.groupBox_2) self.use_artist_genres.setObjectName("use_artist_genres") - self.verticalLayout_2.addWidget(self.use_artist_genres) + self.verticalLayout_8.addWidget(self.use_artist_genres) self.horizontalLayout_4 = QtWidgets.QHBoxLayout() self.horizontalLayout_4.setObjectName("horizontalLayout_4") spacerItem = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum) self.horizontalLayout_4.addItem(spacerItem) - self.use_artist_only_if_no_release = QtWidgets.QCheckBox(self.wikidata_vert_layout) + self.use_artist_only_if_no_release = QtWidgets.QCheckBox(self.groupBox_2) self.use_artist_only_if_no_release.setEnabled(False) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -56,12 +65,12 @@ def setupUi(self, WikidataOptionsPage): self.use_artist_only_if_no_release.setChecked(False) self.use_artist_only_if_no_release.setObjectName("use_artist_only_if_no_release") self.horizontalLayout_4.addWidget(self.use_artist_only_if_no_release) - self.verticalLayout_2.addLayout(self.horizontalLayout_4) + self.verticalLayout_8.addLayout(self.horizontalLayout_4) self.horizontalLayout_2 = QtWidgets.QHBoxLayout() self.horizontalLayout_2.setObjectName("horizontalLayout_2") spacerItem1 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum) self.horizontalLayout_2.addItem(spacerItem1) - self.ignore_genres_from_these_artists_label = QtWidgets.QLabel(self.wikidata_vert_layout) + self.ignore_genres_from_these_artists_label = QtWidgets.QLabel(self.groupBox_2) self.ignore_genres_from_these_artists_label.setEnabled(False) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -70,34 +79,64 @@ def setupUi(self, WikidataOptionsPage): self.ignore_genres_from_these_artists_label.setSizePolicy(sizePolicy) self.ignore_genres_from_these_artists_label.setObjectName("ignore_genres_from_these_artists_label") self.horizontalLayout_2.addWidget(self.ignore_genres_from_these_artists_label) - self.verticalLayout_2.addLayout(self.horizontalLayout_2) + self.verticalLayout_8.addLayout(self.horizontalLayout_2) self.horizontalLayout_3 = QtWidgets.QHBoxLayout() self.horizontalLayout_3.setObjectName("horizontalLayout_3") spacerItem2 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum) self.horizontalLayout_3.addItem(spacerItem2) - self.ignore_genres_from_these_artists = QtWidgets.QLineEdit(self.wikidata_vert_layout) + self.ignore_genres_from_these_artists = QtWidgets.QLineEdit(self.groupBox_2) self.ignore_genres_from_these_artists.setEnabled(False) self.ignore_genres_from_these_artists.setObjectName("ignore_genres_from_these_artists") self.horizontalLayout_3.addWidget(self.ignore_genres_from_these_artists) - self.verticalLayout_2.addLayout(self.horizontalLayout_3) - self.line_2 = QtWidgets.QFrame(self.wikidata_vert_layout) - self.line_2.setFrameShape(QtWidgets.QFrame.HLine) - self.line_2.setFrameShadow(QtWidgets.QFrame.Sunken) - self.line_2.setObjectName("line_2") - self.verticalLayout_2.addWidget(self.line_2) + self.verticalLayout_8.addLayout(self.horizontalLayout_3) + self.verticalLayout_2.addWidget(self.groupBox_2) + self.wikidata_vert_layout = QtWidgets.QGroupBox(WikidataOptionsPage) + self.wikidata_vert_layout.setEnabled(True) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.wikidata_vert_layout.sizePolicy().hasHeightForWidth()) + self.wikidata_vert_layout.setSizePolicy(sizePolicy) + self.wikidata_vert_layout.setObjectName("wikidata_vert_layout") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.wikidata_vert_layout) + self.verticalLayout_3.setObjectName("verticalLayout_3") self.use_work_genres = QtWidgets.QCheckBox(self.wikidata_vert_layout) self.use_work_genres.setChecked(True) self.use_work_genres.setObjectName("use_work_genres") - self.verticalLayout_2.addWidget(self.use_work_genres) - self.line_3 = QtWidgets.QFrame(self.wikidata_vert_layout) - self.line_3.setFrameShape(QtWidgets.QFrame.HLine) - self.line_3.setFrameShadow(QtWidgets.QFrame.Sunken) - self.line_3.setObjectName("line_3") - self.verticalLayout_2.addWidget(self.line_3) + self.verticalLayout_3.addWidget(self.use_work_genres) + self.verticalLayout_2.addWidget(self.wikidata_vert_layout) + self.groupBox_3 = QtWidgets.QGroupBox(WikidataOptionsPage) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.groupBox_3.sizePolicy().hasHeightForWidth()) + self.groupBox_3.setSizePolicy(sizePolicy) + self.groupBox_3.setMinimumSize(QtCore.QSize(0, 120)) + self.groupBox_3.setObjectName("groupBox_3") + self.verticalLayout_9 = QtWidgets.QVBoxLayout(self.groupBox_3) + self.verticalLayout_9.setObjectName("verticalLayout_9") + self.verticalLayout = QtWidgets.QVBoxLayout() + self.verticalLayout.setObjectName("verticalLayout") + self.line_4 = QtWidgets.QFrame(self.groupBox_3) + self.line_4.setFrameShape(QtWidgets.QFrame.HLine) + self.line_4.setFrameShadow(QtWidgets.QFrame.Sunken) + self.line_4.setObjectName("line_4") + self.verticalLayout.addWidget(self.line_4) + self.ignore_genres_label = QtWidgets.QLabel(self.groupBox_3) + self.ignore_genres_label.setObjectName("ignore_genres_label") + self.verticalLayout.addWidget(self.ignore_genres_label) + self.ignore_these_genres = QtWidgets.QLineEdit(self.groupBox_3) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.ignore_these_genres.sizePolicy().hasHeightForWidth()) + self.ignore_these_genres.setSizePolicy(sizePolicy) + self.ignore_these_genres.setObjectName("ignore_these_genres") + self.verticalLayout.addWidget(self.ignore_these_genres) self.horizontalLayout_5 = QtWidgets.QHBoxLayout() self.horizontalLayout_5.setObjectName("horizontalLayout_5") - self.genre_delimiter_label = QtWidgets.QLabel(self.wikidata_vert_layout) - self.genre_delimiter_label.setEnabled(True) + self.genre_delimiter_label = QtWidgets.QLabel(self.groupBox_3) + self.genre_delimiter_label.setEnabled(False) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -106,8 +145,8 @@ def setupUi(self, WikidataOptionsPage): self.genre_delimiter_label.setWordWrap(False) self.genre_delimiter_label.setObjectName("genre_delimiter_label") self.horizontalLayout_5.addWidget(self.genre_delimiter_label) - self.genre_delimiter = QtWidgets.QComboBox(self.wikidata_vert_layout) - self.genre_delimiter.setEnabled(True) + self.genre_delimiter = QtWidgets.QComboBox(self.groupBox_3) + self.genre_delimiter.setEnabled(False) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -123,41 +162,20 @@ def setupUi(self, WikidataOptionsPage): self.genre_delimiter.addItem("") self.genre_delimiter.addItem("") self.genre_delimiter.setItemText(4, ", ") - self.genre_delimiter.addItem("") - self.genre_delimiter.setItemText(5, "") self.horizontalLayout_5.addWidget(self.genre_delimiter) - spacerItem3 = QtWidgets.QSpacerItem(150, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + spacerItem3 = QtWidgets.QSpacerItem(10, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum) self.horizontalLayout_5.addItem(spacerItem3) - self.verticalLayout_2.addLayout(self.horizontalLayout_5) - self.verticalLayout = QtWidgets.QVBoxLayout() - self.verticalLayout.setObjectName("verticalLayout") - self.line_4 = QtWidgets.QFrame(self.wikidata_vert_layout) - self.line_4.setFrameShape(QtWidgets.QFrame.HLine) - self.line_4.setFrameShadow(QtWidgets.QFrame.Sunken) - self.line_4.setObjectName("line_4") - self.verticalLayout.addWidget(self.line_4) - self.ignore_genres_label = QtWidgets.QLabel(self.wikidata_vert_layout) - self.ignore_genres_label.setObjectName("ignore_genres_label") - self.verticalLayout.addWidget(self.ignore_genres_label) - self.ignore_these_genres = QtWidgets.QLineEdit(self.wikidata_vert_layout) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.ignore_these_genres.sizePolicy().hasHeightForWidth()) - self.ignore_these_genres.setSizePolicy(sizePolicy) - self.ignore_these_genres.setObjectName("ignore_these_genres") - self.verticalLayout.addWidget(self.ignore_these_genres) - self.verticalLayout_2.addLayout(self.verticalLayout) - self.use_release_group_genres.raise_() - self.use_work_genres.raise_() - self.line_2.raise_() - self.line_3.raise_() - self.verticalLayout_4.addWidget(self.wikidata_vert_layout) - self.verticalLayout_3 = QtWidgets.QVBoxLayout() - self.verticalLayout_3.setObjectName("verticalLayout_3") - spacerItem4 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout_3.addItem(spacerItem4) - self.verticalLayout_4.addLayout(self.verticalLayout_3) + self.genre_delimiter_note = QtWidgets.QLabel(self.groupBox_3) + self.genre_delimiter_note.setEnabled(False) + self.genre_delimiter_note.setObjectName("genre_delimiter_note") + self.horizontalLayout_5.addWidget(self.genre_delimiter_note) + spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.horizontalLayout_5.addItem(spacerItem4) + self.verticalLayout.addLayout(self.horizontalLayout_5) + self.verticalLayout_9.addLayout(self.verticalLayout) + self.verticalLayout_2.addWidget(self.groupBox_3) + spacerItem5 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout_2.addItem(spacerItem5) self.retranslateUi(WikidataOptionsPage) self.use_artist_genres.toggled['bool'].connect(self.ignore_genres_from_these_artists_label.setEnabled) @@ -167,15 +185,20 @@ def setupUi(self, WikidataOptionsPage): def retranslateUi(self, WikidataOptionsPage): _translate = QtCore.QCoreApplication.translate + self.groupBox.setTitle(_translate("WikidataOptionsPage", "Release Group Genre Settings")) self.use_release_group_genres.setText(_translate("WikidataOptionsPage", "Use Release Group genres")) + self.groupBox_2.setTitle(_translate("WikidataOptionsPage", "Artist Genre Settings")) self.use_artist_genres.setText(_translate("WikidataOptionsPage", "Use Artist genres")) self.use_artist_only_if_no_release.setText(_translate("WikidataOptionsPage", "Use Artist genres only if no Release Group genres exist")) - self.ignore_genres_from_these_artists_label.setText(_translate("WikidataOptionsPage", "Ignore genres from these Artist: (comma separated regular expressions)")) + self.ignore_genres_from_these_artists_label.setText(_translate("WikidataOptionsPage", "Ignore Artist genres from these Artist(s): (comma separated regular expressions)")) + self.wikidata_vert_layout.setTitle(_translate("WikidataOptionsPage", "Work Genre Settings")) self.use_work_genres.setText(_translate("WikidataOptionsPage", "Use Work genres, when applicable")) + self.groupBox_3.setTitle(_translate("WikidataOptionsPage", "General Settings")) + self.ignore_genres_label.setText(_translate("WikidataOptionsPage", "Ignore these genres: (comma separated regular expressions)")) self.genre_delimiter_label.setText(_translate("WikidataOptionsPage", "Genre Delimiter:")) self.genre_delimiter.setItemText(1, _translate("WikidataOptionsPage", "; ")) self.genre_delimiter.setItemText(3, _translate("WikidataOptionsPage", " ; ")) - self.ignore_genres_label.setText(_translate("WikidataOptionsPage", "Ignore these genres: (comma separated regular expressions)")) + self.genre_delimiter_note.setText(_translate("WikidataOptionsPage", "(only applicable to ID3v2.3 tags)")) if __name__ == "__main__": From 8b73bf782534dfef772b13e24941d3253e4dd441 Mon Sep 17 00:00:00 2001 From: Wieland Hoffmann Date: Tue, 1 Jan 2019 11:50:37 +0100 Subject: [PATCH 080/123] Revert "Disable doctests for addrelease.py" This reverts commit a63ff909135072cdcb6ffe3688c537b13c12598f. Picard 2.1 can be installed & imported from PyPI. --- .travis.yml | 2 ++ test.py | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/.travis.yml b/.travis.yml index d31c641f..b0caee80 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,4 +6,6 @@ python: - "3.5" - "3.6" - "3.7" +before_install: + - pip3 install picard script: python test.py diff --git a/test.py b/test.py index 3bd1f762..272343ed 100644 --- a/test.py +++ b/test.py @@ -1,3 +1,4 @@ +import doctest import os import glob import json @@ -87,5 +88,11 @@ def test_valid_json(self): self.assertIsInstance(data['version'], str) +def load_tests(loader, tests, ignore): + from plugins.addrelease import addrelease + tests.addTests(doctest.DocTestSuite(addrelease)) + return tests + + if __name__ == '__main__': unittest.main() From 36bba24bd437689368267f453990f29d0ea29a7d Mon Sep 17 00:00:00 2001 From: evandrocoan Date: Tue, 1 Jan 2019 09:10:46 -0200 Subject: [PATCH 081/123] Fixed misspellings --- plugins/add_album_column/__init__.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/plugins/add_album_column/__init__.py b/plugins/add_album_column/__init__.py index f4fbecbd..528e005d 100644 --- a/plugins/add_album_column/__init__.py +++ b/plugins/add_album_column/__init__.py @@ -3,8 +3,22 @@ # # Licensing # -# Channel Manager Main, Create and maintain channel files -# Copyright (C) 2017 Evandro Coan +# Add Album Column, Add the Album column to the main window panel +# Copyright (C) 2019 Evandro Coan +# +# Redistributions of source code must retain the above +# copyright notice, this list of conditions and the +# following disclaimer. +# +# Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials +# provided with the distribution. +# +# Neither the name Evandro Coan nor the names of any +# contributors may be used to endorse or promote products +# derived from this software without specific prior written +# permission. # # This program is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the @@ -26,7 +40,7 @@ PLUGIN_VERSION = "1.0" PLUGIN_API_VERSIONS = ["2.0"] -PLUGIN_LICENSE = "GPLv3" +PLUGIN_LICENSE = "GPL-3.0-or-later" PLUGIN_LICENSE_URL = "http://www.gnu.org/licenses/" from picard.ui.itemviews import MainPanel From 8e32bc44c3bb79517b4872583fb7ab70237c65f8 Mon Sep 17 00:00:00 2001 From: Wieland Hoffmann Date: Tue, 1 Jan 2019 14:25:02 +0100 Subject: [PATCH 082/123] Generate data for the 2.0 branch by default The master branch doesn't even exist anymore. --- generate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generate.py b/generate.py index 590be619..e35ce46c 100755 --- a/generate.py +++ b/generate.py @@ -12,7 +12,7 @@ from get_plugin_data import get_plugin_data VERSION_TO_BRANCH = { - None: 'master', + None: '2.0', '1.0': 'master', '2.0': '2.0', } From 6e1cf6a55a014d880514bbc58cfd540ab973a163 Mon Sep 17 00:00:00 2001 From: Wieland Hoffmann Date: Tue, 1 Jan 2019 14:26:14 +0100 Subject: [PATCH 083/123] Generate data from the 1.0 branch for Picard 1.0 --- generate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generate.py b/generate.py index e35ce46c..415a571e 100755 --- a/generate.py +++ b/generate.py @@ -13,7 +13,7 @@ VERSION_TO_BRANCH = { None: '2.0', - '1.0': 'master', + '1.0': '1.0', '2.0': '2.0', } From f777a2aaa05bee33464236053582e51afa3e2672 Mon Sep 17 00:00:00 2001 From: Wieland Hoffmann Date: Tue, 1 Jan 2019 14:28:29 +0100 Subject: [PATCH 084/123] Use check_call call() just ignored any and all errors from the subprocesses. check_call() raises an exception at the point where the error happens. --- generate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/generate.py b/generate.py index 415a571e..1c80f67a 100755 --- a/generate.py +++ b/generate.py @@ -7,7 +7,7 @@ import zipfile from hashlib import md5 -from subprocess import call +from subprocess import check_call from get_plugin_data import get_plugin_data @@ -106,12 +106,12 @@ def zip_files(dest_dir): parser.add_argument('--no-zip', action='store_false', dest='zip', help="Do not generate the zip files in the build output") parser.add_argument('--no-json', action='store_false', dest='json', help="Do not generate the json file in the build output") args = parser.parse_args() - call(["git", "checkout", "-q", VERSION_TO_BRANCH[args.version], '--', 'plugins']) + check_call(["git", "checkout", "-q", VERSION_TO_BRANCH[args.version], '--', 'plugins']) dest_dir = os.path.abspath(os.path.join(args.build_dir, args.version or '')) if not os.path.exists(dest_dir): os.makedirs(dest_dir) if args.pull: - call(["git", "pull", "-q"]) + check_call(["git", "pull", "-q"]) if args.json: build_json(dest_dir) if args.zip: From 39a121f77169a4a58e233d3998b93c5bd805640b Mon Sep 17 00:00:00 2001 From: evandrocoan Date: Tue, 1 Jan 2019 11:57:18 -0200 Subject: [PATCH 085/123] Added WARNING: This plugin cannot be disabled #195 --- plugins/add_album_column/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/add_album_column/__init__.py b/plugins/add_album_column/__init__.py index 528e005d..f74a7bb6 100644 --- a/plugins/add_album_column/__init__.py +++ b/plugins/add_album_column/__init__.py @@ -36,7 +36,11 @@ PLUGIN_NAME = u"Add Album Column" PLUGIN_AUTHOR = u"Evandro Coan" -PLUGIN_DESCRIPTION = "Add the Album column to the main window panel." +PLUGIN_DESCRIPTION = """Add the Album column to the main window panel. + +WARNING: This plugin cannot be disabled. See: +https://github.com/metabrainz/picard-plugins/pull/195 +""" PLUGIN_VERSION = "1.0" PLUGIN_API_VERSIONS = ["2.0"] From 95a09b20c6125bf1034c1d5e3a2c4878dce47a14 Mon Sep 17 00:00:00 2001 From: Paul Brackin Date: Fri, 4 Jan 2019 14:15:44 -0800 Subject: [PATCH 086/123] change "type" parm to "item_type", check opposite case on id3v23 state during options init, some better QT var names, minor house cleaning change "type" parm to "item_type", check opposite case on id3v23 state during options init, some better QT var names, minor house cleaning for PEP compliance --- plugins/wikidata/__init__.py | 24 +-- plugins/wikidata/options_wikidata.ui | 215 ++++++++++++------------ plugins/wikidata/ui_options_wikidata.py | 164 +++++++++--------- 3 files changed, 198 insertions(+), 205 deletions(-) diff --git a/plugins/wikidata/__init__.py b/plugins/wikidata/__init__.py index 43d99f1e..19e9fbc4 100644 --- a/plugins/wikidata/__init__.py +++ b/plugins/wikidata/__init__.py @@ -56,7 +56,6 @@ class Wikidata: ARTIST = 2 WORK = 3 - def __init__(self): # Key: mbid, value: List of metadata entries to be updated when we have parsed everything self.requests = {} @@ -94,11 +93,11 @@ def process_release(self, album, metadata, release): item_id = dict.get(metadata, 'musicbrainz_releasegroupid')[0] log.info('WIKIDATA: Processing release group %s ' % item_id) - self.process_request(metadata, album, item_id, type='release-group') + self.process_request(metadata, album, item_id, item_type='release-group') for artist in dict.get(metadata, 'musicbrainz_albumartistid'): item_id = artist log.info('WIKIDATA: Processing release artist %s' % item_id) - self.process_request(metadata, album, item_id, type='artist') + self.process_request(metadata, album, item_id, item_type='artist') # Main processing function # First see if we have already found what we need in the cache, finalize loading @@ -107,10 +106,10 @@ def process_release(self, album, metadata, release): # Otherwise we are the first one to look up this item, start a new request # metadata, map containing the new metadata # - def process_request(self, metadata, album, item_id, type): + def process_request(self, metadata, album, item_id, item_type): log.debug('WIKIDATA: Looking up cache for item: %s' % item_id) log.debug('WIKIDATA: Album request count: %s' % album._requests) - log.debug('WIKIDATA: Item type %s' % type) + log.debug('WIKIDATA: Item type %s' % item_type) if item_id in self.cache: log.debug('WIKIDATA: Found item in cache') genre_list = self.cache[item_id] @@ -135,7 +134,7 @@ def process_request(self, metadata, album, item_id, type): log.debug('WIKIDATA: First request for this item') log.debug('WIKIDATA: About to call Musicbrainz to look up %s ' % item_id) - path = '/ws/2/%s/%s' % (type, item_id) + path = '/ws/2/%s/%s' % (item_type, item_id) queryargs = {"inc": "url-rels"} self.ws.get(self.mb_host, self.mb_port, path, partial(self.musicbrainz_release_lookup, item_id, @@ -259,7 +258,7 @@ def parse_wikidata_response(self, item, item_id, genre_source_type, response, re new_genre = set(old_genre_list) new_genre.update(genre_list) - # sort the new genre list so that they don't appear as new entries (not a change) next time + # Sort the new genre list so that they don't appear as new entries (not a change) next time log.debug('WIKIDATA: setting metadata genre to : %s ' % new_genre) if self.genre_delimiter: metadata["genre"] = self.genre_delimiter.join(sorted(new_genre)) @@ -292,22 +291,22 @@ def process_track(self, album, metadata, trackXmlNode, releaseXmlNode): if self.use_release_group_genres: for release_group in metadata.getall('musicbrainz_releasegroupid'): log.debug('WIKIDATA: Looking up release group metadata for %s ' % release_group) - self.process_request(metadata, album, release_group, type='release-group') + self.process_request(metadata, album, release_group, item_type='release-group') if self.use_artist_genres: for artist in metadata.getall('musicbrainz_albumartistid'): log.debug('WIKIDATA: Processing release artist %s' % artist) - self.process_request(metadata, album, artist, type='artist') + self.process_request(metadata, album, artist, item_type='artist') if self.use_artist_genres: for artist in metadata.getall('musicbrainz_artistid'): log.debug('WIKIDATA: Processing track artist %s' % artist) - self.process_request(metadata, album, artist, type='artist') + self.process_request(metadata, album, artist, item_type='artist') if self.use_work_genres: for workid in metadata.getall('musicbrainz_workid'): log.debug('WIKIDATA: Processing track artist %s' % workid) - self.process_request(metadata, album, workid, type='work') + self.process_request(metadata, album, workid, item_type='work') def update_settings(self): self.mb_host = config.setting["server_host"] @@ -363,6 +362,9 @@ def __init__(self, parent=None): if not config.setting["write_id3v23"]: self.ui.genre_delimiter.setEnabled(False); self.ui.genre_delimiter_label.setEnabled(False); + else: + self.ui.genre_delimiter.setEnabled(True); + self.ui.genre_delimiter_label.setEnabled(True); def info(self): pass diff --git a/plugins/wikidata/options_wikidata.ui b/plugins/wikidata/options_wikidata.ui index debcf569..d3f2c21f 100644 --- a/plugins/wikidata/options_wikidata.ui +++ b/plugins/wikidata/options_wikidata.ui @@ -12,7 +12,7 @@ - + true @@ -55,7 +55,7 @@ - + true @@ -83,7 +83,7 @@ - + @@ -125,7 +125,7 @@ - + @@ -161,7 +161,7 @@ - + @@ -191,7 +191,7 @@ - + true @@ -219,7 +219,7 @@ - + 0 @@ -235,22 +235,31 @@ General Settings - + - + + + Ignore these genres: (comma separated regular expressions) + + + + + - + Qt::Horizontal - - - - - - Ignore these genres: (comma separated regular expressions) + + QSizePolicy::Fixed - + + + 20 + 20 + + + @@ -262,108 +271,98 @@ + + + + + + false + + + + 0 + 0 + + + + Genre Delimiter: (only applicable to ID3v2.3 tags) + + + false + + + + + - + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 17 + + + + + + + + false + + + + 0 + 0 + + + + true + - - - false - - - - 0 - 0 - - - - Genre Delimiter: - - - false - - + + / + - - - false - - - - 0 - 0 - - - - true - - - - / - - - - - ; - - - - - ; - - - - - ; - - - - - , - - - + + ; + - - - Qt::Horizontal - - - QSizePolicy::Maximum - - - - 10 - 20 - - - + + ; + - - - false - - - (only applicable to ID3v2.3 tags) - - + + ; + - - - Qt::Horizontal - - - - 40 - 20 - - - + + , + - + + + + + + Qt::Horizontal + + + + 40 + 20 + + + diff --git a/plugins/wikidata/ui_options_wikidata.py b/plugins/wikidata/ui_options_wikidata.py index c16bb22f..3092c5c9 100644 --- a/plugins/wikidata/ui_options_wikidata.py +++ b/plugins/wikidata/ui_options_wikidata.py @@ -14,18 +14,18 @@ def setupUi(self, WikidataOptionsPage): WikidataOptionsPage.resize(602, 512) self.verticalLayout_2 = QtWidgets.QVBoxLayout(WikidataOptionsPage) self.verticalLayout_2.setObjectName("verticalLayout_2") - self.groupBox = QtWidgets.QGroupBox(WikidataOptionsPage) - self.groupBox.setEnabled(True) + self.releaseGroup_groupBox = QtWidgets.QGroupBox(WikidataOptionsPage) + self.releaseGroup_groupBox.setEnabled(True) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.groupBox.sizePolicy().hasHeightForWidth()) - self.groupBox.setSizePolicy(sizePolicy) - self.groupBox.setMinimumSize(QtCore.QSize(0, 0)) - self.groupBox.setObjectName("groupBox") - self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.groupBox) + sizePolicy.setHeightForWidth(self.releaseGroup_groupBox.sizePolicy().hasHeightForWidth()) + self.releaseGroup_groupBox.setSizePolicy(sizePolicy) + self.releaseGroup_groupBox.setMinimumSize(QtCore.QSize(0, 0)) + self.releaseGroup_groupBox.setObjectName("releaseGroup_groupBox") + self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.releaseGroup_groupBox) self.verticalLayout_4.setObjectName("verticalLayout_4") - self.use_release_group_genres = QtWidgets.QCheckBox(self.groupBox) + self.use_release_group_genres = QtWidgets.QCheckBox(self.releaseGroup_groupBox) self.use_release_group_genres.setEnabled(True) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -35,26 +35,26 @@ def setupUi(self, WikidataOptionsPage): self.use_release_group_genres.setChecked(True) self.use_release_group_genres.setObjectName("use_release_group_genres") self.verticalLayout_4.addWidget(self.use_release_group_genres) - self.verticalLayout_2.addWidget(self.groupBox) - self.groupBox_2 = QtWidgets.QGroupBox(WikidataOptionsPage) - self.groupBox_2.setEnabled(True) + self.verticalLayout_2.addWidget(self.releaseGroup_groupBox) + self.artist_groupBox = QtWidgets.QGroupBox(WikidataOptionsPage) + self.artist_groupBox.setEnabled(True) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.groupBox_2.sizePolicy().hasHeightForWidth()) - self.groupBox_2.setSizePolicy(sizePolicy) - self.groupBox_2.setMinimumSize(QtCore.QSize(0, 0)) - self.groupBox_2.setObjectName("groupBox_2") - self.verticalLayout_8 = QtWidgets.QVBoxLayout(self.groupBox_2) + sizePolicy.setHeightForWidth(self.artist_groupBox.sizePolicy().hasHeightForWidth()) + self.artist_groupBox.setSizePolicy(sizePolicy) + self.artist_groupBox.setMinimumSize(QtCore.QSize(0, 0)) + self.artist_groupBox.setObjectName("artist_groupBox") + self.verticalLayout_8 = QtWidgets.QVBoxLayout(self.artist_groupBox) self.verticalLayout_8.setObjectName("verticalLayout_8") - self.use_artist_genres = QtWidgets.QCheckBox(self.groupBox_2) + self.use_artist_genres = QtWidgets.QCheckBox(self.artist_groupBox) self.use_artist_genres.setObjectName("use_artist_genres") self.verticalLayout_8.addWidget(self.use_artist_genres) - self.horizontalLayout_4 = QtWidgets.QHBoxLayout() - self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.hLayout_use_artist_no_release = QtWidgets.QHBoxLayout() + self.hLayout_use_artist_no_release.setObjectName("hLayout_use_artist_no_release") spacerItem = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_4.addItem(spacerItem) - self.use_artist_only_if_no_release = QtWidgets.QCheckBox(self.groupBox_2) + self.hLayout_use_artist_no_release.addItem(spacerItem) + self.use_artist_only_if_no_release = QtWidgets.QCheckBox(self.artist_groupBox) self.use_artist_only_if_no_release.setEnabled(False) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -64,13 +64,13 @@ def setupUi(self, WikidataOptionsPage): self.use_artist_only_if_no_release.setCheckable(True) self.use_artist_only_if_no_release.setChecked(False) self.use_artist_only_if_no_release.setObjectName("use_artist_only_if_no_release") - self.horizontalLayout_4.addWidget(self.use_artist_only_if_no_release) - self.verticalLayout_8.addLayout(self.horizontalLayout_4) - self.horizontalLayout_2 = QtWidgets.QHBoxLayout() - self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.hLayout_use_artist_no_release.addWidget(self.use_artist_only_if_no_release) + self.verticalLayout_8.addLayout(self.hLayout_use_artist_no_release) + self.hLayout_ignore_genres_from_artists_label = QtWidgets.QHBoxLayout() + self.hLayout_ignore_genres_from_artists_label.setObjectName("hLayout_ignore_genres_from_artists_label") spacerItem1 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_2.addItem(spacerItem1) - self.ignore_genres_from_these_artists_label = QtWidgets.QLabel(self.groupBox_2) + self.hLayout_ignore_genres_from_artists_label.addItem(spacerItem1) + self.ignore_genres_from_these_artists_label = QtWidgets.QLabel(self.artist_groupBox) self.ignore_genres_from_these_artists_label.setEnabled(False) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -78,74 +78,74 @@ def setupUi(self, WikidataOptionsPage): sizePolicy.setHeightForWidth(self.ignore_genres_from_these_artists_label.sizePolicy().hasHeightForWidth()) self.ignore_genres_from_these_artists_label.setSizePolicy(sizePolicy) self.ignore_genres_from_these_artists_label.setObjectName("ignore_genres_from_these_artists_label") - self.horizontalLayout_2.addWidget(self.ignore_genres_from_these_artists_label) - self.verticalLayout_8.addLayout(self.horizontalLayout_2) - self.horizontalLayout_3 = QtWidgets.QHBoxLayout() - self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.hLayout_ignore_genres_from_artists_label.addWidget(self.ignore_genres_from_these_artists_label) + self.verticalLayout_8.addLayout(self.hLayout_ignore_genres_from_artists_label) + self.hLayout_ignore_genres_from_artists = QtWidgets.QHBoxLayout() + self.hLayout_ignore_genres_from_artists.setObjectName("hLayout_ignore_genres_from_artists") spacerItem2 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_3.addItem(spacerItem2) - self.ignore_genres_from_these_artists = QtWidgets.QLineEdit(self.groupBox_2) + self.hLayout_ignore_genres_from_artists.addItem(spacerItem2) + self.ignore_genres_from_these_artists = QtWidgets.QLineEdit(self.artist_groupBox) self.ignore_genres_from_these_artists.setEnabled(False) self.ignore_genres_from_these_artists.setObjectName("ignore_genres_from_these_artists") - self.horizontalLayout_3.addWidget(self.ignore_genres_from_these_artists) - self.verticalLayout_8.addLayout(self.horizontalLayout_3) - self.verticalLayout_2.addWidget(self.groupBox_2) - self.wikidata_vert_layout = QtWidgets.QGroupBox(WikidataOptionsPage) - self.wikidata_vert_layout.setEnabled(True) + self.hLayout_ignore_genres_from_artists.addWidget(self.ignore_genres_from_these_artists) + self.verticalLayout_8.addLayout(self.hLayout_ignore_genres_from_artists) + self.verticalLayout_2.addWidget(self.artist_groupBox) + self.work_groupBox = QtWidgets.QGroupBox(WikidataOptionsPage) + self.work_groupBox.setEnabled(True) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.wikidata_vert_layout.sizePolicy().hasHeightForWidth()) - self.wikidata_vert_layout.setSizePolicy(sizePolicy) - self.wikidata_vert_layout.setObjectName("wikidata_vert_layout") - self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.wikidata_vert_layout) + sizePolicy.setHeightForWidth(self.work_groupBox.sizePolicy().hasHeightForWidth()) + self.work_groupBox.setSizePolicy(sizePolicy) + self.work_groupBox.setObjectName("work_groupBox") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.work_groupBox) self.verticalLayout_3.setObjectName("verticalLayout_3") - self.use_work_genres = QtWidgets.QCheckBox(self.wikidata_vert_layout) + self.use_work_genres = QtWidgets.QCheckBox(self.work_groupBox) self.use_work_genres.setChecked(True) self.use_work_genres.setObjectName("use_work_genres") self.verticalLayout_3.addWidget(self.use_work_genres) - self.verticalLayout_2.addWidget(self.wikidata_vert_layout) - self.groupBox_3 = QtWidgets.QGroupBox(WikidataOptionsPage) + self.verticalLayout_2.addWidget(self.work_groupBox) + self.generalSettings_groupBox = QtWidgets.QGroupBox(WikidataOptionsPage) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.groupBox_3.sizePolicy().hasHeightForWidth()) - self.groupBox_3.setSizePolicy(sizePolicy) - self.groupBox_3.setMinimumSize(QtCore.QSize(0, 120)) - self.groupBox_3.setObjectName("groupBox_3") - self.verticalLayout_9 = QtWidgets.QVBoxLayout(self.groupBox_3) - self.verticalLayout_9.setObjectName("verticalLayout_9") - self.verticalLayout = QtWidgets.QVBoxLayout() + sizePolicy.setHeightForWidth(self.generalSettings_groupBox.sizePolicy().hasHeightForWidth()) + self.generalSettings_groupBox.setSizePolicy(sizePolicy) + self.generalSettings_groupBox.setMinimumSize(QtCore.QSize(0, 120)) + self.generalSettings_groupBox.setObjectName("generalSettings_groupBox") + self.verticalLayout = QtWidgets.QVBoxLayout(self.generalSettings_groupBox) self.verticalLayout.setObjectName("verticalLayout") - self.line_4 = QtWidgets.QFrame(self.groupBox_3) - self.line_4.setFrameShape(QtWidgets.QFrame.HLine) - self.line_4.setFrameShadow(QtWidgets.QFrame.Sunken) - self.line_4.setObjectName("line_4") - self.verticalLayout.addWidget(self.line_4) - self.ignore_genres_label = QtWidgets.QLabel(self.groupBox_3) + self.ignore_genres_label = QtWidgets.QLabel(self.generalSettings_groupBox) self.ignore_genres_label.setObjectName("ignore_genres_label") self.verticalLayout.addWidget(self.ignore_genres_label) - self.ignore_these_genres = QtWidgets.QLineEdit(self.groupBox_3) + self.hLayout_ignore_these_genres = QtWidgets.QHBoxLayout() + self.hLayout_ignore_these_genres.setObjectName("hLayout_ignore_these_genres") + spacerItem3 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum) + self.hLayout_ignore_these_genres.addItem(spacerItem3) + self.ignore_these_genres = QtWidgets.QLineEdit(self.generalSettings_groupBox) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.ignore_these_genres.sizePolicy().hasHeightForWidth()) self.ignore_these_genres.setSizePolicy(sizePolicy) self.ignore_these_genres.setObjectName("ignore_these_genres") - self.verticalLayout.addWidget(self.ignore_these_genres) - self.horizontalLayout_5 = QtWidgets.QHBoxLayout() - self.horizontalLayout_5.setObjectName("horizontalLayout_5") - self.genre_delimiter_label = QtWidgets.QLabel(self.groupBox_3) + self.hLayout_ignore_these_genres.addWidget(self.ignore_these_genres) + self.verticalLayout.addLayout(self.hLayout_ignore_these_genres) + self.genre_delimiter_label = QtWidgets.QLabel(self.generalSettings_groupBox) self.genre_delimiter_label.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.genre_delimiter_label.sizePolicy().hasHeightForWidth()) self.genre_delimiter_label.setSizePolicy(sizePolicy) self.genre_delimiter_label.setWordWrap(False) self.genre_delimiter_label.setObjectName("genre_delimiter_label") - self.horizontalLayout_5.addWidget(self.genre_delimiter_label) - self.genre_delimiter = QtWidgets.QComboBox(self.groupBox_3) + self.verticalLayout.addWidget(self.genre_delimiter_label) + self.hLayout_genre_delimiter = QtWidgets.QHBoxLayout() + self.hLayout_genre_delimiter.setObjectName("hLayout_genre_delimiter") + spacerItem4 = QtWidgets.QSpacerItem(20, 17, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum) + self.hLayout_genre_delimiter.addItem(spacerItem4) + self.genre_delimiter = QtWidgets.QComboBox(self.generalSettings_groupBox) self.genre_delimiter.setEnabled(False) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -162,20 +162,13 @@ def setupUi(self, WikidataOptionsPage): self.genre_delimiter.addItem("") self.genre_delimiter.addItem("") self.genre_delimiter.setItemText(4, ", ") - self.horizontalLayout_5.addWidget(self.genre_delimiter) - spacerItem3 = QtWidgets.QSpacerItem(10, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_5.addItem(spacerItem3) - self.genre_delimiter_note = QtWidgets.QLabel(self.groupBox_3) - self.genre_delimiter_note.setEnabled(False) - self.genre_delimiter_note.setObjectName("genre_delimiter_note") - self.horizontalLayout_5.addWidget(self.genre_delimiter_note) - spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.horizontalLayout_5.addItem(spacerItem4) - self.verticalLayout.addLayout(self.horizontalLayout_5) - self.verticalLayout_9.addLayout(self.verticalLayout) - self.verticalLayout_2.addWidget(self.groupBox_3) - spacerItem5 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.verticalLayout_2.addItem(spacerItem5) + self.hLayout_genre_delimiter.addWidget(self.genre_delimiter) + spacerItem5 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.hLayout_genre_delimiter.addItem(spacerItem5) + self.verticalLayout.addLayout(self.hLayout_genre_delimiter) + self.verticalLayout_2.addWidget(self.generalSettings_groupBox) + spacerItem6 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout_2.addItem(spacerItem6) self.retranslateUi(WikidataOptionsPage) self.use_artist_genres.toggled['bool'].connect(self.ignore_genres_from_these_artists_label.setEnabled) @@ -185,20 +178,19 @@ def setupUi(self, WikidataOptionsPage): def retranslateUi(self, WikidataOptionsPage): _translate = QtCore.QCoreApplication.translate - self.groupBox.setTitle(_translate("WikidataOptionsPage", "Release Group Genre Settings")) + self.releaseGroup_groupBox.setTitle(_translate("WikidataOptionsPage", "Release Group Genre Settings")) self.use_release_group_genres.setText(_translate("WikidataOptionsPage", "Use Release Group genres")) - self.groupBox_2.setTitle(_translate("WikidataOptionsPage", "Artist Genre Settings")) + self.artist_groupBox.setTitle(_translate("WikidataOptionsPage", "Artist Genre Settings")) self.use_artist_genres.setText(_translate("WikidataOptionsPage", "Use Artist genres")) self.use_artist_only_if_no_release.setText(_translate("WikidataOptionsPage", "Use Artist genres only if no Release Group genres exist")) self.ignore_genres_from_these_artists_label.setText(_translate("WikidataOptionsPage", "Ignore Artist genres from these Artist(s): (comma separated regular expressions)")) - self.wikidata_vert_layout.setTitle(_translate("WikidataOptionsPage", "Work Genre Settings")) + self.work_groupBox.setTitle(_translate("WikidataOptionsPage", "Work Genre Settings")) self.use_work_genres.setText(_translate("WikidataOptionsPage", "Use Work genres, when applicable")) - self.groupBox_3.setTitle(_translate("WikidataOptionsPage", "General Settings")) + self.generalSettings_groupBox.setTitle(_translate("WikidataOptionsPage", "General Settings")) self.ignore_genres_label.setText(_translate("WikidataOptionsPage", "Ignore these genres: (comma separated regular expressions)")) - self.genre_delimiter_label.setText(_translate("WikidataOptionsPage", "Genre Delimiter:")) + self.genre_delimiter_label.setText(_translate("WikidataOptionsPage", "Genre Delimiter: (only applicable to ID3v2.3 tags)")) self.genre_delimiter.setItemText(1, _translate("WikidataOptionsPage", "; ")) self.genre_delimiter.setItemText(3, _translate("WikidataOptionsPage", " ; ")) - self.genre_delimiter_note.setText(_translate("WikidataOptionsPage", "(only applicable to ID3v2.3 tags)")) if __name__ == "__main__": From 3345236bec81a5994aa19cca17356673872ebc1a Mon Sep 17 00:00:00 2001 From: David Mandelberg Date: Tue, 22 Jan 2019 20:46:49 -0500 Subject: [PATCH 087/123] reorder_sides: Switch from the XML API to JSON. --- plugins/reorder_sides/reorder_sides.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/reorder_sides/reorder_sides.py b/plugins/reorder_sides/reorder_sides.py index d486a3bd..14958e0f 100644 --- a/plugins/reorder_sides/reorder_sides.py +++ b/plugins/reorder_sides/reorder_sides.py @@ -102,15 +102,15 @@ def get_side_info(release): side_info = collections.OrderedDict() - for medium in release.medium_list[0].medium: + for medium in release['media']: current_side = None - for track in medium.track_list[0].track: - tracknumber = track.children['number'][0].text + for track in medium['tracks']: + tracknumber = track['number'] trackside = tracknumber_to_side(tracknumber) try: - int_tracknumber = int(track.children['position'][0].text) + int_tracknumber = int(track['position']) except ValueError: # Non-integer tracknumber, so give up. return None @@ -137,7 +137,7 @@ def get_side_info(release): try: side_info[current_side] = [ - int(medium.children['position'][0].text), + int(medium['position']), int_tracknumber, int_tracknumber, ] From 16db54c3c64e20e862b7431313df2079acf4c11a Mon Sep 17 00:00:00 2001 From: David Mandelberg Date: Tue, 22 Jan 2019 20:50:20 -0500 Subject: [PATCH 088/123] reorder_sides: Replace the deprecated string_() function with str(). --- plugins/reorder_sides/reorder_sides.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/reorder_sides/reorder_sides.py b/plugins/reorder_sides/reorder_sides.py index 14958e0f..b4f570e3 100644 --- a/plugins/reorder_sides/reorder_sides.py +++ b/plugins/reorder_sides/reorder_sides.py @@ -215,12 +215,12 @@ def reorder_sides(tagger, metadata, *args): side_first_tracknumber = side_info[side][1] side_last_tracknumber = side_info[side][2] - metadata['totaldiscs'] = string_(len(all_sides)) - metadata['discnumber'] = string_(all_sides.index(side) + 1) + metadata['totaldiscs'] = str(len(all_sides)) + metadata['discnumber'] = str(all_sides.index(side) + 1) metadata['totaltracks'] = \ - string_(side_last_tracknumber - side_first_tracknumber + 1) + str(side_last_tracknumber - side_first_tracknumber + 1) metadata['tracknumber'] = \ - string_(int(metadata['tracknumber']) - side_first_tracknumber + 1) + str(int(metadata['tracknumber']) - side_first_tracknumber + 1) register_track_metadata_processor(reorder_sides) From ddcc66358faecfd9c23f3b88c2c94d3b11c3ea0a Mon Sep 17 00:00:00 2001 From: David Mandelberg Date: Tue, 22 Jan 2019 20:51:02 -0500 Subject: [PATCH 089/123] reorder_sides: Bump version number. --- plugins/reorder_sides/reorder_sides.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/reorder_sides/reorder_sides.py b/plugins/reorder_sides/reorder_sides.py index b4f570e3..6b96cdda 100644 --- a/plugins/reorder_sides/reorder_sides.py +++ b/plugins/reorder_sides/reorder_sides.py @@ -28,7 +28,7 @@ changers (https://en.wikipedia.org/wiki/Record_changer#Automatic_sequencing) play in the correct order.""" -PLUGIN_VERSION = '1.1' +PLUGIN_VERSION = '1.2' PLUGIN_API_VERSIONS = ['2.0'] PLUGIN_LICENSE = 'GPL-3.0-or-later' PLUGIN_LICENSE_URL = 'https://www.gnu.org/licenses/gpl-3.0.html' From b0169e0e4697899d7ca72ee15c8bc044a832c618 Mon Sep 17 00:00:00 2001 From: David Mandelberg Date: Tue, 22 Jan 2019 22:08:50 -0500 Subject: [PATCH 090/123] instruments: Add a new plugin to populate an ~instruments tag. Based on https://github.com/dseomn/miscellaneous/blob/b353167085f6e70d770d494bd780858fdaf50843/personal-picard-plugins/instruments.py --- plugins/instruments/instruments.py | 89 ++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 plugins/instruments/instruments.py diff --git a/plugins/instruments/instruments.py b/plugins/instruments/instruments.py new file mode 100644 index 00000000..98171cee --- /dev/null +++ b/plugins/instruments/instruments.py @@ -0,0 +1,89 @@ +# MusicBrainz Picard plugin to add an ~instruments tag. +# Copyright (C) 2019 David Mandelberg +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +PLUGIN_NAME = 'Instruments' +PLUGIN_AUTHOR = 'David Mandelberg' +PLUGIN_DESCRIPTION = """\ + Adds a multi-valued tag of all the instruments (including vocals), for use in + scripts. + """ +PLUGIN_VERSION = '1.0' +PLUGIN_API_VERSIONS = ['2.0'] +PLUGIN_LICENSE = 'GPL-3.0-or-later' +PLUGIN_LICENSE_URL = 'https://www.gnu.org/licenses/gpl-3.0.html' + +from typing import Generator, Optional + +from picard import metadata +from picard import plugin + + +def _iterate_instruments(instrument_list: str) -> Generator[str, None, None]: + """Yields individual instruments from a string listing them. + + Args: + instrument_list: List of instruments in the form 'A, B and C'. + """ + remaining = instrument_list + while remaining: + instrument, _, remaining = remaining.partition(', ') + if not remaining: + instrument, _, remaining = instrument.partition(' and ') + if ' and ' in remaining: + raise ValueError('Instrument list contains multiple \'and\'s: {!r}' + .format(instrument_list)) + yield instrument + + +def _strip_instrument_prefixes(instrument: str) -> Optional[str]: + """Returns the instrument name without qualifying prefixes, or None. + + Args: + instrument: Potentially prefixed instrument name, e.g., 'solo bassoon'. + + Returns: + The instrument name with all prefixes stripped, or None if there's nothing + other than prefixes. The all-prefixes case can happen with relationships + like 'guest performer'. + """ + instrument_prefixes = { + 'additional', + 'guest', + 'solo', + } + remaining = instrument + while remaining: + prefix, sep, remaining = remaining.partition(' ') + if prefix not in instrument_prefixes: + return ''.join((prefix, sep, remaining)) + return None + + +def add_instruments(tagger, metadata_, *args): + key_prefix = 'performer:' + instruments = set() + for key in metadata_.keys(): + if not key.startswith(key_prefix): + continue + for instrument in _iterate_instruments(key[len(key_prefix):]): + instrument = _strip_instrument_prefixes(instrument) + if instrument: + instruments.add(instrument) + metadata_['~instruments'] = list(instruments) + + +metadata.register_track_metadata_processor( + add_instruments, priority=plugin.PluginPriority.HIGH) From 069ab5dd27491456bf5054d5a0ea7f5a831b74fd Mon Sep 17 00:00:00 2001 From: Paul Brackin Date: Thu, 14 Feb 2019 13:47:54 -0800 Subject: [PATCH 091/123] remove def info(), make release_group_genre_sourced private --- plugins/wikidata/.__init__.py.un~ | Bin 0 -> 1580 bytes plugins/wikidata/__init__.py | 7 ++----- 2 files changed, 2 insertions(+), 5 deletions(-) create mode 100644 plugins/wikidata/.__init__.py.un~ diff --git a/plugins/wikidata/.__init__.py.un~ b/plugins/wikidata/.__init__.py.un~ new file mode 100644 index 0000000000000000000000000000000000000000..f307857dd28329a2c029f5c4e05fa5bc036de64e GIT binary patch literal 1580 zcmWH`%$*;a=aT=FfoWNBa~Mm&>t|hO5_%6l+bo*4;&A49cP^9kP_8;Ri6}h=2F5N1 z1n>Yd8G)D?Dg>fom?0+hK?e&+mXRU;0#pGLlm<~CsrNuE2jct(0J3V z21f@2!&eEQ`R{=E127hPfix&8oD{Ht+|-i9l*E$6X!WAhoYchP)cEwG{L+H>^whkf z)cE53(xT+l6!lmITZNFK(p1w%a6}`g2%y0Xph*7+#GuqN8quIYpeX=GbRlv?10$K? z6H-KHVTosj%ru4K)SNWEQeYG(7L{Zcm&E7i Date: Tue, 12 Feb 2019 08:42:38 +0100 Subject: [PATCH 092/123] lastfm: Increase min_tag_usage default to 90% This gives more relevant results and filters out less used tags. Matches the default of Picard's builtin folksonomy tags support. --- plugins/lastfm/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/lastfm/__init__.py b/plugins/lastfm/__init__.py index f8c725d7..e0c36a58 100644 --- a/plugins/lastfm/__init__.py +++ b/plugins/lastfm/__init__.py @@ -3,7 +3,7 @@ PLUGIN_NAME = 'Last.fm' PLUGIN_AUTHOR = 'Lukáš Lalinský, Philipp Wolfer' PLUGIN_DESCRIPTION = 'Use tags from Last.fm as genre.' -PLUGIN_VERSION = "0.8" +PLUGIN_VERSION = "0.9" PLUGIN_API_VERSIONS = ["2.0"] import re @@ -212,7 +212,7 @@ class LastfmOptionsPage(OptionsPage): options = [ BoolOption("setting", "lastfm_use_track_tags", False), BoolOption("setting", "lastfm_use_artist_tags", False), - IntOption("setting", "lastfm_min_tag_usage", 15), + IntOption("setting", "lastfm_min_tag_usage", 90), TextOption("setting", "lastfm_ignore_tags", "seen live, favorites, /\\d+ of \\d+ stars/"), TextOption("setting", "lastfm_join_tags", ""), From 91907faa3bc88dc75e2ba8d0663893295e5a107e Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 16 Feb 2019 21:33:03 +0100 Subject: [PATCH 093/123] wikidata: Renamed to Wikidata Genre --- plugins/wikidata/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/wikidata/__init__.py b/plugins/wikidata/__init__.py index e27f2fa6..e71251eb 100644 --- a/plugins/wikidata/__init__.py +++ b/plugins/wikidata/__init__.py @@ -5,7 +5,7 @@ # terms of the Do What The Fuck You Want To Public License, Version 2, # as published by Sam Hocevar. See http://www.wtfpl.net/ for more details. -PLUGIN_NAME = 'wikidata-genre' +PLUGIN_NAME = 'Wikidata Genre' PLUGIN_AUTHOR = 'Daniel Sobey, Sambhav Kothari' PLUGIN_DESCRIPTION = 'query wikidata to get genre tags' PLUGIN_VERSION = '1.3' From 9f7b7ae3b4dcfde41501006285277034557522b1 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 17 Feb 2019 11:51:22 +0100 Subject: [PATCH 094/123] Amazon cover art provider plugin --- plugins/amazon/amazon.py | 131 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 plugins/amazon/amazon.py diff --git a/plugins/amazon/amazon.py b/plugins/amazon/amazon.py new file mode 100644 index 00000000..c7cbdad3 --- /dev/null +++ b/plugins/amazon/amazon.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# +# Picard, the next-generation MusicBrainz tagger +# Copyright (C) 2007 Oliver Charles +# Copyright (C) 2007-2011, 2019 Philipp Wolfer +# Copyright (C) 2007, 2010, 2011 Lukáš Lalinský +# Copyright (C) 2011 Michael Wiencek +# Copyright (C) 2011-2012 Wieland Hoffmann +# Copyright (C) 2013-2016 Laurent Monin +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +PLUGIN_NAME = 'Amazon cover art' +PLUGIN_AUTHOR = 'MusicBrainz Picard developers' +PLUGIN_DESCRIPTION = 'Use cover art from Amazon.' +PLUGIN_VERSION = "1.0" +PLUGIN_API_VERSIONS = ["2.1"] +PLUGIN_LICENSE = "GPL-2.0-or-later" +PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" + +from picard import log +from picard.coverart.image import CoverArtImage +from picard.coverart.providers import ( + CoverArtProvider, + register_cover_art_provider, +) +from picard.util import parse_amazon_url + +# amazon image file names are unique on all servers and constructed like +# ..[SML]ZZZZZZZ.jpg +# A release sold on amazon.de has always = 03, for example. +# Releases not sold on amazon.com, don't have a "01"-version of the image, +# so we need to make sure we grab an existing image. +AMAZON_SERVER = { + "amazon.jp": { + "server": "ec1.images-amazon.com", + "id": "09", + }, + "amazon.co.jp": { + "server": "ec1.images-amazon.com", + "id": "09", + }, + "amazon.co.uk": { + "server": "ec1.images-amazon.com", + "id": "02", + }, + "amazon.de": { + "server": "ec2.images-amazon.com", + "id": "03", + }, + "amazon.com": { + "server": "ec1.images-amazon.com", + "id": "01", + }, + "amazon.ca": { + "server": "ec1.images-amazon.com", + "id": "01", # .com and .ca are identical + }, + "amazon.fr": { + "server": "ec1.images-amazon.com", + "id": "08" + }, +} + +AMAZON_IMAGE_PATH = '/images/P/%(asin)s.%(serverid)s.%(size)s.jpg' + +# First item in the list will be tried first +AMAZON_SIZES = ( + # huge size option is only available for items + # that have a ZOOMing picture on its amazon web page + # and it doesn't work for all of the domain names + # '_SCRM_', # huge size + 'LZZZZZZZ', # large size, option format 1 + # '_SCLZZZZZZZ_', # large size, option format 3 + 'MZZZZZZZ', # default image size, format 1 + # '_SCMZZZZZZZ_', # medium size, option format 3 + # 'TZZZZZZZ', # medium image size, option format 1 + # '_SCTZZZZZZZ_', # small size, option format 3 + # 'THUMBZZZ', # small size, option format 1 +) + + +class CoverArtProviderAmazon(CoverArtProvider): + + """Use Amazon ASIN Musicbrainz relationships to get cover art""" + + NAME = "Amazon" + TITLE = N_('Amazon') + + def enabled(self): + return (super().enabled() + and not self.coverart.front_image_found) + + def queue_images(self): + self.match_url_relations(('amazon asin', 'has_Amazon_ASIN'), + self._queue_from_asin_relation) + return CoverArtProvider.FINISHED + + def _queue_from_asin_relation(self, url): + """Queue cover art images from Amazon""" + amz = parse_amazon_url(url) + if amz is None: + return + log.debug("Found ASIN relation : %s %s", amz['host'], amz['asin']) + if amz['host'] in AMAZON_SERVER: + serverInfo = AMAZON_SERVER[amz['host']] + else: + serverInfo = AMAZON_SERVER['amazon.com'] + host = serverInfo['server'] + for size in AMAZON_SIZES: + path = AMAZON_IMAGE_PATH % { + 'asin': amz['asin'], + 'serverid': serverInfo['id'], + 'size': size + } + self.queue_put(CoverArtImage("http://%s%s" % (host, path))) + + +register_cover_art_provider(CoverArtProviderAmazon) From 2ca997f2143a9634f69daf5c33b1c686488f4466 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 16 Feb 2019 14:36:51 +0100 Subject: [PATCH 095/123] TheAudioDB cover art plugin --- plugins/theaudiodb/__init__.py | 156 ++++++++++++++++++++ plugins/theaudiodb/options_theaudiodb.ui | 133 +++++++++++++++++ plugins/theaudiodb/ui_options_theaudiodb.py | 68 +++++++++ 3 files changed, 357 insertions(+) create mode 100644 plugins/theaudiodb/__init__.py create mode 100644 plugins/theaudiodb/options_theaudiodb.ui create mode 100644 plugins/theaudiodb/ui_options_theaudiodb.py diff --git a/plugins/theaudiodb/__init__.py b/plugins/theaudiodb/__init__.py new file mode 100644 index 00000000..e0865eda --- /dev/null +++ b/plugins/theaudiodb/__init__.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2015-2019 Philipp Wolfer +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + +PLUGIN_NAME = 'TheAudioDB cover art' +PLUGIN_AUTHOR = 'Philipp Wolfer' +PLUGIN_DESCRIPTION = 'Use cover art from TheAudioDB.' +PLUGIN_VERSION = "1.0" +PLUGIN_API_VERSIONS = ["2.0", "2.1"] +PLUGIN_LICENSE = "GPL-2.0-or-later" +PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" + +from PyQt5.QtCore import QUrl +from PyQt5.QtNetwork import QNetworkReply +from picard import config, log +from picard.coverart.providers import CoverArtProvider, register_cover_art_provider +from picard.coverart.image import CoverArtImage +from picard.ui.options import register_options_page, OptionsPage +from picard.config import TextOption +from picard.plugins.theaudiodb.ui_options_theaudiodb import Ui_TheAudioDbOptionsPage + +THEAUDIODB_HOST = "www.theaudiodb.com" +THEAUDIODB_PORT = 443 +THEAUDIODB_APIKEY = "195003" + +OPTION_CDART_ALWAYS = "always" +OPTION_CDART_NEVER = "never" +OPTION_CDART_NOALBUMART = "noalbumart" + + +class TheAudioDbCoverArtImage(CoverArtImage): + + """Image from The Audio DB""" + + support_types = True + sourceprefix = "AUDIODB" + + def parse_url(self, url): + super().parse_url(url) + # Workaround for Picard always returning port 80 regardless of the scheme + self.port = self.url.port(443 if self.url.scheme() == 'https' else 80) + + +class CoverArtProviderTheAudioDb(CoverArtProvider): + + """Use TheAudioDB to get cover art""" + + NAME = "TheAudioDB" + + def enabled(self): + return super().enabled() and not self.coverart.front_image_found + + def queue_images(self): + release_group_id = self.metadata["musicbrainz_releasegroupid"] + path = "/api/v1/json/%s/album-mb.php" % (THEAUDIODB_APIKEY, ) + queryargs = { + "i": bytes(QUrl.toPercentEncoding(release_group_id)).decode() + } + log.debug("CoverArtProviderTheAudioDb.queue_downloads: %s?i=%s" % (path, queryargs["i"])) + self.album.tagger.webservice.get( + THEAUDIODB_HOST, + THEAUDIODB_PORT, + path, + self._json_downloaded, + priority=True, + important=False, + parse_response_type='json', + queryargs=queryargs) + self.album._requests += 1 + return CoverArtProvider.WAIT + + def _json_downloaded(self, data, reply, error): + self.album._requests -= 1 + + if error: + if error != QNetworkReply.ContentNotFoundError: + error_level = log.error + else: + error_level = log.debug + error_level("Problem requesting metadata in TheAudioDB plugin: %s", error) + else: + try: + release = data["album"][0] + albumArtUrl = release.get("strAlbumThumb") + cdArtUrl = release.get("strAlbumCDart") + + if albumArtUrl: + self._select_and_add_cover_art(albumArtUrl, ["front"]) + + if cdArtUrl and \ + (config.setting["theaudiodb_use_cdart"] == OPTION_CDART_ALWAYS + or (config.setting["theaudiodb_use_cdart"] == OPTION_CDART_NOALBUMART + and not albumArtUrl)): + types = ["medium"] + if not albumArtUrl: + types.append("front") + self._select_and_add_cover_art(cdArtUrl, types) + except (AttributeError, KeyError, TypeError): + log.error("Problem processing downloaded metadata in TheAudioDB plugin: %s", exc_info=True) + + self.next_in_queue() + + def _select_and_add_cover_art(self, url, types): + log.debug("CoverArtProviderTheAudioDb found artwork %s" % url) + self.queue_put(TheAudioDbCoverArtImage(url, types=types)) + + +class TheAudioDbOptionsPage(OptionsPage): + + NAME = "theaudiodb" + TITLE = "TheAudioDB" + PARENT = "plugins" + + options = [ + TextOption("setting", "theaudiodb_use_cdart", OPTION_CDART_NOALBUMART), + ] + + def __init__(self, parent=None): + super().__init__(parent) + self.ui = Ui_TheAudioDbOptionsPage() + self.ui.setupUi(self) + + def load(self): + if config.setting["theaudiodb_use_cdart"] == OPTION_CDART_ALWAYS: + self.ui.theaudiodb_cdart_use_always.setChecked(True) + elif config.setting["theaudiodb_use_cdart"] == OPTION_CDART_NEVER: + self.ui.theaudiodb_cdart_use_never.setChecked(True) + elif config.setting["theaudiodb_use_cdart"] == OPTION_CDART_NOALBUMART: + self.ui.theaudiodb_cdart_use_if_no_albumcover.setChecked(True) + + def save(self): + if self.ui.theaudiodb_cdart_use_always.isChecked(): + config.setting["theaudiodb_use_cdart"] = OPTION_CDART_ALWAYS + elif self.ui.theaudiodb_cdart_use_never.isChecked(): + config.setting["theaudiodb_use_cdart"] = OPTION_CDART_NEVER + elif self.ui.theaudiodb_cdart_use_if_no_albumcover.isChecked(): + config.setting["theaudiodb_use_cdart"] = OPTION_CDART_NOALBUMART + + +register_cover_art_provider(CoverArtProviderTheAudioDb) +register_options_page(TheAudioDbOptionsPage) diff --git a/plugins/theaudiodb/options_theaudiodb.ui b/plugins/theaudiodb/options_theaudiodb.ui new file mode 100644 index 00000000..f811b4f7 --- /dev/null +++ b/plugins/theaudiodb/options_theaudiodb.ui @@ -0,0 +1,133 @@ + + + TheAudioDbOptionsPage + + + + 0 + 0 + 442 + 364 + + + + + 6 + + + 9 + + + 9 + + + 9 + + + 9 + + + + + TheAudioDB cover art + + + + 2 + + + 9 + + + 9 + + + 9 + + + 9 + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'Cantarell'; font-size:10pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">This plugin loads cover art from <a href="https://www.theaudiodb.com"><span style=" text-decoration: underline; color:#0000ff;">TheAudioDB</span></a>. If you want to improve the results of this plugin please contribute.</p></body></html> + + + Qt::RichText + + + true + + + true + + + + + + + + + + Medium images + + + + + + Always load medium images + + + + + + + Load only if no front cover is available + + + + + + + Never load medium images + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/plugins/theaudiodb/ui_options_theaudiodb.py b/plugins/theaudiodb/ui_options_theaudiodb.py new file mode 100644 index 00000000..b13fb8aa --- /dev/null +++ b/plugins/theaudiodb/ui_options_theaudiodb.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'options_theaduiodb.ui' +# +# Created by: PyQt5 UI code generator 5.12 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_TheAudioDbOptionsPage(object): + def setupUi(self, TheAudioDbOptionsPage): + TheAudioDbOptionsPage.setObjectName("TheAudioDbOptionsPage") + TheAudioDbOptionsPage.resize(442, 364) + self.vboxlayout = QtWidgets.QVBoxLayout(TheAudioDbOptionsPage) + self.vboxlayout.setContentsMargins(9, 9, 9, 9) + self.vboxlayout.setSpacing(6) + self.vboxlayout.setObjectName("vboxlayout") + self.groupBox = QtWidgets.QGroupBox(TheAudioDbOptionsPage) + self.groupBox.setObjectName("groupBox") + self.vboxlayout1 = QtWidgets.QVBoxLayout(self.groupBox) + self.vboxlayout1.setContentsMargins(9, 9, 9, 9) + self.vboxlayout1.setSpacing(2) + self.vboxlayout1.setObjectName("vboxlayout1") + self.label = QtWidgets.QLabel(self.groupBox) + self.label.setTextFormat(QtCore.Qt.RichText) + self.label.setWordWrap(True) + self.label.setOpenExternalLinks(True) + self.label.setObjectName("label") + self.vboxlayout1.addWidget(self.label) + self.vboxlayout.addWidget(self.groupBox) + self.verticalGroupBox = QtWidgets.QGroupBox(TheAudioDbOptionsPage) + self.verticalGroupBox.setObjectName("verticalGroupBox") + self.verticalLayout = QtWidgets.QVBoxLayout(self.verticalGroupBox) + self.verticalLayout.setObjectName("verticalLayout") + self.theaudiodb_cdart_use_always = QtWidgets.QRadioButton(self.verticalGroupBox) + self.theaudiodb_cdart_use_always.setObjectName("theaudiodb_cdart_use_always") + self.verticalLayout.addWidget(self.theaudiodb_cdart_use_always) + self.theaudiodb_cdart_use_if_no_albumcover = QtWidgets.QRadioButton(self.verticalGroupBox) + self.theaudiodb_cdart_use_if_no_albumcover.setObjectName("theaudiodb_cdart_use_if_no_albumcover") + self.verticalLayout.addWidget(self.theaudiodb_cdart_use_if_no_albumcover) + self.theaudiodb_cdart_use_never = QtWidgets.QRadioButton(self.verticalGroupBox) + self.theaudiodb_cdart_use_never.setObjectName("theaudiodb_cdart_use_never") + self.verticalLayout.addWidget(self.theaudiodb_cdart_use_never) + self.vboxlayout.addWidget(self.verticalGroupBox) + spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.vboxlayout.addItem(spacerItem) + spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.vboxlayout.addItem(spacerItem1) + + self.retranslateUi(TheAudioDbOptionsPage) + QtCore.QMetaObject.connectSlotsByName(TheAudioDbOptionsPage) + + def retranslateUi(self, TheAudioDbOptionsPage): + _translate = QtCore.QCoreApplication.translate + self.groupBox.setTitle(_translate("TheAudioDbOptionsPage", "TheAudioDB cover art")) + self.label.setText(_translate("TheAudioDbOptionsPage", "\n" +"\n" +"

    This plugin loads cover art from TheAudioDB. If you want to improve the results of this plugin please contribute.

    ")) + self.verticalGroupBox.setTitle(_translate("TheAudioDbOptionsPage", "Medium images")) + self.theaudiodb_cdart_use_always.setText(_translate("TheAudioDbOptionsPage", "Always load medium images")) + self.theaudiodb_cdart_use_if_no_albumcover.setText(_translate("TheAudioDbOptionsPage", "Load only if no front cover is available")) + self.theaudiodb_cdart_use_never.setText(_translate("TheAudioDbOptionsPage", "Never load medium images")) + + From 9574715a2067d8511dd6307e020294980d001f49 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 16 Feb 2019 21:43:46 +0100 Subject: [PATCH 096/123] fanarttv: Minor code cleanup --- plugins/fanarttv/__init__.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/plugins/fanarttv/__init__.py b/plugins/fanarttv/__init__.py index b9d3d049..d538b676 100644 --- a/plugins/fanarttv/__init__.py +++ b/plugins/fanarttv/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2015 Philipp Wolfer +# Copyright (C) 2015-2019 Philipp Wolfer # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -20,8 +20,8 @@ PLUGIN_NAME = 'fanart.tv cover art' PLUGIN_AUTHOR = 'Philipp Wolfer, Sambhav Kothari' PLUGIN_DESCRIPTION = 'Use cover art from fanart.tv. To use this plugin you have to register a personal API key on https://fanart.tv/get-an-api-key/' -PLUGIN_VERSION = "1.5" -PLUGIN_API_VERSIONS = ["2.0"] +PLUGIN_VERSION = "1.5.1" +PLUGIN_API_VERSIONS = ["2.0", "2.1"] PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" @@ -29,9 +29,15 @@ from PyQt5.QtCore import QUrl from PyQt5.QtNetwork import QNetworkReply from picard import config, log -from picard.coverart.providers import CoverArtProvider, register_cover_art_provider +from picard.coverart.providers import ( + CoverArtProvider, + register_cover_art_provider, +) from picard.coverart.image import CoverArtImage -from picard.ui.options import register_options_page, OptionsPage +from picard.ui.options import ( + register_options_page, + OptionsPage, +) from picard.config import TextOption from picard.plugins.fanarttv.ui_options_fanarttv import Ui_FanartTvOptionsPage @@ -54,7 +60,7 @@ def cover_sort_key(cover): class FanartTvCoverArtImage(CoverArtImage): - """Image from Cover Art Archive""" + """Image from fanart.tv""" support_types = True sourceprefix = "FATV" @@ -67,9 +73,8 @@ class CoverArtProviderFanartTv(CoverArtProvider): NAME = "fanart.tv" def enabled(self): - return self._client_key != "" and \ - super(CoverArtProviderFanartTv, self).enabled() and \ - not self.coverart.front_image_found + return (self._client_key and super().enabled() + and not self.coverart.front_image_found) def queue_images(self): release_group_id = self.metadata["musicbrainz_releasegroupid"] @@ -119,7 +124,7 @@ def _json_downloaded(self, release_group_id, data, reply, error): and "albumcover" not in release)): covers = release["cdart"] types = ["medium"] - if not "albumcover" in release: + if "albumcover" not in release: types.append("front") self._select_and_add_cover_art(covers, types) except (AttributeError, KeyError, TypeError): @@ -146,7 +151,7 @@ class FanartTvOptionsPage(OptionsPage): ] def __init__(self, parent=None): - super(FanartTvOptionsPage, self).__init__(parent) + super().__init__(parent) self.ui = Ui_FanartTvOptionsPage() self.ui.setupUi(self) From 1bab628b3776f6e17765e8b9758a30fdefeb7742 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sun, 17 Feb 2019 14:24:09 +0100 Subject: [PATCH 097/123] Amazon: Set API version to 2.2 this way the plugin is not usable on eralier versions of Picard where it does not make sense --- plugins/amazon/amazon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/amazon/amazon.py b/plugins/amazon/amazon.py index c7cbdad3..d2f0b90b 100644 --- a/plugins/amazon/amazon.py +++ b/plugins/amazon/amazon.py @@ -26,7 +26,7 @@ PLUGIN_AUTHOR = 'MusicBrainz Picard developers' PLUGIN_DESCRIPTION = 'Use cover art from Amazon.' PLUGIN_VERSION = "1.0" -PLUGIN_API_VERSIONS = ["2.1"] +PLUGIN_API_VERSIONS = ["2.2"] PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" From b830e9969c20521e12b88b010d6c991b07dc2516 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 18 Feb 2019 08:21:14 +0100 Subject: [PATCH 098/123] TheAudioDB: Handle empty result gracefully Do not rely on exception handling --- plugins/theaudiodb/__init__.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/plugins/theaudiodb/__init__.py b/plugins/theaudiodb/__init__.py index e0865eda..d826b3e2 100644 --- a/plugins/theaudiodb/__init__.py +++ b/plugins/theaudiodb/__init__.py @@ -20,7 +20,7 @@ PLUGIN_NAME = 'TheAudioDB cover art' PLUGIN_AUTHOR = 'Philipp Wolfer' PLUGIN_DESCRIPTION = 'Use cover art from TheAudioDB.' -PLUGIN_VERSION = "1.0" +PLUGIN_VERSION = "1.0.1" PLUGIN_API_VERSIONS = ["2.0", "2.1"] PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" @@ -95,7 +95,11 @@ def _json_downloaded(self, data, reply, error): error_level("Problem requesting metadata in TheAudioDB plugin: %s", error) else: try: - release = data["album"][0] + releases = data.get("album") + if not releases: + log.info("No cover art found on TheAudioDB for %s", reply.url().url()) + return + release = releases[0] albumArtUrl = release.get("strAlbumThumb") cdArtUrl = release.get("strAlbumCDart") @@ -110,10 +114,10 @@ def _json_downloaded(self, data, reply, error): if not albumArtUrl: types.append("front") self._select_and_add_cover_art(cdArtUrl, types) - except (AttributeError, KeyError, TypeError): + except (TypeError): log.error("Problem processing downloaded metadata in TheAudioDB plugin: %s", exc_info=True) - - self.next_in_queue() + finally: + self.next_in_queue() def _select_and_add_cover_art(self, url, types): log.debug("CoverArtProviderTheAudioDb found artwork %s" % url) From 5f162f979d87226a9b303c0e26b39cb3b5110ce1 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 18 Feb 2019 08:23:52 +0100 Subject: [PATCH 099/123] TheAudioDB: Unified log messages --- plugins/theaudiodb/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/theaudiodb/__init__.py b/plugins/theaudiodb/__init__.py index d826b3e2..eb11b18f 100644 --- a/plugins/theaudiodb/__init__.py +++ b/plugins/theaudiodb/__init__.py @@ -71,7 +71,7 @@ def queue_images(self): queryargs = { "i": bytes(QUrl.toPercentEncoding(release_group_id)).decode() } - log.debug("CoverArtProviderTheAudioDb.queue_downloads: %s?i=%s" % (path, queryargs["i"])) + log.debug("TheAudioDB: Queued download: %s?i=%s" % (path, queryargs["i"])) self.album.tagger.webservice.get( THEAUDIODB_HOST, THEAUDIODB_PORT, @@ -92,12 +92,12 @@ def _json_downloaded(self, data, reply, error): error_level = log.error else: error_level = log.debug - error_level("Problem requesting metadata in TheAudioDB plugin: %s", error) + error_level("TheAudioDB: Problem requesting metadata: %s", error) else: try: releases = data.get("album") if not releases: - log.info("No cover art found on TheAudioDB for %s", reply.url().url()) + log.debug("TheAudioDB: No cover art found for %s", reply.url().url()) return release = releases[0] albumArtUrl = release.get("strAlbumThumb") @@ -115,12 +115,12 @@ def _json_downloaded(self, data, reply, error): types.append("front") self._select_and_add_cover_art(cdArtUrl, types) except (TypeError): - log.error("Problem processing downloaded metadata in TheAudioDB plugin: %s", exc_info=True) + log.error("TheAudioDB: Problem processing downloaded metadata: %s", exc_info=True) finally: self.next_in_queue() def _select_and_add_cover_art(self, url, types): - log.debug("CoverArtProviderTheAudioDb found artwork %s" % url) + log.debug("TheAudioDB: Found artwork %s" % url) self.queue_put(TheAudioDbCoverArtImage(url, types=types)) From e88d65f2a689c50cfa457e45c16c7c4694053429 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Sat, 16 Feb 2019 21:04:24 +0100 Subject: [PATCH 100/123] BPM: Use a separate thread pool Otherwise the metadata box would not update on selection changes, since it used the same thread pool for updating the UI. Fixes #200 --- plugins/bpm/__init__.py | 91 ++++++++++++++++++++++++----------------- 1 file changed, 54 insertions(+), 37 deletions(-) diff --git a/plugins/bpm/__init__.py b/plugins/bpm/__init__.py index 58f1d010..3b47f41c 100644 --- a/plugins/bpm/__init__.py +++ b/plugins/bpm/__init__.py @@ -8,11 +8,11 @@ # PLUGIN_NAME = "BPM Analyzer" -PLUGIN_AUTHOR = "Len Joubert, Sambhav Kothari" +PLUGIN_AUTHOR = "Len Joubert, Sambhav Kothari, Philipp Wolfer" PLUGIN_DESCRIPTION = """Calculate BPM for selected files and albums. Linux only version with dependancy on Aubio and Numpy""" PLUGIN_LICENSE = "GPL-2.0" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" -PLUGIN_VERSION = "1.3" +PLUGIN_VERSION = "1.4" PLUGIN_API_VERSIONS = ["2.0"] # PLUGIN_INCOMPATIBLE_PLATFORMS = [ # 'win32', 'cygwyn', 'darwin', 'os2', 'os2emx', 'riscos', 'atheos'] @@ -21,6 +21,7 @@ from numpy import median, diff from collections import defaultdict from functools import partial +from PyQt5 import QtCore from subprocess import check_call from picard.album import Album, NatAlbum from picard.track import Track @@ -40,46 +41,26 @@ } -def get_file_bpm(self, path): - """ Calculate the beats per minute (bpm) of a given file. - path: path to the file - buf_size length of FFT - hop_size number of frames between two consecutive runs - samplerate sampling rate of the signal to analyze - """ - - samplerate, buf_size, hop_size = bpm_slider_settings[ - BPMOptionsPage.config.setting["bpm_slider_parameter"]] - mediasource = source(path, samplerate, hop_size) - samplerate = mediasource.samplerate - beattracking = tempo("specdiff", buf_size, hop_size, samplerate) - # List of beats, in samples - beats = [] - # Total number of frames read - total_frames = 0 - - while True: - samples, read = mediasource() - is_beat = beattracking(samples) - if is_beat: - this_beat = beattracking.get_last_s() - beats.append(this_beat) - total_frames += read - if read < hop_size: - break - - # Convert to periods and to bpm - bpms = 60. / diff(beats) - return median(bpms) - - class FileBPM(BaseAction): NAME = N_("Calculate BPM...") + def __init__(self): + super().__init__() + self._close = False + self.thread_pool = QtCore.QThreadPool(self) + self.tagger.aboutToQuit.connect(self._cleanup) + + def _cleanup(self): + if self._close: + return + self._close = True + self.thread_pool.waitForDone() + def _add_file_to_queue(self, file): thread.run_task( partial(self._calculate_bpm, file), - partial(self._calculate_bpm_callback, file)) + partial(self._calculate_bpm_callback, file), + thread_pool=self.thread_pool) def callback(self, objs): for obj in objs: @@ -94,8 +75,10 @@ def _calculate_bpm(self, file): N_('Calculating BPM for "%(filename)s"...'), {'filename': file.filename} ) - calculated_bpm = get_file_bpm(self.tagger, file.filename) + calculated_bpm = self._get_file_bpm(file.filename) # self.tagger.log.debug('%s' % (calculated_bpm)) + if self._close: + return file.metadata["bpm"] = str(round(calculated_bpm, 1)) file.update() @@ -111,6 +94,40 @@ def _calculate_bpm_callback(self, file, result=None, error=None): {'filename': file.filename} ) + def _get_file_bpm(self, path): + """ Calculate the beats per minute (bpm) of a given file. + path: path to the file + buf_size length of FFT + hop_size number of frames between two consecutive runs + samplerate sampling rate of the signal to analyze + """ + + samplerate, buf_size, hop_size = bpm_slider_settings[ + BPMOptionsPage.config.setting["bpm_slider_parameter"]] + mediasource = source(path, samplerate, hop_size) + samplerate = mediasource.samplerate + beattracking = tempo("specdiff", buf_size, hop_size, samplerate) + # List of beats, in samples + beats = [] + # Total number of frames read + total_frames = 0 + + while True: + if self._close: + return + samples, read = mediasource() + is_beat = beattracking(samples) + if is_beat: + this_beat = beattracking.get_last_s() + beats.append(this_beat) + total_frames += read + if read < hop_size: + break + + # Convert to periods and to bpm + bpms = 60. / diff(beats) + return median(bpms) + class BPMOptionsPage(OptionsPage): From 7c58103cad5d95faf279eac5a38b6dcf90bf90fb Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 18 Feb 2019 17:37:20 +0100 Subject: [PATCH 101/123] BPM: Do not use separate thread pool, but close threads on application close This will work well with the updated implementation in Picard 2.1.3 while still giving the benefit of quick closing of the application. --- plugins/bpm/__init__.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/plugins/bpm/__init__.py b/plugins/bpm/__init__.py index 3b47f41c..4f319285 100644 --- a/plugins/bpm/__init__.py +++ b/plugins/bpm/__init__.py @@ -21,7 +21,6 @@ from numpy import median, diff from collections import defaultdict from functools import partial -from PyQt5 import QtCore from subprocess import check_call from picard.album import Album, NatAlbum from picard.track import Track @@ -47,20 +46,15 @@ class FileBPM(BaseAction): def __init__(self): super().__init__() self._close = False - self.thread_pool = QtCore.QThreadPool(self) self.tagger.aboutToQuit.connect(self._cleanup) def _cleanup(self): - if self._close: - return self._close = True - self.thread_pool.waitForDone() def _add_file_to_queue(self, file): thread.run_task( partial(self._calculate_bpm, file), - partial(self._calculate_bpm_callback, file), - thread_pool=self.thread_pool) + partial(self._calculate_bpm_callback, file)) def callback(self, objs): for obj in objs: From c4a9873e1a5ed4a7905c1f7a2d3127d323498b59 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 1 Mar 2019 18:32:10 +0100 Subject: [PATCH 102/123] musixmatch: Use Picard's webservice implementation --- plugins/musixmatch/__init__.py | 88 ++++++--- plugins/musixmatch/musixmatch/__init__.py | 7 - plugins/musixmatch/musixmatch/track.py | 187 ------------------ plugins/musixmatch/musixmatch/util.py | 224 ---------------------- 4 files changed, 58 insertions(+), 448 deletions(-) delete mode 100644 plugins/musixmatch/musixmatch/__init__.py delete mode 100644 plugins/musixmatch/musixmatch/track.py delete mode 100644 plugins/musixmatch/musixmatch/util.py diff --git a/plugins/musixmatch/__init__.py b/plugins/musixmatch/__init__.py index 738c2298..74247ec1 100644 --- a/plugins/musixmatch/__init__.py +++ b/plugins/musixmatch/__init__.py @@ -1,24 +1,76 @@ PLUGIN_NAME = 'Musixmatch Lyrics' -PLUGIN_AUTHOR = 'm-yn, Sambhav Kothari' +PLUGIN_AUTHOR = 'm-yn, Sambhav Kothari, Philipp Wolfer' PLUGIN_DESCRIPTION = 'Fetch first 30% of lyrics from Musixmatch' -PLUGIN_VERSION = '1.0' +PLUGIN_VERSION = '1.1' PLUGIN_API_VERSIONS = ["2.0"] PLUGIN_LICENSE = "GPL-2.0" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" +from functools import partial +from picard import config, log from picard.metadata import register_track_metadata_processor from picard.ui.options import register_options_page, OptionsPage -from picard.config import TextOption from .ui_options_musixmatch import Ui_MusixmatchOptionsPage +MUSIXMATCH_HOST = 'api.musixmatch.com' +MUSIXMATCH_PORT = 80 + + +def handle_result(album, metadata, data, reply, error): + try: + if error: + log.error(error) + return + message = data.get('message', {}) + header = message.get('header') + if header.get('status_code') != 200: + log.warning('MusixMatch: Server returned no result: %s', data) + return + result = message.get('body', {}).get('lyrics') + if result: + lyrics = result.get('lyrics_body') + if lyrics: + metadata['lyrics:description'] = lyrics + except AttributeError: + log.error('MusixMatch: Error handling server response %s', + data, exc_info=True) + finally: + album._requests -= 1 + album._finalize_loading(error) + + +def process_track(album, metadata, release, track): + apikey = config.setting['musixmatch_api_key'] + if not apikey: + log.warning('MusixMatch: No API key configured') + return + if metadata['language'] == 'zxx': + log.debug('MusixMatch: Track %s has no lyrics, skipping query', + metadata['musicbrainz_recordingid']) + return + queryargs = { + 'apikey': apikey, + 'track_mbid': metadata['musicbrainz_recordingid'] + } + album.tagger.webservice.get( + MUSIXMATCH_HOST, + MUSIXMATCH_PORT, + "/ws/1.1/track.lyrics.get", + partial(handle_result, album, metadata), + parse_response_type='json', + queryargs=queryargs + ) + album._requests += 1 + + class MusixmatchOptionsPage(OptionsPage): NAME = 'musixmatch' TITLE = 'Musixmatch API Key' PARENT = "plugins" options = [ - TextOption("setting", "musixmatch_api_key", "") + config.TextOption("setting", "musixmatch_api_key", "") ] def __init__(self, parent=None): @@ -27,35 +79,11 @@ def __init__(self, parent=None): self.ui.setupUi(self) def load(self): - self.ui.api_key.setText(self.config.setting["musixmatch_api_key"]) + self.ui.api_key.setText(config.setting["musixmatch_api_key"]) def save(self): self.config.setting["musixmatch_api_key"] = self.ui.api_key.text() -register_options_page(MusixmatchOptionsPage) - -import picard.tagger as tagger -import os -try: - os.environ['MUSIXMATCH_API_KEY'] = tagger.config.setting[ - "musixmatch_api_key"] -except: - pass -from .musixmatch import track as TRACK - - -def process_track(album, metadata, release, track): - if('MUSIXMATCH_API_KEY' not in os.environ): - return - try: - t = TRACK.Track(metadata['musicbrainz_trackid'], - musicbrainz=True).lyrics() - if t['instrumental'] == 1: - lyrics = "[Instrumental]" - else: - lyrics = t['lyrics_body'] - metadata['lyrics:description'] = lyrics - except Exception as e: - pass register_track_metadata_processor(process_track) +register_options_page(MusixmatchOptionsPage) diff --git a/plugins/musixmatch/musixmatch/__init__.py b/plugins/musixmatch/musixmatch/__init__.py deleted file mode 100644 index 0c8fd4b4..00000000 --- a/plugins/musixmatch/musixmatch/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -PLUGIN_NAME = 'Musixmatch Lyrics' -PLUGIN_AUTHOR = 'm-yn, Sambhav Kothari' -PLUGIN_DESCRIPTION = 'Fetch first 30% of lyrics from Musixmatch' -PLUGIN_VERSION = '1.0' -PLUGIN_API_VERSIONS = ["2.0"] -PLUGIN_LICENSE = "GPL-2.0" -PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" diff --git a/plugins/musixmatch/musixmatch/track.py b/plugins/musixmatch/musixmatch/track.py deleted file mode 100644 index d2f22da9..00000000 --- a/plugins/musixmatch/musixmatch/track.py +++ /dev/null @@ -1,187 +0,0 @@ -""" -track.py - by Amelie Anglade and Thierry Bertin-Mahieux - amelie.anglade@gmail.com & tb2332@columbia.edu - -Edited by m-yn: no urllib2, no Queue, no bisect - -Class and functions to query MusixMatch regarding a track -(find the track, get lyrics, chart info, ...) - -(c) 2011, A. Anglade and T. Bertin-Mahieux - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" - -import os -import sys -from . import util - - -class Track(object): - """ - Class to query the musixmatch API tracks - If the class is constructed with a MusixMatch ID (default), - we assume the ID exists. - The constructor can find the track from a musicbrainz ID - or Echo Nest track ID. - Then, one can search for lyrics or charts. - """ - - #track.get in API - def __init__(self, track_id, musicbrainz=False, echonest=False, - trackdata=None): - """ - Create a Track object based on a given ID. - If musicbrainz or echonest is True, search for the song. - Takes a musixmatch ID (if both musicbrainz and echonest are False) - or musicbrainz id or echo nest track id - Raises an exception if the track is not found. - INPUT - track_id - track id (from whatever service) - musicbrainz - set to True if track_id from musicbrainz - echonest - set to True if track_id from The Echo Nest - trackdata - if you already have the information about - the track (after a search), bypass API call - """ - if musicbrainz and echonest: - msg = 'Creating a Track, only musicbrainz OR echonest can be True.' - raise ValueError(msg) - if trackdata is None: - if musicbrainz: - # params = {'musicbrainz_id': track_id} - # amin - params = {'track_mbid': track_id} - elif echonest: - params = {'echonest_track_id': track_id} - else: - params = {'track_id': track_id} - # url call - body = util.call('track.get', params) - trackdata = body['track'] - # save result - for k in list(trackdata.keys()): - self.__setattr__(k, trackdata[k]) - - # track.lyrics.get in the API - def lyrics(self): - """ - Get the lyrics for that track. - RETURN - dictionary containing keys: - - 'lyrics_body' (main data) - - 'lyrics_id' - - 'lyrics_language' - - 'lyrics copyright' - - 'pixel_tracking_url' - - 'script_tracking_url' - """ - body = util.call('track.lyrics.get', {'track_id': self.track_id}) - return body["lyrics"] - - #track.subtitle.get in API - def subtitles(self): - """ - Get subtitles, available for a few songs as of 02/2011 - Returns dictionary. - """ - body = util.call('track.subtitle.get', {'track_id': self.track_id}) - return body["subtitle"] - - # track.lyrics.feedback.post - def feedback(self, feedback): - """ - To leave feedback about lyrics for this track. - PARAMETERS - 'feedback' can be one of: - * wrong_attribution: the lyrics shown are not by the artist that I selected. - * bad_characters: there are strange characters and/or words - that are partially scrambled. - * lines_too_long: the text for each verse is too long! - * wrong_verses: there are some verses missing from the beginning - or at the end. - * wrong_formatting: the text looks horrible, please fix it! - """ - params = {'track_id': self.track_id, 'lyrics_id': self.lyrics_id, - 'feedback': feedback} - body = util.call('track.lyrics.feedback.post', params) - - def __str__(self): - """ pretty printout """ - return 'MusixMatch Track: ' + str(self.__dict__) - - -#track.search in API -def search(**args): - """ - Parameters: - q: a string that will be searched in every data field - (q_track, q_artist, q_lyrics) - q_track: words to be searched among track titles - q_artist: words to be searched among artist names - q_track_artist: words to be searched among track titles or artist names - q_lyrics: words to be searched into the lyrics - page: requested page of results - page_size: desired number of items per result page - f_has_lyrics: exclude tracks without an available lyrics - (automatic if q_lyrics is set) - f_artist_id: filter the results by the artist_id - f_artist_mbid: filter the results by the artist_mbid - quorum_factor: only works together with q and q_track_artist parameter. - Possible values goes from 0.1 to 0.9 - A value of 0.9 means: 'match at least 90 percent of the words'. - """ - # sanity check - valid_params = ('q', 'q_track', 'q_artist', 'q_track_artist', 'q_lyrics', - 'page', 'page_size', 'f_has_lyrics', 'f_artist_id', - 'f_artist_mbid', 'quorum_factor', 'apikey') - for k in list(args.keys()): - if not k in valid_params: - raise util.MusixMatchAPIError(-1, - 'Invalid track search param: ' + str(k)) - # call and gather a list of tracks - track_list = list() - params = dict((k, v) for k, v in list(args.items()) if not v is None) - body = util.call('track.search', params) - track_list_dict = body["track_list"] - for track_dict in track_list_dict: - t = Track(-1, trackdata=track_dict["track"]) - track_list.append(t) - return track_list - - -#track.chart.get in API -def chart(**args): - """ - Parameters: - page: requested page of results - page_size: desired number of items per result page - country: the country code of the desired country chart - f_has_lyrics: exclude tracks without an available lyrics - (automatic if q_lyrics is set) - """ - # sanity check - valid_params = ('page', 'page_size', 'country', 'f_has_lyrics', 'apikey') - for k in list(args.keys()): - if not k in valid_params: - raise util.MusixMatchAPIError(-1, 'Invalid chart param: ' + str(k)) - # do the call and gather track list - track_list = list() - params = dict((k, v) for k, v in list(args.items()) if not v is None) - body = util.call('track.chart.get', params) - track_list_dict = body["track_list"] - for track_dict in track_list_dict: - t = Track(-1, trackdata=track_dict["track"]) - track_list.append(t) - return track_list diff --git a/plugins/musixmatch/musixmatch/util.py b/plugins/musixmatch/musixmatch/util.py deleted file mode 100644 index c3679ac3..00000000 --- a/plugins/musixmatch/musixmatch/util.py +++ /dev/null @@ -1,224 +0,0 @@ -""" -util.py - by Amelie Anglade and Thierry Bertin-Mahieux - amelie.anglade@gmail.com & tb2332@columbia.edu - -Edited by m-yn: no urllib2, no Queue, no bisect - -Set of util functions used by the MusixMatch Python API, -mostly do HMTL calls. - -(c) 2011, A. Anglade and T. Bertin-Mahieux - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -""" - -import os -import sys -import time -import copy -import urllib.request, urllib.parse, urllib.error -try: - import json -except ImportError: - import simplejson as json - -# MusixMatch API key, should be an environment variable -MUSIXMATCH_API_KEY = None -if('MUSIXMATCH_API_KEY' in os.environ): - MUSIXMATCH_API_KEY = os.environ['MUSIXMATCH_API_KEY'] - -# details of the website to call -API_HOST = 'api.musixmatch.com' -API_SELECTOR = '/ws/1.1/' - -# cache time length (seconds) -CACHE_TLENGTH = 3600 - - -class TimedCache(): - """ - Class to cach hashable object for a given time length - """ - - def __init__(self, verbose=0): - """ contructor, init main dict and priority queue """ - self.stuff = {} - self.last_cleanup = time.time() - self.verbose = verbose - - def cache(self, query, res): - """ - Cache a query with a given result - Use the occasion to remove one old stuff if needed - """ - # remove old stuff - curr_time = time.time() - if curr_time - self.last_cleanup > CACHE_TLENGTH: - if self.verbose: - print('we cleanup cache') - new_stuff = {} - new_stuff.update([x for x in list(self.stuff.items()) if curr_time - x[1][0] < CACHE_TLENGTH]) - self.stuff = new_stuff - self.last_cleanup = curr_time - # add object to cache (try/except should be useless now) - try: - hashcode = hash(query) - if self.verbose: - print(('cache, hashcode is:', hashcode)) - self.stuff[hashcode] = (time.time(), copy.deepcopy(res)) - except TypeError as e: - print(('Error, stuff not hashable:', e)) - pass - - def query_cache(self, query): - """ - query the cache for a given query - Return None if not there or too old - """ - hashcode = hash(query) - if self.verbose: - print(('query_cache, hashcode is:', hashcode)) - if hashcode in list(self.stuff.keys()): - data = self.stuff[hashcode] - if time.time() - data[0] > CACHE_TLENGTH: - self.stuff.pop(hashcode) - return None - return data[1] - return None - -# instace of the cache -MXMPY_CACHE = TimedCache() - - -# typical API error message -class MusixMatchAPIError(Exception): - """ - Error raised when the status code returned by - the MusixMatch API is not 200 - """ - - def __init__(self, code, message=None): - self.mxm_code = code - if message is None: - message = status_code(code) - self.args = ('MusixMatch API Error %d: %s' % (code, message),) - - -def call(method, params, nocaching=False): - """ - Do the GET call to the MusixMatch API - Paramteres - method - string describing the method, e.g. track.get - params - dictionary of params, e.g. track_id -> 123 - nocaching - set to True to disable caching - """ - for k, v in list(params.items()): - if isinstance(v, str): - params[k] = v.encode('utf-8') - # sanity checks - params['format'] = 'json' - if not 'apikey' in list(params.keys()) or params['apikey'] is None: - params['apikey'] = MUSIXMATCH_API_KEY - if params['apikey'] is None: - raise MusixMatchAPIError(-1, 'EMPTY API KEY, NOT IN YOUR ENVIRONMENT?') - params = urllib.parse.urlencode(params) - # caching - if not nocaching: - cached_res = MXMPY_CACHE.query_cache(method + str(params)) - if not cached_res is None: - return cached_res - # encode the url request, call - url = 'http://%s%s%s?%s' % (API_HOST, API_SELECTOR, method, params) - # print url - f = urllib.request.urlopen(url) - response = f.read() - # decode response into json - response = decode_json(response) - # return body if status is OK - res_checked = check_status(response) - # cache - if not nocaching: - MXMPY_CACHE.cache(method + str(params), res_checked) - # done - return res_checked - - -def decode_json(raw_json): - """ - Transform the json into a python dictionary - or raise a ValueError - """ - try: - response_dict = json.loads(raw_json) - except ValueError: - raise MusixMatchAPIError(-1, "Unknown error.") - return response_dict - - -def check_status(response): - """ - Checks the response in JSON format - Raise an error, or returns the body of the message - RETURN: - body of the message in JSON - except if error was raised - """ - if not 'message' in list(response.keys()): - raise MusixMatchAPIError(-1) - msg = response['message'] - if not 'header' in list(msg.keys()): - raise MusixMatchAPIError(-1) - header = msg['header'] - if not 'status_code' in list(header.keys()): - raise MusixMatchAPIError(-1) - code = header['status_code'] - if code != 200: - raise MusixMatchAPIError(code) - # all good, return body - body = msg['body'] - return body - - -def status_code(value): - """ - Get a value, i.e. error code as a int. - Returns an appropriate message. - """ - if value == 200: - q = "The request was successful." - return q - if value == 400: - q = "The request had bad syntax or was inherently impossible" - q += " to be satisfied." - return q - if value == 401: - q = "Authentication failed, probably because of a bad API key." - return q - if value == 402: - q = "A limit was reached, either you exceeded per hour requests" - q += " limits or your balance is insufficient." - return q - if value == 403: - q = "You are not authorized to perform this operation / the api" - q += " version you're trying to use has been shut down." - return q - if value == 404: - q = "Requested resource was not found." - return q - if value == 405: - q = "Requested method was not found." - return q - # wrong code? - return "Unknown error code: " + str(value) From 0442ecb7e28c6cbb9eb02a1e2578c5886664504b Mon Sep 17 00:00:00 2001 From: Andrea Avallone Date: Mon, 4 Mar 2019 22:46:14 +0100 Subject: [PATCH 103/123] Initial commit --- plugins/apiseeds-lyrics/apiseeds-lyrics.py | 116 +++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 plugins/apiseeds-lyrics/apiseeds-lyrics.py diff --git a/plugins/apiseeds-lyrics/apiseeds-lyrics.py b/plugins/apiseeds-lyrics/apiseeds-lyrics.py new file mode 100644 index 00000000..c834fa5a --- /dev/null +++ b/plugins/apiseeds-lyrics/apiseeds-lyrics.py @@ -0,0 +1,116 @@ +PLUGIN_NAME = 'Apiseeds Lyrics' +PLUGIN_AUTHOR = 'Avallone Andrea' +PLUGIN_DESCRIPTION = '''Fetch lyrics from Apiseeds Lyrics which provides millions of lyrics from artist all around the world. Lyrics provided are for educational purposes and personal use only. Commercial use is not allowed. In order to use Apiseeds you need to get a free API key at https://apiseeds.com. Want to contribute? Check out the project page at https://github.com/avalloneandrea/apiseeds-lyrics!''' +PLUGIN_VERSION = '1.0.0' +PLUGIN_API_VERSIONS = ['2.0.0'] +PLUGIN_LICENSE = 'MIT' +PLUGIN_LICENSE_URL = 'https://opensource.org/licenses/MIT' + + +from functools import partial +from picard import config, log +from picard.config import TextOption +from picard.metadata import register_track_metadata_processor +from picard.ui.options import register_options_page, OptionsPage +from picard.util import load_json +from picard.webservice import ratecontrol +from PyQt5 import QtWidgets +from urllib.parse import quote, urlencode + + +APISEEDS_HOST = 'orion.apiseeds.com' +APISEEDS_PORT = 443 +APISEEDS_RATE_LIMIT = 60 * 1000 / 200 +ratecontrol.set_minimum_delay((APISEEDS_HOST, APISEEDS_PORT), APISEEDS_RATE_LIMIT) + + +def process_result(album, metadata, response, reply, error): + + try: + data = load_json(response) + lyrics = data['result']['track']['text'] + metadata['lyrics'] = lyrics + log.info('{}: lyrics found for track {}'.format(PLUGIN_NAME, metadata['title'])) + + except: + log.info('{}: lyrics NOT found for track {}'.format(PLUGIN_NAME, metadata['title'])) + + finally: + album._requests -= 1 + album._finalize_loading(None) + + +def process_track(album, metadata, release, track): + + apikey = config.setting['apiseeds_api_key'] + if (apikey is None): + log.error('{}: API key is missing, please provide a valid value'.format(PLUGIN_NAME)) + return + + artist = metadata['artist'] + if (artist is None): + log.error('{}: artist is missing, please provide a valid value'.format(PLUGIN_NAME)) + return + + title = metadata['title'] + if (title is None): + log.error('{}: title is missing, please provide a valid value'.format(PLUGIN_NAME)) + return + + apiseeds_path = '/api/music/lyric/{}/{}'.format(artist, title) + apiseeds_params = {'apikey': apikey} + album._requests += 1 + log.info('{}: GET {}?{}'.format(PLUGIN_NAME, quote(apiseeds_path), urlencode(apiseeds_params))) + + album.tagger.webservice.get( + APISEEDS_HOST, + APISEEDS_PORT, + apiseeds_path, + partial(process_result, album, metadata), + parse_response_type=None, + priority=True, + queryargs=apiseeds_params) + + +class ApiseedsLyricsOptionsPage(OptionsPage): + + NAME = 'apiseeds_lyrics' + TITLE = 'Apiseeds Lyrics' + PARENT = 'plugins' + + options = [TextOption('setting', 'apiseeds_api_key', None)] + + def __init__(self, parent=None): + + super(ApiseedsLyricsOptionsPage, self).__init__(parent) + self.box = QtWidgets.QVBoxLayout(self) + + self.label = QtWidgets.QLabel(self) + self.label.setText('Apiseeds API key') + self.box.addWidget(self.label) + + self.description = QtWidgets.QLabel(self) + self.description.setText('Apiseeds Lyrics provides millions of lyrics from artist all around the world. Lyrics provided are for educational purposes and personal use only. Commercial use is not allowed. In order to use Apiseeds Lyrics you need to get a free API key here.') + self.description.setOpenExternalLinks(True) + self.box.addWidget(self.description) + + self.input = QtWidgets.QLineEdit(self) + self.box.addWidget(self.input) + + self.contribute = QtWidgets.QLabel(self) + self.contribute.setText('Want to contribute? Check out the project page!') + self.contribute.setOpenExternalLinks(True) + self.box.addWidget(self.contribute) + + self.spacer = QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.box.addItem(self.spacer) + + def load(self): + self.input.setText(config.setting['apiseeds_api_key']) + + def save(self): + config.setting['apiseeds_api_key'] = self.input.text() + + +register_options_page(ApiseedsLyricsOptionsPage) +register_track_metadata_processor(process_track) From 2351d0c5261c861c149eb54cdaeee9b3edee5689 Mon Sep 17 00:00:00 2001 From: Andrea Avallone Date: Thu, 7 Mar 2019 15:48:16 +0100 Subject: [PATCH 104/123] Small fixes --- plugins/apiseeds-lyrics/apiseeds-lyrics.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/plugins/apiseeds-lyrics/apiseeds-lyrics.py b/plugins/apiseeds-lyrics/apiseeds-lyrics.py index c834fa5a..c951cace 100644 --- a/plugins/apiseeds-lyrics/apiseeds-lyrics.py +++ b/plugins/apiseeds-lyrics/apiseeds-lyrics.py @@ -1,6 +1,9 @@ PLUGIN_NAME = 'Apiseeds Lyrics' -PLUGIN_AUTHOR = 'Avallone Andrea' -PLUGIN_DESCRIPTION = '''Fetch lyrics from Apiseeds Lyrics which provides millions of lyrics from artist all around the world. Lyrics provided are for educational purposes and personal use only. Commercial use is not allowed. In order to use Apiseeds you need to get a free API key at https://apiseeds.com. Want to contribute? Check out the project page at https://github.com/avalloneandrea/apiseeds-lyrics!''' +PLUGIN_AUTHOR = 'Andrea Avallone' +PLUGIN_DESCRIPTION = 'Fetch lyrics from Apiseeds Lyrics, which provides millions of lyrics from artist all around the world. ' \ + 'Lyrics provided are for educational purposes and personal use only. Commercial use is not allowed. ' \ + 'In order to use Apiseeds you need to get a free API key at https://apiseeds.com. ' \ + 'Want to contribute? Check out the project page at https://github.com/avalloneandrea/apiseeds-lyrics!''' PLUGIN_VERSION = '1.0.0' PLUGIN_API_VERSIONS = ['2.0.0'] PLUGIN_LICENSE = 'MIT' @@ -90,7 +93,9 @@ def __init__(self, parent=None): self.box.addWidget(self.label) self.description = QtWidgets.QLabel(self) - self.description.setText('Apiseeds Lyrics provides millions of lyrics from artist all around the world. Lyrics provided are for educational purposes and personal use only. Commercial use is not allowed. In order to use Apiseeds Lyrics you need to get a free API key here.') + self.description.setText('Apiseeds Lyrics provides millions of lyrics from artist all around the world. ' + 'Lyrics provided are for educational purposes and personal use only. Commercial use is not allowed. ' + 'In order to use Apiseeds Lyrics you need to get a free API key here.') self.description.setOpenExternalLinks(True) self.box.addWidget(self.description) @@ -112,5 +117,5 @@ def save(self): config.setting['apiseeds_api_key'] = self.input.text() -register_options_page(ApiseedsLyricsOptionsPage) register_track_metadata_processor(process_track) +register_options_page(ApiseedsLyricsOptionsPage) From 743ad6dfe92e3ac4febe55913f378ee4ddb48be1 Mon Sep 17 00:00:00 2001 From: Andrea Avallone Date: Thu, 7 Mar 2019 15:50:15 +0100 Subject: [PATCH 105/123] Replace log.info and log.error with log.debug --- plugins/apiseeds-lyrics/apiseeds-lyrics.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/apiseeds-lyrics/apiseeds-lyrics.py b/plugins/apiseeds-lyrics/apiseeds-lyrics.py index c951cace..0149c0d5 100644 --- a/plugins/apiseeds-lyrics/apiseeds-lyrics.py +++ b/plugins/apiseeds-lyrics/apiseeds-lyrics.py @@ -33,10 +33,10 @@ def process_result(album, metadata, response, reply, error): data = load_json(response) lyrics = data['result']['track']['text'] metadata['lyrics'] = lyrics - log.info('{}: lyrics found for track {}'.format(PLUGIN_NAME, metadata['title'])) + log.debug('{}: lyrics found for track {}'.format(PLUGIN_NAME, metadata['title'])) except: - log.info('{}: lyrics NOT found for track {}'.format(PLUGIN_NAME, metadata['title'])) + log.debug('{}: lyrics NOT found for track {}'.format(PLUGIN_NAME, metadata['title'])) finally: album._requests -= 1 @@ -47,23 +47,23 @@ def process_track(album, metadata, release, track): apikey = config.setting['apiseeds_api_key'] if (apikey is None): - log.error('{}: API key is missing, please provide a valid value'.format(PLUGIN_NAME)) + log.debug('{}: API key is missing, please provide a valid value'.format(PLUGIN_NAME)) return artist = metadata['artist'] if (artist is None): - log.error('{}: artist is missing, please provide a valid value'.format(PLUGIN_NAME)) + log.debug('{}: artist is missing, please provide a valid value'.format(PLUGIN_NAME)) return title = metadata['title'] if (title is None): - log.error('{}: title is missing, please provide a valid value'.format(PLUGIN_NAME)) + log.debug('{}: title is missing, please provide a valid value'.format(PLUGIN_NAME)) return apiseeds_path = '/api/music/lyric/{}/{}'.format(artist, title) apiseeds_params = {'apikey': apikey} album._requests += 1 - log.info('{}: GET {}?{}'.format(PLUGIN_NAME, quote(apiseeds_path), urlencode(apiseeds_params))) + log.debug('{}: GET {}?{}'.format(PLUGIN_NAME, quote(apiseeds_path), urlencode(apiseeds_params))) album.tagger.webservice.get( APISEEDS_HOST, From e63d16c4cf135a68e35864177b790af4a07315fe Mon Sep 17 00:00:00 2001 From: Andrea Avallone Date: Thu, 7 Mar 2019 15:53:20 +0100 Subject: [PATCH 106/123] Replace load_json(response) with parse_response_type='json' --- plugins/apiseeds-lyrics/apiseeds-lyrics.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/apiseeds-lyrics/apiseeds-lyrics.py b/plugins/apiseeds-lyrics/apiseeds-lyrics.py index 0149c0d5..eb0a1d0e 100644 --- a/plugins/apiseeds-lyrics/apiseeds-lyrics.py +++ b/plugins/apiseeds-lyrics/apiseeds-lyrics.py @@ -15,7 +15,6 @@ from picard.config import TextOption from picard.metadata import register_track_metadata_processor from picard.ui.options import register_options_page, OptionsPage -from picard.util import load_json from picard.webservice import ratecontrol from PyQt5 import QtWidgets from urllib.parse import quote, urlencode @@ -30,8 +29,7 @@ def process_result(album, metadata, response, reply, error): try: - data = load_json(response) - lyrics = data['result']['track']['text'] + lyrics = response['result']['track']['text'] metadata['lyrics'] = lyrics log.debug('{}: lyrics found for track {}'.format(PLUGIN_NAME, metadata['title'])) @@ -70,7 +68,7 @@ def process_track(album, metadata, release, track): APISEEDS_PORT, apiseeds_path, partial(process_result, album, metadata), - parse_response_type=None, + parse_response_type='json', priority=True, queryargs=apiseeds_params) From a57e567c099ef17b849532bd98087bdc155fa5c7 Mon Sep 17 00:00:00 2001 From: Andrea Avallone Date: Thu, 7 Mar 2019 15:55:01 +0100 Subject: [PATCH 107/123] Fix checks on apikey, artist and title --- plugins/apiseeds-lyrics/apiseeds-lyrics.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/apiseeds-lyrics/apiseeds-lyrics.py b/plugins/apiseeds-lyrics/apiseeds-lyrics.py index eb0a1d0e..ee467bb8 100644 --- a/plugins/apiseeds-lyrics/apiseeds-lyrics.py +++ b/plugins/apiseeds-lyrics/apiseeds-lyrics.py @@ -44,17 +44,17 @@ def process_result(album, metadata, response, reply, error): def process_track(album, metadata, release, track): apikey = config.setting['apiseeds_api_key'] - if (apikey is None): + if not apikey: log.debug('{}: API key is missing, please provide a valid value'.format(PLUGIN_NAME)) return artist = metadata['artist'] - if (artist is None): + if not artist: log.debug('{}: artist is missing, please provide a valid value'.format(PLUGIN_NAME)) return title = metadata['title'] - if (title is None): + if not title: log.debug('{}: title is missing, please provide a valid value'.format(PLUGIN_NAME)) return From 2fd937c573000db19821aa002cc5410c1942b4f8 Mon Sep 17 00:00:00 2001 From: Andrea Avallone Date: Fri, 8 Mar 2019 08:32:09 +0100 Subject: [PATCH 108/123] Version 1.0.1 --- plugins/apiseeds-lyrics/apiseeds-lyrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/apiseeds-lyrics/apiseeds-lyrics.py b/plugins/apiseeds-lyrics/apiseeds-lyrics.py index ee467bb8..7a6af724 100644 --- a/plugins/apiseeds-lyrics/apiseeds-lyrics.py +++ b/plugins/apiseeds-lyrics/apiseeds-lyrics.py @@ -4,7 +4,7 @@ 'Lyrics provided are for educational purposes and personal use only. Commercial use is not allowed. ' \ 'In order to use Apiseeds you need to get a free API key at https://apiseeds.com. ' \ 'Want to contribute? Check out the project page at https://github.com/avalloneandrea/apiseeds-lyrics!''' -PLUGIN_VERSION = '1.0.0' +PLUGIN_VERSION = '1.0.1' PLUGIN_API_VERSIONS = ['2.0.0'] PLUGIN_LICENSE = 'MIT' PLUGIN_LICENSE_URL = 'https://opensource.org/licenses/MIT' From c58442033b1a1ce8acd090c827748b800a88d2fb Mon Sep 17 00:00:00 2001 From: Andrea Avallone Date: Fri, 8 Mar 2019 08:59:57 +0100 Subject: [PATCH 109/123] Move processor to class --- plugins/apiseeds-lyrics/apiseeds-lyrics.py | 109 +++++++++++---------- 1 file changed, 56 insertions(+), 53 deletions(-) diff --git a/plugins/apiseeds-lyrics/apiseeds-lyrics.py b/plugins/apiseeds-lyrics/apiseeds-lyrics.py index 7a6af724..62ade937 100644 --- a/plugins/apiseeds-lyrics/apiseeds-lyrics.py +++ b/plugins/apiseeds-lyrics/apiseeds-lyrics.py @@ -1,3 +1,13 @@ +from functools import partial +from urllib.parse import quote, urlencode + +from PyQt5 import QtWidgets +from picard import config, log +from picard.config import TextOption +from picard.metadata import register_track_metadata_processor +from picard.ui.options import register_options_page, OptionsPage +from picard.webservice import ratecontrol + PLUGIN_NAME = 'Apiseeds Lyrics' PLUGIN_AUTHOR = 'Andrea Avallone' PLUGIN_DESCRIPTION = 'Fetch lyrics from Apiseeds Lyrics, which provides millions of lyrics from artist all around the world. ' \ @@ -10,67 +20,60 @@ PLUGIN_LICENSE_URL = 'https://opensource.org/licenses/MIT' -from functools import partial -from picard import config, log -from picard.config import TextOption -from picard.metadata import register_track_metadata_processor -from picard.ui.options import register_options_page, OptionsPage -from picard.webservice import ratecontrol -from PyQt5 import QtWidgets -from urllib.parse import quote, urlencode - - -APISEEDS_HOST = 'orion.apiseeds.com' -APISEEDS_PORT = 443 -APISEEDS_RATE_LIMIT = 60 * 1000 / 200 -ratecontrol.set_minimum_delay((APISEEDS_HOST, APISEEDS_PORT), APISEEDS_RATE_LIMIT) +class ApiseedsLyricsMetadataProcessor(object): + apiseeds_host = 'orion.apiseeds.com' + apiseeds_port = 443 + apiseeds_rate_limit = 60 * 1000 / 200 -def process_result(album, metadata, response, reply, error): + def __init__(self): + super(ApiseedsLyricsMetadataProcessor, self).__init__() + ratecontrol.set_minimum_delay((self.apiseeds_host, self.apiseeds_port), self.apiseeds_rate_limit) - try: - lyrics = response['result']['track']['text'] - metadata['lyrics'] = lyrics - log.debug('{}: lyrics found for track {}'.format(PLUGIN_NAME, metadata['title'])) + def process_metadata(self, album, metadata, track, release): - except: - log.debug('{}: lyrics NOT found for track {}'.format(PLUGIN_NAME, metadata['title'])) + apikey = config.setting['apiseeds_apikey'] + if not apikey: + log.debug('{}: API key is missing, please provide a valid value'.format(PLUGIN_NAME)) + return - finally: - album._requests -= 1 - album._finalize_loading(None) + artist = metadata['artist'] + if not artist: + log.debug('{}: artist is missing, please provide a valid value'.format(PLUGIN_NAME)) + return + title = metadata['title'] + if not title: + log.debug('{}: title is missing, please provide a valid value'.format(PLUGIN_NAME)) + return -def process_track(album, metadata, release, track): + apiseeds_path = '/api/music/lyric/{}/{}'.format(artist, title) + apiseeds_params = {'apikey': apikey} + album._requests += 1 + log.debug('{}: GET {}?{}'.format(PLUGIN_NAME, quote(apiseeds_path), urlencode(apiseeds_params))) - apikey = config.setting['apiseeds_api_key'] - if not apikey: - log.debug('{}: API key is missing, please provide a valid value'.format(PLUGIN_NAME)) - return + album.tagger.webservice.get( + self.apiseeds_host, + self.apiseeds_port, + apiseeds_path, + partial(self.process_response, album, metadata), + parse_response_type='json', + priority=True, + queryargs=apiseeds_params) - artist = metadata['artist'] - if not artist: - log.debug('{}: artist is missing, please provide a valid value'.format(PLUGIN_NAME)) - return + def process_response(self, album, metadata, document, reply, error): - title = metadata['title'] - if not title: - log.debug('{}: title is missing, please provide a valid value'.format(PLUGIN_NAME)) - return + try: + lyrics = document['result']['track']['text'] + metadata['lyrics'] = lyrics + log.debug('{}: lyrics found for track {}'.format(PLUGIN_NAME, metadata['title'])) - apiseeds_path = '/api/music/lyric/{}/{}'.format(artist, title) - apiseeds_params = {'apikey': apikey} - album._requests += 1 - log.debug('{}: GET {}?{}'.format(PLUGIN_NAME, quote(apiseeds_path), urlencode(apiseeds_params))) + except: + log.debug('{}: lyrics NOT found for track {}'.format(PLUGIN_NAME, metadata['title'])) - album.tagger.webservice.get( - APISEEDS_HOST, - APISEEDS_PORT, - apiseeds_path, - partial(process_result, album, metadata), - parse_response_type='json', - priority=True, - queryargs=apiseeds_params) + finally: + album._requests -= 1 + album._finalize_loading(None) class ApiseedsLyricsOptionsPage(OptionsPage): @@ -79,7 +82,7 @@ class ApiseedsLyricsOptionsPage(OptionsPage): TITLE = 'Apiseeds Lyrics' PARENT = 'plugins' - options = [TextOption('setting', 'apiseeds_api_key', None)] + options = [TextOption('setting', 'apiseeds_apikey', None)] def __init__(self, parent=None): @@ -109,11 +112,11 @@ def __init__(self, parent=None): self.box.addItem(self.spacer) def load(self): - self.input.setText(config.setting['apiseeds_api_key']) + self.input.setText(config.setting['apiseeds_apikey']) def save(self): - config.setting['apiseeds_api_key'] = self.input.text() + config.setting['apiseeds_apikey'] = self.input.text() -register_track_metadata_processor(process_track) +register_track_metadata_processor(ApiseedsLyricsMetadataProcessor().process_metadata) register_options_page(ApiseedsLyricsOptionsPage) From cb201ecb641e16e89055797b3fd934a2ecf1169f Mon Sep 17 00:00:00 2001 From: Andrea Avallone Date: Fri, 8 Mar 2019 09:00:10 +0100 Subject: [PATCH 110/123] Version 1.0.2 --- plugins/apiseeds-lyrics/apiseeds-lyrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/apiseeds-lyrics/apiseeds-lyrics.py b/plugins/apiseeds-lyrics/apiseeds-lyrics.py index 62ade937..d5dfb876 100644 --- a/plugins/apiseeds-lyrics/apiseeds-lyrics.py +++ b/plugins/apiseeds-lyrics/apiseeds-lyrics.py @@ -14,7 +14,7 @@ 'Lyrics provided are for educational purposes and personal use only. Commercial use is not allowed. ' \ 'In order to use Apiseeds you need to get a free API key at https://apiseeds.com. ' \ 'Want to contribute? Check out the project page at https://github.com/avalloneandrea/apiseeds-lyrics!''' -PLUGIN_VERSION = '1.0.1' +PLUGIN_VERSION = '1.0.2' PLUGIN_API_VERSIONS = ['2.0.0'] PLUGIN_LICENSE = 'MIT' PLUGIN_LICENSE_URL = 'https://opensource.org/licenses/MIT' From b2feb9bd713f75b6b10185905ac9ac5aa377b85c Mon Sep 17 00:00:00 2001 From: Andrea Avallone Date: Fri, 8 Mar 2019 09:05:48 +0100 Subject: [PATCH 111/123] Rename plugin to conform snake_case naming style --- .../apiseeds-lyrics.py => apiseeds_lyrics/apiseeds_lyrics.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename plugins/{apiseeds-lyrics/apiseeds-lyrics.py => apiseeds_lyrics/apiseeds_lyrics.py} (100%) diff --git a/plugins/apiseeds-lyrics/apiseeds-lyrics.py b/plugins/apiseeds_lyrics/apiseeds_lyrics.py similarity index 100% rename from plugins/apiseeds-lyrics/apiseeds-lyrics.py rename to plugins/apiseeds_lyrics/apiseeds_lyrics.py From abfd303eb1a8d08de5e1d0466e1f895b870a8235 Mon Sep 17 00:00:00 2001 From: Andrea Avallone Date: Fri, 8 Mar 2019 10:42:56 +0100 Subject: [PATCH 112/123] Fix method could be a function --- plugins/apiseeds_lyrics/apiseeds_lyrics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/apiseeds_lyrics/apiseeds_lyrics.py b/plugins/apiseeds_lyrics/apiseeds_lyrics.py index d5dfb876..591f2198 100644 --- a/plugins/apiseeds_lyrics/apiseeds_lyrics.py +++ b/plugins/apiseeds_lyrics/apiseeds_lyrics.py @@ -61,7 +61,8 @@ def process_metadata(self, album, metadata, track, release): priority=True, queryargs=apiseeds_params) - def process_response(self, album, metadata, document, reply, error): + @staticmethod + def process_response(album, metadata, document, reply, error): try: lyrics = document['result']['track']['text'] From 383d40a08793d9aa19ee53bd128ade547504292f Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Fri, 8 Mar 2019 14:28:22 +0100 Subject: [PATCH 113/123] Various PEP8 fixes and general code cleanup --- plugins/fanarttv/__init__.py | 55 +++++++++------- plugins/loadasnat/loadasnat.py | 99 ++++++++++++++++++----------- plugins/papercdcase/papercdcase.py | 25 ++++---- plugins/theaudiodb/__init__.py | 46 ++++++++------ plugins/videotools/__init__.py | 19 ++++-- plugins/videotools/formats.py | 26 +------- plugins/workandmovement/__init__.py | 33 +++++----- 7 files changed, 170 insertions(+), 133 deletions(-) diff --git a/plugins/fanarttv/__init__.py b/plugins/fanarttv/__init__.py index d538b676..88f515af 100644 --- a/plugins/fanarttv/__init__.py +++ b/plugins/fanarttv/__init__.py @@ -19,9 +19,11 @@ PLUGIN_NAME = 'fanart.tv cover art' PLUGIN_AUTHOR = 'Philipp Wolfer, Sambhav Kothari' -PLUGIN_DESCRIPTION = 'Use cover art from fanart.tv. To use this plugin you have to register a personal API key on https://fanart.tv/get-an-api-key/' -PLUGIN_VERSION = "1.5.1" -PLUGIN_API_VERSIONS = ["2.0", "2.1"] +PLUGIN_DESCRIPTION = ('Use cover art from fanart.tv. To use this plugin you ' + 'have to register a personal API key on ' + 'https://fanart.tv/get-an-api-key/') +PLUGIN_VERSION = "1.5.2" +PLUGIN_API_VERSIONS = ["2.0", "2.1", "2.2"] PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" @@ -39,7 +41,7 @@ OptionsPage, ) from picard.config import TextOption -from picard.plugins.fanarttv.ui_options_fanarttv import Ui_FanartTvOptionsPage +from .ui_options_fanarttv import Ui_FanartTvOptionsPage FANART_HOST = "webservice.fanart.tv" FANART_PORT = 80 @@ -58,6 +60,10 @@ def cover_sort_key(cover): return 0 +def encode_queryarg(arg): + return bytes(QUrl.toPercentEncoding(arg)).decode() + + class FanartTvCoverArtImage(CoverArtImage): """Image from fanart.tv""" @@ -80,8 +86,8 @@ def queue_images(self): release_group_id = self.metadata["musicbrainz_releasegroupid"] path = "/v3/music/albums/%s" % (release_group_id, ) queryargs = { - "api_key": bytes(QUrl.toPercentEncoding(FANART_APIKEY)).decode(), - "client_key": bytes(QUrl.toPercentEncoding(self._client_key)).decode(), + "api_key": encode_queryarg(FANART_APIKEY), + "client_key": encode_queryarg(self._client_key), } log.debug("CoverArtProviderFanartTv.queue_downloads: %s" % path) self.album.tagger.webservice.get( @@ -108,27 +114,30 @@ def _json_downloaded(self, release_group_id, data, reply, error): error_level = log.error else: error_level = log.debug - error_level("Problem requesting metadata in fanart.tv plugin: %s", error) + error_level("Problem requesting metadata in fanart.tv plugin: %s", + error) else: try: release = data["albums"][release_group_id] + has_cover = "albumcover" in release + has_cdart = "cdart" in release + use_cdart = config.setting["fanarttv_use_cdart"] - if "albumcover" in release: + if has_cover: covers = release["albumcover"] types = ["front"] self._select_and_add_cover_art(covers, types) - if "cdart" in release and \ - (config.setting["fanarttv_use_cdart"] == OPTION_CDART_ALWAYS - or (config.setting["fanarttv_use_cdart"] == OPTION_CDART_NOALBUMART - and "albumcover" not in release)): + if has_cdart and (use_cdart == OPTION_CDART_ALWAYS + or (use_cdart == OPTION_CDART_NOALBUMART + and not has_cover)): covers = release["cdart"] types = ["medium"] - if "albumcover" not in release: + if not has_cover: types.append("front") self._select_and_add_cover_art(covers, types) except (AttributeError, KeyError, TypeError): - log.error("Problem processing downloaded metadata in fanart.tv plugin: %s", exc_info=True) + log.error("Problem processing downloaded metadata in fanart.tv plugin", exc_info=True) self.next_in_queue() @@ -156,22 +165,24 @@ def __init__(self, parent=None): self.ui.setupUi(self) def load(self): - self.ui.fanarttv_client_key.setText(config.setting["fanarttv_client_key"]) - if config.setting["fanarttv_use_cdart"] == OPTION_CDART_ALWAYS: + setting = config.setting + self.ui.fanarttv_client_key.setText(setting["fanarttv_client_key"]) + if setting["fanarttv_use_cdart"] == OPTION_CDART_ALWAYS: self.ui.fanarttv_cdart_use_always.setChecked(True) - elif config.setting["fanarttv_use_cdart"] == OPTION_CDART_NEVER: + elif setting["fanarttv_use_cdart"] == OPTION_CDART_NEVER: self.ui.fanarttv_cdart_use_never.setChecked(True) - elif config.setting["fanarttv_use_cdart"] == OPTION_CDART_NOALBUMART: + elif setting["fanarttv_use_cdart"] == OPTION_CDART_NOALBUMART: self.ui.fanarttv_cdart_use_if_no_albumcover.setChecked(True) def save(self): - config.setting["fanarttv_client_key"] = self.ui.fanarttv_client_key.text() + setting = config.setting + setting["fanarttv_client_key"] = self.ui.fanarttv_client_key.text() if self.ui.fanarttv_cdart_use_always.isChecked(): - config.setting["fanarttv_use_cdart"] = OPTION_CDART_ALWAYS + setting["fanarttv_use_cdart"] = OPTION_CDART_ALWAYS elif self.ui.fanarttv_cdart_use_never.isChecked(): - config.setting["fanarttv_use_cdart"] = OPTION_CDART_NEVER + setting["fanarttv_use_cdart"] = OPTION_CDART_NEVER elif self.ui.fanarttv_cdart_use_if_no_albumcover.isChecked(): - config.setting["fanarttv_use_cdart"] = OPTION_CDART_NOALBUMART + setting["fanarttv_use_cdart"] = OPTION_CDART_NOALBUMART register_cover_art_provider(CoverArtProviderFanartTv) diff --git a/plugins/loadasnat/loadasnat.py b/plugins/loadasnat/loadasnat.py index 1a537006..1b2ce52d 100644 --- a/plugins/loadasnat/loadasnat.py +++ b/plugins/loadasnat/loadasnat.py @@ -1,50 +1,75 @@ # -*- coding: utf-8 -*- +# +# Copyright (C) 2017, 2019 Philipp Wolfer +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. PLUGIN_NAME = "Load as non-album track" PLUGIN_AUTHOR = "Philipp Wolfer" -PLUGIN_DESCRIPTION = "Allows loading selected tracks as non-album tracks. Useful for tagging single tracks where you do not care about the album." -PLUGIN_VERSION = "0.1" -PLUGIN_API_VERSIONS = ["1.4.0", "2.0"] -PLUGIN_LICENSE = "GPL-2.0" +PLUGIN_DESCRIPTION = ("Allows loading selected tracks as non-album tracks. " + "Useful for tagging single tracks where you do not care " + "about the album.") +PLUGIN_VERSION = "0.2" +PLUGIN_API_VERSIONS = ["1.4.0", "2.0", "2.1", "2.2"] +PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" from picard import log from picard.album import Track -from picard.ui.itemviews import BaseAction, register_track_action +from picard.ui.itemviews import ( + BaseAction, + register_track_action, +) + class LoadAsNat(BaseAction): - NAME = "Load as non-album track..." - - def callback(self, objs): - tracks = [t for t in objs if isinstance(t, Track)] - - if len(tracks) == 0: - return - - for track in tracks: - nat = self.tagger.load_nat(track.metadata['musicbrainz_recordingid']) - for file in track.iterfiles(): - file.move(nat) - file.metadata.delete('albumartist') - file.metadata.delete('albumartistsort') - file.metadata.delete('albumsort') - file.metadata.delete('asin') - file.metadata.delete('barcode') - file.metadata.delete('catalognumber') - file.metadata.delete('discnumber') - file.metadata.delete('discsubtitle') - file.metadata.delete('media') - file.metadata.delete('musicbrainz_albumartistid') - file.metadata.delete('musicbrainz_albumid') - file.metadata.delete('musicbrainz_discid') - file.metadata.delete('musicbrainz_releasegroupid') - file.metadata.delete('releasecountry') - file.metadata.delete('releasestatus') - file.metadata.delete('releasetype') - file.metadata.delete('totaldiscs') - file.metadata.delete('totaltracks') - file.metadata.delete('tracknumber') - log.debug("[LoadAsNat] deleted tags: %r", file.metadata.deleted_tags) + NAME = "Load as non-album track..." + + def callback(self, objs): + tracks = [t for t in objs if isinstance(t, Track)] + + if len(tracks) == 0: + return + + for track in tracks: + nat = self.tagger.load_nat( + track.metadata['musicbrainz_recordingid']) + for file in track.iterfiles(): + file.move(nat) + metadata = file.metadata + metadata.delete('albumartist') + metadata.delete('albumartistsort') + metadata.delete('albumsort') + metadata.delete('asin') + metadata.delete('barcode') + metadata.delete('catalognumber') + metadata.delete('discnumber') + metadata.delete('discsubtitle') + metadata.delete('media') + metadata.delete('musicbrainz_albumartistid') + metadata.delete('musicbrainz_albumid') + metadata.delete('musicbrainz_discid') + metadata.delete('musicbrainz_releasegroupid') + metadata.delete('releasecountry') + metadata.delete('releasestatus') + metadata.delete('releasetype') + metadata.delete('totaldiscs') + metadata.delete('totaltracks') + metadata.delete('tracknumber') + log.debug("[LoadAsNat] deleted tags: %r", metadata.deleted_tags) register_track_action(LoadAsNat()) diff --git a/plugins/papercdcase/papercdcase.py b/plugins/papercdcase/papercdcase.py index bd2fcb22..37e9fcce 100644 --- a/plugins/papercdcase/papercdcase.py +++ b/plugins/papercdcase/papercdcase.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2015 Philipp Wolfer +# Copyright (C) 2015, 2019 Philipp Wolfer # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -19,9 +19,10 @@ PLUGIN_NAME = 'Paper CD case' PLUGIN_AUTHOR = 'Philipp Wolfer, Sambhav Kothari' -PLUGIN_DESCRIPTION = 'Create a paper CD case from an album or cluster using http://papercdcase.com' -PLUGIN_VERSION = "1.2" -PLUGIN_API_VERSIONS = ["2.0"] +PLUGIN_DESCRIPTION = ('Create a paper CD case from an album or cluster ' + 'using http://papercdcase.com') +PLUGIN_VERSION = "1.2.1" +PLUGIN_API_VERSIONS = ["2.0", "2.1", "2.2"] PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" @@ -29,26 +30,28 @@ from PyQt5.QtCore import QUrl, QUrlQuery from picard.album import Album from picard.cluster import Cluster -from picard.ui.itemviews import BaseAction, register_album_action, register_cluster_action +from picard.ui.itemviews import ( + BaseAction, + register_album_action, + register_cluster_action, +) from picard.util import webbrowser2 from picard.util import textencoding PAPERCDCASE_URL = 'http://papercdcase.com/advanced.php' - - URLENCODE_ASCII_CHARS = [ord(' '), ord('%'), ord('&'), ord('/'), ord('?')] def urlencode(s): - l = [] + chars = [] for c in s: n = ord(c) if (n < 128 or n > 255) and n not in URLENCODE_ASCII_CHARS: - l.append(c) + chars.append(c) else: - l.append('%' + hex(n)[2:].upper()) - return ''.join(l) + chars.append('%' + hex(n)[2:].upper()) + return ''.join(chars) def build_papercdcase_url(artist, album, tracks): diff --git a/plugins/theaudiodb/__init__.py b/plugins/theaudiodb/__init__.py index eb11b18f..38967ca7 100644 --- a/plugins/theaudiodb/__init__.py +++ b/plugins/theaudiodb/__init__.py @@ -20,19 +20,25 @@ PLUGIN_NAME = 'TheAudioDB cover art' PLUGIN_AUTHOR = 'Philipp Wolfer' PLUGIN_DESCRIPTION = 'Use cover art from TheAudioDB.' -PLUGIN_VERSION = "1.0.1" -PLUGIN_API_VERSIONS = ["2.0", "2.1"] +PLUGIN_VERSION = "1.0.2" +PLUGIN_API_VERSIONS = ["2.0", "2.1", "2.2"] PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" from PyQt5.QtCore import QUrl from PyQt5.QtNetwork import QNetworkReply from picard import config, log -from picard.coverart.providers import CoverArtProvider, register_cover_art_provider +from picard.coverart.providers import ( + CoverArtProvider, + register_cover_art_provider, +) from picard.coverart.image import CoverArtImage -from picard.ui.options import register_options_page, OptionsPage +from picard.ui.options import ( + register_options_page, + OptionsPage, +) from picard.config import TextOption -from picard.plugins.theaudiodb.ui_options_theaudiodb import Ui_TheAudioDbOptionsPage +from .ui_options_theaudiodb import Ui_TheAudioDbOptionsPage THEAUDIODB_HOST = "www.theaudiodb.com" THEAUDIODB_PORT = 443 @@ -52,7 +58,8 @@ class TheAudioDbCoverArtImage(CoverArtImage): def parse_url(self, url): super().parse_url(url) - # Workaround for Picard always returning port 80 regardless of the scheme + # Workaround for Picard always returning port 80 regardless of the + # scheme. No longer necessary for Picard >= 2.1.3 self.port = self.url.port(443 if self.url.scheme() == 'https' else 80) @@ -71,7 +78,7 @@ def queue_images(self): queryargs = { "i": bytes(QUrl.toPercentEncoding(release_group_id)).decode() } - log.debug("TheAudioDB: Queued download: %s?i=%s" % (path, queryargs["i"])) + log.debug("TheAudioDB: Queued download: %s?i=%s", path, queryargs["i"]) self.album.tagger.webservice.get( THEAUDIODB_HOST, THEAUDIODB_PORT, @@ -97,25 +104,26 @@ def _json_downloaded(self, data, reply, error): try: releases = data.get("album") if not releases: - log.debug("TheAudioDB: No cover art found for %s", reply.url().url()) + log.debug("TheAudioDB: No cover art found for %s", + reply.url().url()) return release = releases[0] - albumArtUrl = release.get("strAlbumThumb") - cdArtUrl = release.get("strAlbumCDart") + albumart_url = release.get("strAlbumThumb") + cdart_url = release.get("strAlbumCDart") + use_cdart = config.setting["theaudiodb_use_cdart"] - if albumArtUrl: - self._select_and_add_cover_art(albumArtUrl, ["front"]) + if albumart_url: + self._select_and_add_cover_art(albumart_url, ["front"]) - if cdArtUrl and \ - (config.setting["theaudiodb_use_cdart"] == OPTION_CDART_ALWAYS - or (config.setting["theaudiodb_use_cdart"] == OPTION_CDART_NOALBUMART - and not albumArtUrl)): + if cdart_url and (use_cdart == OPTION_CDART_ALWAYS + or (use_cdart == OPTION_CDART_NOALBUMART + and not albumart_url)): types = ["medium"] - if not albumArtUrl: + if not albumart_url: types.append("front") - self._select_and_add_cover_art(cdArtUrl, types) + self._select_and_add_cover_art(cdart_url, types) except (TypeError): - log.error("TheAudioDB: Problem processing downloaded metadata: %s", exc_info=True) + log.error("TheAudioDB: Problem processing downloaded metadata", exc_info=True) finally: self.next_in_queue() diff --git a/plugins/videotools/__init__.py b/plugins/videotools/__init__.py index 209171e8..b163d7cf 100644 --- a/plugins/videotools/__init__.py +++ b/plugins/videotools/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2014, 2017 Philipp Wolfer +# Copyright (C) 2014, 2017, 2019 Philipp Wolfer # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -19,15 +19,24 @@ PLUGIN_NAME = 'Video tools' PLUGIN_AUTHOR = 'Philipp Wolfer' -PLUGIN_DESCRIPTION = 'Improves the video support in Picard by adding support for Matroska, WebM, AVI, QuickTime and MPEG files (renaming and fingerprinting only, no tagging) and providing $is_audio() and $is_video() scripting functions.' -PLUGIN_VERSION = "0.3" -PLUGIN_API_VERSIONS = ["1.3.0", "2.0"] +PLUGIN_DESCRIPTION = ('Improves the video support in Picard by adding support ' + 'for Matroska, WebM, AVI, QuickTime and MPEG files ' + '(renaming and fingerprinting only, no tagging) and ' + 'providing $is_audio() and $is_video() scripting ' + 'functions.') +PLUGIN_VERSION = "0.4" +PLUGIN_API_VERSIONS = ["2.0", "2.1", "2.2"] PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" from picard.formats import register_format from picard.script import register_script_function -from picard.plugins.videotools.formats import MatroskaFile, MpegFile, QuickTimeFile, RiffFile +from picard.plugins.videotools.formats import ( + MatroskaFile, + MpegFile, + QuickTimeFile, + RiffFile, +) from picard.plugins.videotools.script import is_audio, is_video diff --git a/plugins/videotools/formats.py b/plugins/videotools/formats.py index 0283524d..978a195c 100644 --- a/plugins/videotools/formats.py +++ b/plugins/videotools/formats.py @@ -1,26 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2014 Philipp Wolfer -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -# 02110-1301, USA. - -from __future__ import absolute_import -# -*- coding: utf-8 -*- -# -# Copyright (C) 2014 Philipp Wolfer +# Copyright (C) 2014, 2019 Philipp Wolfer # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -40,8 +20,6 @@ from . import enzyme from picard import log from picard.file import File -from picard.formats import register_format -from picard.formats.wav import WAVFile from picard.metadata import Metadata @@ -54,7 +32,7 @@ def _load(self, filename): try: parser = enzyme.parse(filename) - log.debug("Metadata for %s:\n%s", filename, unicode(parser)) + log.debug("Metadata for %s:\n%s", filename, str(parser)) self._convertMetadata(parser, metadata) except Exception as err: log.error("Could not parse file %r: %r", filename, err) diff --git a/plugins/workandmovement/__init__.py b/plugins/workandmovement/__init__.py index 46c69666..6355f40d 100644 --- a/plugins/workandmovement/__init__.py +++ b/plugins/workandmovement/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2018 Philipp Wolfer +# Copyright (C) 2018-2019 Philipp Wolfer # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -20,8 +20,8 @@ PLUGIN_NAME = 'Work & Movement' PLUGIN_AUTHOR = 'Philipp Wolfer' PLUGIN_DESCRIPTION = 'Set work and movement based on work relationships' -PLUGIN_VERSION = '1.0' -PLUGIN_API_VERSIONS = ['2.1'] +PLUGIN_VERSION = '1.0.1' +PLUGIN_API_VERSIONS = ['2.1', '2.2'] PLUGIN_LICENSE = 'GPL-2.0-or-later' PLUGIN_LICENSE_URL = 'https://www.gnu.org/licenses/gpl-2.0.html' @@ -37,6 +37,10 @@ from picard.metadata import register_track_metadata_processor +_re_work_title = re.compile(r'(?P.*):\s+(?P[IVXLCDM]+)\.\s+(?P.*)') +_re_part_number = re.compile(r'(?P[0-9IVXLCDM]+)\.?\s+') + + class Work: def __init__(self, title, mbid=None): self.mbid = mbid @@ -74,8 +78,8 @@ def is_parent_work(rel): def is_movement_like(rel): return ('movement' in rel['attributes'] - or 'act' in rel['attributes'] - or 'ordering-key' in rel) + or 'act' in rel['attributes'] + or 'ordering-key' in rel) def is_child_work(rel): @@ -97,7 +101,6 @@ def number_to_int(s): raise ValueError(e) -_re_work_title = re.compile(r'(?P.*):\s+(?P[IVXLCDM]+)\.\s+(?P.*)') def parse_work_name(title): return _re_work_title.search(title) @@ -122,23 +125,23 @@ def create_work_and_movement_from_title(work): if not work.part_number: work.part_number = number elif work.part_number != number: - log.warning('Movement number mismatch for "%s": %s != %i' % ( - title, match.group('movementnumber'), work.part_number)) + log.warning('Movement number mismatch for "%s": %s != %i', + title, match.group('movementnumber'), work.part_number) if not work.parent: work.parent = Work(match.group('work')) work.parent.is_work = True elif work.parent.title != match.group('work'): - log.warning('Movement work name mismatch for "%s": "%s" != "%s"' % ( - title, match.group('work'), work.parent.title)) + log.warning('Movement work name mismatch for "%s": "%s" != "%s"', + title, match.group('work'), work.parent.title) return work -_re_part_number = re.compile(r'(?P[0-9IVXLCDM]+)\.?\s+') def normalize_movement_title(work): """ - Removes the parent work title and part number from the beginning of `work.title`. - This ensures movement names don't contain duplicated information even if - they do not follow the strict naming format used by `create_work_and_movement_from_title`. + Removes the parent work title and part number from the beginning of + `work.title`. This ensures movement names don't contain duplicated + information even if they do not follow the strict naming format used by + `create_work_and_movement_from_title`. """ movement_title = work.title if work.parent: @@ -204,7 +207,7 @@ def process_track(album, metadata, track, release): else: recording = track - if not 'relations' in recording: + if 'relations' not in recording: return work = Work(recording['title']) From 038eff550e950c157a52df353fb7eafbadf5663a Mon Sep 17 00:00:00 2001 From: Andrea Avallone Date: Fri, 8 Mar 2019 10:44:35 +0100 Subject: [PATCH 114/123] Small fixes --- plugins/apiseeds_lyrics/apiseeds_lyrics.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/apiseeds_lyrics/apiseeds_lyrics.py b/plugins/apiseeds_lyrics/apiseeds_lyrics.py index 591f2198..2d02dafc 100644 --- a/plugins/apiseeds_lyrics/apiseeds_lyrics.py +++ b/plugins/apiseeds_lyrics/apiseeds_lyrics.py @@ -13,7 +13,7 @@ PLUGIN_DESCRIPTION = 'Fetch lyrics from Apiseeds Lyrics, which provides millions of lyrics from artist all around the world. ' \ 'Lyrics provided are for educational purposes and personal use only. Commercial use is not allowed. ' \ 'In order to use Apiseeds you need to get a free API key at https://apiseeds.com. ' \ - 'Want to contribute? Check out the project page at https://github.com/avalloneandrea/apiseeds-lyrics!''' + 'Want to contribute? Check out the project page at https://github.com/avalloneandrea/apiseeds-lyrics!' PLUGIN_VERSION = '1.0.2' PLUGIN_API_VERSIONS = ['2.0.0'] PLUGIN_LICENSE = 'MIT' @@ -24,11 +24,11 @@ class ApiseedsLyricsMetadataProcessor(object): apiseeds_host = 'orion.apiseeds.com' apiseeds_port = 443 - apiseeds_rate_limit = 60 * 1000 / 200 + apiseeds_delay = 60 * 1000 / 200 # 200 requests per minute def __init__(self): super(ApiseedsLyricsMetadataProcessor, self).__init__() - ratecontrol.set_minimum_delay((self.apiseeds_host, self.apiseeds_port), self.apiseeds_rate_limit) + ratecontrol.set_minimum_delay((self.apiseeds_host, self.apiseeds_port), self.apiseeds_delay) def process_metadata(self, album, metadata, track, release): @@ -62,10 +62,10 @@ def process_metadata(self, album, metadata, track, release): queryargs=apiseeds_params) @staticmethod - def process_response(album, metadata, document, reply, error): + def process_response(album, metadata, response, reply, error): try: - lyrics = document['result']['track']['text'] + lyrics = response['result']['track']['text'] metadata['lyrics'] = lyrics log.debug('{}: lyrics found for track {}'.format(PLUGIN_NAME, metadata['title'])) From 068ba7d7d83f020828cdddc617324591f8e297ed Mon Sep 17 00:00:00 2001 From: Andrea Avallone Date: Mon, 11 Mar 2019 19:54:55 +0100 Subject: [PATCH 115/123] Set apiseeds_apikey default value --- plugins/apiseeds_lyrics/apiseeds_lyrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/apiseeds_lyrics/apiseeds_lyrics.py b/plugins/apiseeds_lyrics/apiseeds_lyrics.py index 2d02dafc..214f028e 100644 --- a/plugins/apiseeds_lyrics/apiseeds_lyrics.py +++ b/plugins/apiseeds_lyrics/apiseeds_lyrics.py @@ -83,7 +83,7 @@ class ApiseedsLyricsOptionsPage(OptionsPage): TITLE = 'Apiseeds Lyrics' PARENT = 'plugins' - options = [TextOption('setting', 'apiseeds_apikey', None)] + options = [TextOption('setting', 'apiseeds_apikey', '')] def __init__(self, parent=None): From f7b036fa8470201d677c894af5676d0e351eaa7c Mon Sep 17 00:00:00 2001 From: Andrea Avallone Date: Mon, 11 Mar 2019 19:55:27 +0100 Subject: [PATCH 116/123] URL encode artist and title --- plugins/apiseeds_lyrics/apiseeds_lyrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/apiseeds_lyrics/apiseeds_lyrics.py b/plugins/apiseeds_lyrics/apiseeds_lyrics.py index 214f028e..7aaddb2d 100644 --- a/plugins/apiseeds_lyrics/apiseeds_lyrics.py +++ b/plugins/apiseeds_lyrics/apiseeds_lyrics.py @@ -47,7 +47,7 @@ def process_metadata(self, album, metadata, track, release): log.debug('{}: title is missing, please provide a valid value'.format(PLUGIN_NAME)) return - apiseeds_path = '/api/music/lyric/{}/{}'.format(artist, title) + apiseeds_path = '/api/music/lyric/{}/{}'.format(quote(artist, ''), quote(title, '')) apiseeds_params = {'apikey': apikey} album._requests += 1 log.debug('{}: GET {}?{}'.format(PLUGIN_NAME, quote(apiseeds_path), urlencode(apiseeds_params))) From 59affce1080e977362d68044057348fc228a45af Mon Sep 17 00:00:00 2001 From: Andrea Avallone Date: Mon, 11 Mar 2019 19:55:59 +0100 Subject: [PATCH 117/123] Set exceptions to catch --- plugins/apiseeds_lyrics/apiseeds_lyrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/apiseeds_lyrics/apiseeds_lyrics.py b/plugins/apiseeds_lyrics/apiseeds_lyrics.py index 7aaddb2d..4ed082af 100644 --- a/plugins/apiseeds_lyrics/apiseeds_lyrics.py +++ b/plugins/apiseeds_lyrics/apiseeds_lyrics.py @@ -69,7 +69,7 @@ def process_response(album, metadata, response, reply, error): metadata['lyrics'] = lyrics log.debug('{}: lyrics found for track {}'.format(PLUGIN_NAME, metadata['title'])) - except: + except (TypeError, KeyError): log.debug('{}: lyrics NOT found for track {}'.format(PLUGIN_NAME, metadata['title'])) finally: From 2a4019be612a113b6d4223ff26aa7fd778d4fa06 Mon Sep 17 00:00:00 2001 From: Andrea Avallone Date: Mon, 11 Mar 2019 19:56:19 +0100 Subject: [PATCH 118/123] Version 1.0.3 --- plugins/apiseeds_lyrics/apiseeds_lyrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/apiseeds_lyrics/apiseeds_lyrics.py b/plugins/apiseeds_lyrics/apiseeds_lyrics.py index 4ed082af..2c1e9f60 100644 --- a/plugins/apiseeds_lyrics/apiseeds_lyrics.py +++ b/plugins/apiseeds_lyrics/apiseeds_lyrics.py @@ -14,7 +14,7 @@ 'Lyrics provided are for educational purposes and personal use only. Commercial use is not allowed. ' \ 'In order to use Apiseeds you need to get a free API key at https://apiseeds.com. ' \ 'Want to contribute? Check out the project page at https://github.com/avalloneandrea/apiseeds-lyrics!' -PLUGIN_VERSION = '1.0.2' +PLUGIN_VERSION = '1.0.3' PLUGIN_API_VERSIONS = ['2.0.0'] PLUGIN_LICENSE = 'MIT' PLUGIN_LICENSE_URL = 'https://opensource.org/licenses/MIT' From 8cb6013adcdf57430f81386deb86eae208930049 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Tue, 12 Mar 2019 17:43:34 +0100 Subject: [PATCH 119/123] apiseeds_lyrics: Fixed API version specification --- plugins/apiseeds_lyrics/apiseeds_lyrics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/apiseeds_lyrics/apiseeds_lyrics.py b/plugins/apiseeds_lyrics/apiseeds_lyrics.py index 2c1e9f60..bc7235bc 100644 --- a/plugins/apiseeds_lyrics/apiseeds_lyrics.py +++ b/plugins/apiseeds_lyrics/apiseeds_lyrics.py @@ -14,8 +14,8 @@ 'Lyrics provided are for educational purposes and personal use only. Commercial use is not allowed. ' \ 'In order to use Apiseeds you need to get a free API key at https://apiseeds.com. ' \ 'Want to contribute? Check out the project page at https://github.com/avalloneandrea/apiseeds-lyrics!' -PLUGIN_VERSION = '1.0.3' -PLUGIN_API_VERSIONS = ['2.0.0'] +PLUGIN_VERSION = '1.0.4' +PLUGIN_API_VERSIONS = ['2.0'] PLUGIN_LICENSE = 'MIT' PLUGIN_LICENSE_URL = 'https://opensource.org/licenses/MIT' From 88b3ebd5513b984d7b133f1070786331da7e14f8 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 25 Mar 2019 15:05:48 +0100 Subject: [PATCH 120/123] viewvariables: Fixed Picard 2.2 compatibility The plugin relied on the assumption that Metadata is a dict. --- plugins/viewvariables/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/viewvariables/__init__.py b/plugins/viewvariables/__init__.py index 44169407..fb383508 100644 --- a/plugins/viewvariables/__init__.py +++ b/plugins/viewvariables/__init__.py @@ -3,7 +3,7 @@ PLUGIN_NAME = 'View script variables' PLUGIN_AUTHOR = 'Sophist' PLUGIN_DESCRIPTION = '''Display a dialog box listing the metadata variables for the track / file.''' -PLUGIN_VERSION = '0.6' +PLUGIN_VERSION = '0.7' PLUGIN_API_VERSIONS = ['2.0'] PLUGIN_LICENSE = "GPL-2.0" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" @@ -87,7 +87,7 @@ def _display_metadata(self, metadata): i += 1 key_item.setText("_" + key[1:] if key.startswith('~') else key) if key in metadata: - value = dict.get(metadata, key) + value = metadata.getall(key) if len(value) == 1 and value[0] != '': value = value[0] else: From 23d3b7e8c0e5e4a378929e6c52fab3a20f7eb05a Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 3 Jun 2019 14:04:34 +0200 Subject: [PATCH 121/123] loadasnat: Fixed moving multiple files for a track --- plugins/loadasnat/loadasnat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/loadasnat/loadasnat.py b/plugins/loadasnat/loadasnat.py index 1b2ce52d..c50b00d1 100644 --- a/plugins/loadasnat/loadasnat.py +++ b/plugins/loadasnat/loadasnat.py @@ -47,7 +47,7 @@ def callback(self, objs): for track in tracks: nat = self.tagger.load_nat( track.metadata['musicbrainz_recordingid']) - for file in track.iterfiles(): + for file in list(track.linked_files): file.move(nat) metadata = file.metadata metadata.delete('albumartist') From ffe7b2fbba73a5978d1b4f086558bfca7faf9eb0 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Mon, 3 Jun 2019 14:04:34 +0200 Subject: [PATCH 122/123] loadasnat: Fixed moving multiple files for a track --- plugins/loadasnat/loadasnat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/loadasnat/loadasnat.py b/plugins/loadasnat/loadasnat.py index 1b2ce52d..2a8c7ce2 100644 --- a/plugins/loadasnat/loadasnat.py +++ b/plugins/loadasnat/loadasnat.py @@ -22,7 +22,7 @@ PLUGIN_DESCRIPTION = ("Allows loading selected tracks as non-album tracks. " "Useful for tagging single tracks where you do not care " "about the album.") -PLUGIN_VERSION = "0.2" +PLUGIN_VERSION = "0.3" PLUGIN_API_VERSIONS = ["1.4.0", "2.0", "2.1", "2.2"] PLUGIN_LICENSE = "GPL-2.0-or-later" PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html" @@ -47,7 +47,7 @@ def callback(self, objs): for track in tracks: nat = self.tagger.load_nat( track.metadata['musicbrainz_recordingid']) - for file in track.iterfiles(): + for file in list(track.linked_files): file.move(nat) metadata = file.metadata metadata.delete('albumartist') From af3de8bb57dc23075aa53645399c48d906d03b83 Mon Sep 17 00:00:00 2001 From: Philipp Wolfer Date: Thu, 6 Jun 2019 17:48:52 +0200 Subject: [PATCH 123/123] Properly generate ZIPs for single file Python packages If a plugin consists only of a single __init__.py file treat it as a module. --- generate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/generate.py b/generate.py index 1c80f67a..836f5ca3 100755 --- a/generate.py +++ b/generate.py @@ -74,7 +74,8 @@ def zip_files(dest_dir): file_path = os.path.join(root, filename) plugin_files.append(file_path) - if len(plugin_files) == 1: + if (len(plugin_files) == 1 + and os.path.basename(plugin_files[0]) != '__init__.py'): # There's only one file, put it directly into the zipfile archive.write(plugin_files[0], os.path.basename(plugin_files[0]),