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
+
+ -
+
+
+