From d2a60b4ecea5221639e6a95fe0f6556a5f428d6b Mon Sep 17 00:00:00 2001
From: Bob Swift
Date: Mon, 4 Jul 2022 09:48:36 -0600
Subject: [PATCH 1/2] Add Genre Mapper plugin
---
plugins/genre_mapper/__init__.py | 169 +++++++++++++++++
plugins/genre_mapper/options_genre_mapper.ui | 173 ++++++++++++++++++
.../genre_mapper/ui_options_genre_mapper.py | 108 +++++++++++
3 files changed, 450 insertions(+)
create mode 100644 plugins/genre_mapper/__init__.py
create mode 100644 plugins/genre_mapper/options_genre_mapper.ui
create mode 100644 plugins/genre_mapper/ui_options_genre_mapper.py
diff --git a/plugins/genre_mapper/__init__.py b/plugins/genre_mapper/__init__.py
new file mode 100644
index 00000000..c4fb7653
--- /dev/null
+++ b/plugins/genre_mapper/__init__.py
@@ -0,0 +1,169 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2022 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 = 'Genre Mapper'
+PLUGIN_AUTHOR = 'Bob Swift'
+PLUGIN_DESCRIPTION = '''
+This plugin provides the ability to standardize genres in the "genre"
+tag by matching the genres as found to a standard genre as defined in
+the genre replacement mapping configuration option. Once installed a
+settings page will be added to Picard's options, which is where the
+plugin is configured.
+
+Please see the user guide on GitHub for more information.
+'''
+PLUGIN_VERSION = '0.3'
+PLUGIN_API_VERSIONS = ['2.0', '2.1', '2.2', '2.3', '2.6', '2.7', '2.8']
+PLUGIN_LICENSE = "GPL-2.0"
+PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.txt"
+
+import re
+
+from picard import (
+ config,
+ log,
+)
+from picard.metadata import (
+ MULTI_VALUED_JOINER,
+ register_track_metadata_processor,
+)
+from picard.plugin import PluginPriority
+from picard.plugins.genre_mapper.ui_options_genre_mapper import (
+ Ui_GenreMapperOptionsPage,
+)
+
+from picard.ui.options import (
+ OptionsPage,
+ register_options_page,
+)
+
+
+pairs_split = re.compile(r"\r\n|\n\r|\n").split
+
+OPT_MATCH_ENABLED = 'genre_mapper_enabled'
+OPT_MATCH_PAIRS = 'genre_mapper_replacement_pairs'
+OPT_MATCH_FIRST = 'genre_mapper_apply_first_match_only'
+
+
+class GenreMappingPairs():
+ pairs = []
+
+ @classmethod
+ def refresh(cls):
+ log.debug("%s: Refreshing the genre replacement maps processing pairs.", PLUGIN_NAME,)
+ if not config.Option.exists("setting", OPT_MATCH_PAIRS):
+ log.warning("%s: Unable to read the '%s' setting.", PLUGIN_NAME, OPT_MATCH_PAIRS,)
+ return
+
+ def _make_re(map_string):
+ # Replace period with temporary placeholder character (newline)
+ re_string = str(map_string).strip().replace('.', '\n')
+
+ # Convert wildcard characters to regular expression equivalents
+ re_string = re_string.replace('*', '.*').replace('?', '.')
+
+ # Escape carat and dollar sign in regular expression
+ re_string = re_string.replace('^', '\\^').replace('$', '\\$')
+
+ # Replace temporary placeholder characters with escaped periods
+ # and wrap expression with '^' and '$' to force full match
+ re_string = '^' + re_string.replace('\n', '\\.') + '$'
+
+ return re_string
+
+ cls.pairs = []
+ for pair in pairs_split(config.setting[OPT_MATCH_PAIRS]):
+ if "=" not in pair:
+ continue
+ original, replacement = pair.split('=', 1)
+ original = original.strip()
+ if not original:
+ continue
+ replacement = replacement.strip()
+ cls.pairs.append((_make_re(original), replacement))
+ log.debug('%s: Add genre mapping pair: "%s" = "%s"', PLUGIN_NAME, original, replacement,)
+ if not cls.pairs:
+ log.debug("%s: No genre replacement maps defined.", PLUGIN_NAME,)
+
+
+class GenreMapperOptionsPage(OptionsPage):
+
+ NAME = "genre_mapper"
+ TITLE = "Genre Mapper"
+ PARENT = "plugins"
+
+ options = [
+ config.TextOption("setting", OPT_MATCH_PAIRS, ''),
+ config.BoolOption("setting", OPT_MATCH_FIRST, False),
+ config.BoolOption("setting", OPT_MATCH_ENABLED, False),
+ ]
+
+ def __init__(self, parent=None):
+ super().__init__(parent)
+ self.ui = Ui_GenreMapperOptionsPage()
+ self.ui.setupUi(self)
+
+ def load(self):
+ # Enable external link
+ self.ui.format_description.setOpenExternalLinks(True)
+
+ self.ui.genre_mapper_replacement_pairs.setPlainText(config.setting[OPT_MATCH_PAIRS])
+ self.ui.genre_mapper_first_match_only.setChecked(config.setting[OPT_MATCH_FIRST])
+ self.ui.cb_enable_genre_mapping.setChecked(config.setting[OPT_MATCH_ENABLED])
+
+ self.ui.cb_enable_genre_mapping.stateChanged.connect(self._set_enabled_state)
+ self._set_enabled_state()
+
+ def save(self):
+ config.setting[OPT_MATCH_PAIRS] = self.ui.genre_mapper_replacement_pairs.toPlainText()
+ config.setting[OPT_MATCH_FIRST] = self.ui.genre_mapper_first_match_only.isChecked()
+ config.setting[OPT_MATCH_ENABLED] = self.ui.cb_enable_genre_mapping.isChecked()
+
+ GenreMappingPairs.refresh()
+
+ def _set_enabled_state(self, *args):
+ self.ui.gm_replacement_pairs.setEnabled(self.ui.cb_enable_genre_mapping.isChecked())
+
+
+def track_genre_mapper(album, metadata, *args):
+ if not config.setting[OPT_MATCH_ENABLED]:
+ return
+ if 'genre' not in metadata or not metadata['genre']:
+ log.debug("%s: No genres found for: \"%s\"", PLUGIN_NAME, metadata['title'],)
+ return
+ genres = set()
+ metadata_genres = str(metadata['genre']).split(MULTI_VALUED_JOINER)
+ for genre in metadata_genres:
+ for (original, replacement) in GenreMappingPairs.pairs:
+ if genre and re.fullmatch(original, genre, re.IGNORECASE):
+ genre = replacement
+ if config.setting[OPT_MATCH_FIRST]:
+ break
+ if genre:
+ genres.add(genre.title())
+ genres = sorted(genres)
+ log.debug("{0}: Genres updated from {1} to {2}".format(PLUGIN_NAME, metadata_genres, genres,))
+ metadata['genre'] = genres
+
+
+# Register the plugin to run at a LOW priority.
+register_track_metadata_processor(track_genre_mapper, priority=PluginPriority.LOW)
+register_options_page(GenreMapperOptionsPage)
+
+GenreMappingPairs.refresh()
diff --git a/plugins/genre_mapper/options_genre_mapper.ui b/plugins/genre_mapper/options_genre_mapper.ui
new file mode 100644
index 00000000..ba18753c
--- /dev/null
+++ b/plugins/genre_mapper/options_genre_mapper.ui
@@ -0,0 +1,173 @@
+
+
+ GenreMapperOptionsPage
+
+
+
+ 0
+ 0
+ 398
+ 568
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16
+
+ -
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 50
+
+
+
+
+ 75
+ true
+
+
+
+ Genre Mapper
+
+
+
+ 9
+
+
+ 9
+
+
+ 9
+
+
+ 1
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 50
+ false
+
+
+
+ <html><head/><body><p>These are the original / replacement pairs used to map one genre entry to another. Each pair must be entered on a separate line in the form:</p><p><span style=" font-weight:600;">[genre match test string]=[replacement genre]</span></p><p>Supported wildcards in the test string part of the mapping include '*' and '?' to match any number of characters and a single character respectively. Blank lines and lines beginning with an equals sign (=) will be ignored. Case-insensitive tests are used when matching. Replacements will be made in the order they are found in the list. An example for mapping all types of Rock genres (e.g. Country Rock, Hard Rock, Progressive Rock) to "Rock" would be done using the following line:</p><pre style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Courier New'; font-size:10pt; font-weight:600;">*rock*=Rock</span></pre><p>For more information please see the <a href="https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/genre_mapper/docs/README.md"><span style=" text-decoration: underline; color:#0000ff;">User Guide</span></a> on GitHub.<br/></p></body></html>
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+ Enable genre mapping
+
+
+
+ -
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 50
+
+
+
+
+ 75
+ true
+
+
+
+ Replacement Pairs
+
+
+
-
+
+
+
+ 50
+ false
+
+
+
+ Apply only the first matching replacement
+
+
+ false
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 50
+
+
+
+
+ Courier New
+ 10
+
+
+
+ Enter replacement pairs (one per line)
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/plugins/genre_mapper/ui_options_genre_mapper.py b/plugins/genre_mapper/ui_options_genre_mapper.py
new file mode 100644
index 00000000..19e83c30
--- /dev/null
+++ b/plugins/genre_mapper/ui_options_genre_mapper.py
@@ -0,0 +1,108 @@
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file '.\plugins\genre_mapper\options_genre_mapper.ui'
+#
+# Created by: PyQt5 UI code generator 5.14.1
+#
+# WARNING! All changes made in this file will be lost!
+
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+
+class Ui_GenreMapperOptionsPage(object):
+ def setupUi(self, GenreMapperOptionsPage):
+ GenreMapperOptionsPage.setObjectName("GenreMapperOptionsPage")
+ GenreMapperOptionsPage.resize(398, 568)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(GenreMapperOptionsPage.sizePolicy().hasHeightForWidth())
+ GenreMapperOptionsPage.setSizePolicy(sizePolicy)
+ self.vboxlayout = QtWidgets.QVBoxLayout(GenreMapperOptionsPage)
+ self.vboxlayout.setSpacing(16)
+ self.vboxlayout.setObjectName("vboxlayout")
+ self.verticalLayout_4 = QtWidgets.QVBoxLayout()
+ self.verticalLayout_4.setObjectName("verticalLayout_4")
+ self.gm_description = QtWidgets.QGroupBox(GenreMapperOptionsPage)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.gm_description.sizePolicy().hasHeightForWidth())
+ self.gm_description.setSizePolicy(sizePolicy)
+ self.gm_description.setMinimumSize(QtCore.QSize(0, 50))
+ font = QtGui.QFont()
+ font.setBold(True)
+ font.setWeight(75)
+ self.gm_description.setFont(font)
+ self.gm_description.setObjectName("gm_description")
+ self.verticalLayout = QtWidgets.QVBoxLayout(self.gm_description)
+ self.verticalLayout.setContentsMargins(9, 9, 9, 1)
+ self.verticalLayout.setObjectName("verticalLayout")
+ self.format_description = QtWidgets.QLabel(self.gm_description)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.format_description.sizePolicy().hasHeightForWidth())
+ self.format_description.setSizePolicy(sizePolicy)
+ font = QtGui.QFont()
+ font.setBold(False)
+ font.setWeight(50)
+ self.format_description.setFont(font)
+ self.format_description.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
+ self.format_description.setWordWrap(True)
+ self.format_description.setObjectName("format_description")
+ self.verticalLayout.addWidget(self.format_description)
+ self.verticalLayout_4.addWidget(self.gm_description)
+ self.cb_enable_genre_mapping = QtWidgets.QCheckBox(GenreMapperOptionsPage)
+ self.cb_enable_genre_mapping.setObjectName("cb_enable_genre_mapping")
+ self.verticalLayout_4.addWidget(self.cb_enable_genre_mapping)
+ self.gm_replacement_pairs = QtWidgets.QGroupBox(GenreMapperOptionsPage)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.MinimumExpanding)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.gm_replacement_pairs.sizePolicy().hasHeightForWidth())
+ self.gm_replacement_pairs.setSizePolicy(sizePolicy)
+ self.gm_replacement_pairs.setMinimumSize(QtCore.QSize(0, 50))
+ font = QtGui.QFont()
+ font.setBold(True)
+ font.setWeight(75)
+ self.gm_replacement_pairs.setFont(font)
+ self.gm_replacement_pairs.setObjectName("gm_replacement_pairs")
+ self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.gm_replacement_pairs)
+ self.verticalLayout_3.setObjectName("verticalLayout_3")
+ self.genre_mapper_first_match_only = QtWidgets.QCheckBox(self.gm_replacement_pairs)
+ font = QtGui.QFont()
+ font.setBold(False)
+ font.setWeight(50)
+ self.genre_mapper_first_match_only.setFont(font)
+ self.genre_mapper_first_match_only.setChecked(False)
+ self.genre_mapper_first_match_only.setObjectName("genre_mapper_first_match_only")
+ self.verticalLayout_3.addWidget(self.genre_mapper_first_match_only)
+ self.genre_mapper_replacement_pairs = QtWidgets.QPlainTextEdit(self.gm_replacement_pairs)
+ sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum)
+ sizePolicy.setHorizontalStretch(0)
+ sizePolicy.setVerticalStretch(0)
+ sizePolicy.setHeightForWidth(self.genre_mapper_replacement_pairs.sizePolicy().hasHeightForWidth())
+ self.genre_mapper_replacement_pairs.setSizePolicy(sizePolicy)
+ self.genre_mapper_replacement_pairs.setMinimumSize(QtCore.QSize(0, 50))
+ font = QtGui.QFont()
+ font.setFamily("Courier New")
+ font.setPointSize(10)
+ self.genre_mapper_replacement_pairs.setFont(font)
+ self.genre_mapper_replacement_pairs.setObjectName("genre_mapper_replacement_pairs")
+ self.verticalLayout_3.addWidget(self.genre_mapper_replacement_pairs)
+ self.verticalLayout_4.addWidget(self.gm_replacement_pairs)
+ self.vboxlayout.addLayout(self.verticalLayout_4)
+
+ self.retranslateUi(GenreMapperOptionsPage)
+ QtCore.QMetaObject.connectSlotsByName(GenreMapperOptionsPage)
+
+ def retranslateUi(self, GenreMapperOptionsPage):
+ _translate = QtCore.QCoreApplication.translate
+ self.gm_description.setTitle(_translate("GenreMapperOptionsPage", "Genre Mapper"))
+ self.format_description.setText(_translate("GenreMapperOptionsPage", "
These are the original / replacement pairs used to map one genre entry to another. Each pair must be entered on a separate line in the form:
[genre match test string]=[replacement genre]
Supported wildcards in the test string part of the mapping include \'*\' and \'?\' to match any number of characters and a single character respectively. Blank lines and lines beginning with an equals sign (=) will be ignored. Case-insensitive tests are used when matching. Replacements will be made in the order they are found in the list. An example for mapping all types of Rock genres (e.g. Country Rock, Hard Rock, Progressive Rock) to "Rock" would be done using the following line:
*rock*=Rock
For more information please see the User Guide on GitHub.
"))
+ self.cb_enable_genre_mapping.setText(_translate("GenreMapperOptionsPage", "Enable genre mapping"))
+ self.gm_replacement_pairs.setTitle(_translate("GenreMapperOptionsPage", "Replacement Pairs"))
+ self.genre_mapper_first_match_only.setText(_translate("GenreMapperOptionsPage", "Apply only the first matching replacement"))
+ self.genre_mapper_replacement_pairs.setPlaceholderText(_translate("GenreMapperOptionsPage", "Enter replacement pairs (one per line)"))
From d7acc14e1aea2c7ce295c4486747718a57e10414 Mon Sep 17 00:00:00 2001
From: Bob Swift
Date: Mon, 4 Jul 2022 13:07:05 -0600
Subject: [PATCH 2/2] Add option to use regular expressions for match strings
---
plugins/genre_mapper/__init__.py | 21 ++++++++-------
plugins/genre_mapper/options_genre_mapper.ui | 27 ++++++++++++++++++-
.../genre_mapper/ui_options_genre_mapper.py | 16 ++++++++++-
3 files changed, 52 insertions(+), 12 deletions(-)
diff --git a/plugins/genre_mapper/__init__.py b/plugins/genre_mapper/__init__.py
index c4fb7653..8bfdddfc 100644
--- a/plugins/genre_mapper/__init__.py
+++ b/plugins/genre_mapper/__init__.py
@@ -28,7 +28,7 @@
Please see the user guide on GitHub for more information.
'''
-PLUGIN_VERSION = '0.3'
+PLUGIN_VERSION = '0.4'
PLUGIN_API_VERSIONS = ['2.0', '2.1', '2.2', '2.3', '2.6', '2.7', '2.8']
PLUGIN_LICENSE = "GPL-2.0"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.txt"
@@ -59,6 +59,7 @@
OPT_MATCH_ENABLED = 'genre_mapper_enabled'
OPT_MATCH_PAIRS = 'genre_mapper_replacement_pairs'
OPT_MATCH_FIRST = 'genre_mapper_apply_first_match_only'
+OPT_MATCH_REGEX = 'genre_mapper_use_regex'
class GenreMappingPairs():
@@ -66,7 +67,8 @@ class GenreMappingPairs():
@classmethod
def refresh(cls):
- log.debug("%s: Refreshing the genre replacement maps processing pairs.", PLUGIN_NAME,)
+ log.debug("%s: Refreshing the genre replacement maps processing pairs using '%s' translation.",
+ PLUGIN_NAME, 'RegEx' if config.Option.exists("setting", OPT_MATCH_REGEX) and config.setting[OPT_MATCH_REGEX] else 'Simple',)
if not config.Option.exists("setting", OPT_MATCH_PAIRS):
log.warning("%s: Unable to read the '%s' setting.", PLUGIN_NAME, OPT_MATCH_PAIRS,)
return
@@ -74,17 +76,13 @@ def refresh(cls):
def _make_re(map_string):
# Replace period with temporary placeholder character (newline)
re_string = str(map_string).strip().replace('.', '\n')
-
# Convert wildcard characters to regular expression equivalents
re_string = re_string.replace('*', '.*').replace('?', '.')
-
- # Escape carat and dollar sign in regular expression
+ # Escape carat and dollar sign for regular expression
re_string = re_string.replace('^', '\\^').replace('$', '\\$')
-
# Replace temporary placeholder characters with escaped periods
- # and wrap expression with '^' and '$' to force full match
re_string = '^' + re_string.replace('\n', '\\.') + '$'
-
+ # Return regular expression with carat and dollar sign to force match condition on full string
return re_string
cls.pairs = []
@@ -96,7 +94,7 @@ def _make_re(map_string):
if not original:
continue
replacement = replacement.strip()
- cls.pairs.append((_make_re(original), replacement))
+ cls.pairs.append((original if config.setting[OPT_MATCH_REGEX] else _make_re(original), replacement))
log.debug('%s: Add genre mapping pair: "%s" = "%s"', PLUGIN_NAME, original, replacement,)
if not cls.pairs:
log.debug("%s: No genre replacement maps defined.", PLUGIN_NAME,)
@@ -112,6 +110,7 @@ class GenreMapperOptionsPage(OptionsPage):
config.TextOption("setting", OPT_MATCH_PAIRS, ''),
config.BoolOption("setting", OPT_MATCH_FIRST, False),
config.BoolOption("setting", OPT_MATCH_ENABLED, False),
+ config.BoolOption("setting", OPT_MATCH_REGEX, False),
]
def __init__(self, parent=None):
@@ -126,6 +125,7 @@ def load(self):
self.ui.genre_mapper_replacement_pairs.setPlainText(config.setting[OPT_MATCH_PAIRS])
self.ui.genre_mapper_first_match_only.setChecked(config.setting[OPT_MATCH_FIRST])
self.ui.cb_enable_genre_mapping.setChecked(config.setting[OPT_MATCH_ENABLED])
+ self.ui.cb_use_regex.setChecked(config.setting[OPT_MATCH_REGEX])
self.ui.cb_enable_genre_mapping.stateChanged.connect(self._set_enabled_state)
self._set_enabled_state()
@@ -134,6 +134,7 @@ def save(self):
config.setting[OPT_MATCH_PAIRS] = self.ui.genre_mapper_replacement_pairs.toPlainText()
config.setting[OPT_MATCH_FIRST] = self.ui.genre_mapper_first_match_only.isChecked()
config.setting[OPT_MATCH_ENABLED] = self.ui.cb_enable_genre_mapping.isChecked()
+ config.setting[OPT_MATCH_REGEX] = self.ui.cb_use_regex.isChecked()
GenreMappingPairs.refresh()
@@ -151,7 +152,7 @@ def track_genre_mapper(album, metadata, *args):
metadata_genres = str(metadata['genre']).split(MULTI_VALUED_JOINER)
for genre in metadata_genres:
for (original, replacement) in GenreMappingPairs.pairs:
- if genre and re.fullmatch(original, genre, re.IGNORECASE):
+ if genre and re.search(original, genre, re.IGNORECASE):
genre = replacement
if config.setting[OPT_MATCH_FIRST]:
break
diff --git a/plugins/genre_mapper/options_genre_mapper.ui b/plugins/genre_mapper/options_genre_mapper.ui
index ba18753c..bb85ee51 100644
--- a/plugins/genre_mapper/options_genre_mapper.ui
+++ b/plugins/genre_mapper/options_genre_mapper.ui
@@ -73,7 +73,7 @@
- <html><head/><body><p>These are the original / replacement pairs used to map one genre entry to another. Each pair must be entered on a separate line in the form:</p><p><span style=" font-weight:600;">[genre match test string]=[replacement genre]</span></p><p>Supported wildcards in the test string part of the mapping include '*' and '?' to match any number of characters and a single character respectively. Blank lines and lines beginning with an equals sign (=) will be ignored. Case-insensitive tests are used when matching. Replacements will be made in the order they are found in the list. An example for mapping all types of Rock genres (e.g. Country Rock, Hard Rock, Progressive Rock) to "Rock" would be done using the following line:</p><pre style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Courier New'; font-size:10pt; font-weight:600;">*rock*=Rock</span></pre><p>For more information please see the <a href="https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/genre_mapper/docs/README.md"><span style=" text-decoration: underline; color:#0000ff;">User Guide</span></a> on GitHub.<br/></p></body></html>
+ <html><head/><body><p>These are the original / replacement pairs used to map one genre entry to another. Each pair must be entered on a separate line in the form:</p><p><span style=" font-weight:600;">[genre match test string]=[replacement genre]</span></p><p>Unless the "regular expressions" option is enabled, supported wildcards in the test string part of the mapping include '*' and '?' to match any number of characters and a single character respectively. An example for mapping all types of Rock genres (e.g. Country Rock, Hard Rock, Progressive Rock) to "Rock" would be done using the following line:</p><p><span style=" font-family:'Courier New'; font-size:10pt; font-weight:600;">*rock*=Rock</span></p><p>Blank lines and lines beginning with an equals sign (=) will be ignored. Case-insensitive tests are used when matching. Replacements will be made in the order they are found in the list.</p><p>For more information please see the <a href="https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/genre_mapper/docs/README.md"><span style=" text-decoration: underline; color:#0000ff;">User Guide</span></a> on GitHub.<br/></p></body></html>
Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
@@ -120,6 +120,19 @@
Replacement Pairs
+ -
+
+
+
+ 50
+ false
+
+
+
+ Match tests are entered as regular expressions
+
+
+
-
@@ -156,6 +169,12 @@
10
+
+ true
+
+
+ QPlainTextEdit::NoWrap
+
Enter replacement pairs (one per line)
@@ -168,6 +187,12 @@
+
+ cb_enable_genre_mapping
+ cb_use_regex
+ genre_mapper_first_match_only
+ genre_mapper_replacement_pairs
+
diff --git a/plugins/genre_mapper/ui_options_genre_mapper.py b/plugins/genre_mapper/ui_options_genre_mapper.py
index 19e83c30..8b481eb9 100644
--- a/plugins/genre_mapper/ui_options_genre_mapper.py
+++ b/plugins/genre_mapper/ui_options_genre_mapper.py
@@ -58,6 +58,7 @@ def setupUi(self, GenreMapperOptionsPage):
self.cb_enable_genre_mapping.setObjectName("cb_enable_genre_mapping")
self.verticalLayout_4.addWidget(self.cb_enable_genre_mapping)
self.gm_replacement_pairs = QtWidgets.QGroupBox(GenreMapperOptionsPage)
+ self.gm_replacement_pairs.setEnabled(True)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.MinimumExpanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
@@ -71,6 +72,13 @@ def setupUi(self, GenreMapperOptionsPage):
self.gm_replacement_pairs.setObjectName("gm_replacement_pairs")
self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.gm_replacement_pairs)
self.verticalLayout_3.setObjectName("verticalLayout_3")
+ self.cb_use_regex = QtWidgets.QCheckBox(self.gm_replacement_pairs)
+ font = QtGui.QFont()
+ font.setBold(False)
+ font.setWeight(50)
+ self.cb_use_regex.setFont(font)
+ self.cb_use_regex.setObjectName("cb_use_regex")
+ self.verticalLayout_3.addWidget(self.cb_use_regex)
self.genre_mapper_first_match_only = QtWidgets.QCheckBox(self.gm_replacement_pairs)
font = QtGui.QFont()
font.setBold(False)
@@ -90,6 +98,8 @@ def setupUi(self, GenreMapperOptionsPage):
font.setFamily("Courier New")
font.setPointSize(10)
self.genre_mapper_replacement_pairs.setFont(font)
+ self.genre_mapper_replacement_pairs.setTabChangesFocus(True)
+ self.genre_mapper_replacement_pairs.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap)
self.genre_mapper_replacement_pairs.setObjectName("genre_mapper_replacement_pairs")
self.verticalLayout_3.addWidget(self.genre_mapper_replacement_pairs)
self.verticalLayout_4.addWidget(self.gm_replacement_pairs)
@@ -97,12 +107,16 @@ def setupUi(self, GenreMapperOptionsPage):
self.retranslateUi(GenreMapperOptionsPage)
QtCore.QMetaObject.connectSlotsByName(GenreMapperOptionsPage)
+ GenreMapperOptionsPage.setTabOrder(self.cb_enable_genre_mapping, self.cb_use_regex)
+ GenreMapperOptionsPage.setTabOrder(self.cb_use_regex, self.genre_mapper_first_match_only)
+ GenreMapperOptionsPage.setTabOrder(self.genre_mapper_first_match_only, self.genre_mapper_replacement_pairs)
def retranslateUi(self, GenreMapperOptionsPage):
_translate = QtCore.QCoreApplication.translate
self.gm_description.setTitle(_translate("GenreMapperOptionsPage", "Genre Mapper"))
- self.format_description.setText(_translate("GenreMapperOptionsPage", "These are the original / replacement pairs used to map one genre entry to another. Each pair must be entered on a separate line in the form:
[genre match test string]=[replacement genre]
Supported wildcards in the test string part of the mapping include \'*\' and \'?\' to match any number of characters and a single character respectively. Blank lines and lines beginning with an equals sign (=) will be ignored. Case-insensitive tests are used when matching. Replacements will be made in the order they are found in the list. An example for mapping all types of Rock genres (e.g. Country Rock, Hard Rock, Progressive Rock) to "Rock" would be done using the following line:
*rock*=Rock
For more information please see the User Guide on GitHub.
"))
+ self.format_description.setText(_translate("GenreMapperOptionsPage", "These are the original / replacement pairs used to map one genre entry to another. Each pair must be entered on a separate line in the form:
[genre match test string]=[replacement genre]
Unless the "regular expressions" option is enabled, supported wildcards in the test string part of the mapping include \'*\' and \'?\' to match any number of characters and a single character respectively. An example for mapping all types of Rock genres (e.g. Country Rock, Hard Rock, Progressive Rock) to "Rock" would be done using the following line:
*rock*=Rock
Blank lines and lines beginning with an equals sign (=) will be ignored. Case-insensitive tests are used when matching. Replacements will be made in the order they are found in the list.
For more information please see the User Guide on GitHub.
"))
self.cb_enable_genre_mapping.setText(_translate("GenreMapperOptionsPage", "Enable genre mapping"))
self.gm_replacement_pairs.setTitle(_translate("GenreMapperOptionsPage", "Replacement Pairs"))
+ self.cb_use_regex.setText(_translate("GenreMapperOptionsPage", "Match tests are entered as regular expressions"))
self.genre_mapper_first_match_only.setText(_translate("GenreMapperOptionsPage", "Apply only the first matching replacement"))
self.genre_mapper_replacement_pairs.setPlaceholderText(_translate("GenreMapperOptionsPage", "Enter replacement pairs (one per line)"))