-
-
Notifications
You must be signed in to change notification settings - Fork 95
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #333 from rdswift/genre_mapper_plugin
Add Genre Mapper plugin
- Loading branch information
Showing
3 changed files
with
490 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
# -*- 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. | ||
<br /><br /> | ||
Please see the <a href="https://github.com/rdswift/picard-plugins/blob/2.0_RDS_Plugins/plugins/genre_mapper/docs/README.md">user guide</a> on GitHub for more information. | ||
''' | ||
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" | ||
|
||
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' | ||
OPT_MATCH_REGEX = 'genre_mapper_use_regex' | ||
|
||
|
||
class GenreMappingPairs(): | ||
pairs = [] | ||
|
||
@classmethod | ||
def refresh(cls): | ||
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 | ||
|
||
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 for regular expression | ||
re_string = re_string.replace('^', '\\^').replace('$', '\\$') | ||
# Replace temporary placeholder characters with escaped periods | ||
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 = [] | ||
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((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,) | ||
|
||
|
||
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), | ||
config.BoolOption("setting", OPT_MATCH_REGEX, 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_use_regex.setChecked(config.setting[OPT_MATCH_REGEX]) | ||
|
||
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() | ||
config.setting[OPT_MATCH_REGEX] = self.ui.cb_use_regex.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.search(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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<ui version="4.0"> | ||
<class>GenreMapperOptionsPage</class> | ||
<widget class="QWidget" name="GenreMapperOptionsPage"> | ||
<property name="geometry"> | ||
<rect> | ||
<x>0</x> | ||
<y>0</y> | ||
<width>398</width> | ||
<height>568</height> | ||
</rect> | ||
</property> | ||
<property name="sizePolicy"> | ||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred"> | ||
<horstretch>0</horstretch> | ||
<verstretch>0</verstretch> | ||
</sizepolicy> | ||
</property> | ||
<layout class="QVBoxLayout"> | ||
<property name="spacing"> | ||
<number>16</number> | ||
</property> | ||
<item> | ||
<layout class="QVBoxLayout" name="verticalLayout_4"> | ||
<item> | ||
<widget class="QGroupBox" name="gm_description"> | ||
<property name="sizePolicy"> | ||
<sizepolicy hsizetype="Preferred" vsizetype="Minimum"> | ||
<horstretch>0</horstretch> | ||
<verstretch>0</verstretch> | ||
</sizepolicy> | ||
</property> | ||
<property name="minimumSize"> | ||
<size> | ||
<width>0</width> | ||
<height>50</height> | ||
</size> | ||
</property> | ||
<property name="font"> | ||
<font> | ||
<weight>75</weight> | ||
<bold>true</bold> | ||
</font> | ||
</property> | ||
<property name="title"> | ||
<string>Genre Mapper</string> | ||
</property> | ||
<layout class="QVBoxLayout" name="verticalLayout"> | ||
<property name="leftMargin"> | ||
<number>9</number> | ||
</property> | ||
<property name="topMargin"> | ||
<number>9</number> | ||
</property> | ||
<property name="rightMargin"> | ||
<number>9</number> | ||
</property> | ||
<property name="bottomMargin"> | ||
<number>1</number> | ||
</property> | ||
<item> | ||
<widget class="QLabel" name="format_description"> | ||
<property name="sizePolicy"> | ||
<sizepolicy hsizetype="Preferred" vsizetype="Minimum"> | ||
<horstretch>0</horstretch> | ||
<verstretch>0</verstretch> | ||
</sizepolicy> | ||
</property> | ||
<property name="font"> | ||
<font> | ||
<weight>50</weight> | ||
<bold>false</bold> | ||
</font> | ||
</property> | ||
<property name="text"> | ||
<string><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 &quot;regular expressions&quot; 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 &quot;Rock&quot; 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></string> | ||
</property> | ||
<property name="alignment"> | ||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> | ||
</property> | ||
<property name="wordWrap"> | ||
<bool>true</bool> | ||
</property> | ||
</widget> | ||
</item> | ||
</layout> | ||
</widget> | ||
</item> | ||
<item> | ||
<widget class="QCheckBox" name="cb_enable_genre_mapping"> | ||
<property name="text"> | ||
<string>Enable genre mapping</string> | ||
</property> | ||
</widget> | ||
</item> | ||
<item> | ||
<widget class="QGroupBox" name="gm_replacement_pairs"> | ||
<property name="enabled"> | ||
<bool>true</bool> | ||
</property> | ||
<property name="sizePolicy"> | ||
<sizepolicy hsizetype="Preferred" vsizetype="MinimumExpanding"> | ||
<horstretch>0</horstretch> | ||
<verstretch>0</verstretch> | ||
</sizepolicy> | ||
</property> | ||
<property name="minimumSize"> | ||
<size> | ||
<width>0</width> | ||
<height>50</height> | ||
</size> | ||
</property> | ||
<property name="font"> | ||
<font> | ||
<weight>75</weight> | ||
<bold>true</bold> | ||
</font> | ||
</property> | ||
<property name="title"> | ||
<string>Replacement Pairs</string> | ||
</property> | ||
<layout class="QVBoxLayout" name="verticalLayout_3"> | ||
<item> | ||
<widget class="QCheckBox" name="cb_use_regex"> | ||
<property name="font"> | ||
<font> | ||
<weight>50</weight> | ||
<bold>false</bold> | ||
</font> | ||
</property> | ||
<property name="text"> | ||
<string>Match tests are entered as regular expressions</string> | ||
</property> | ||
</widget> | ||
</item> | ||
<item> | ||
<widget class="QCheckBox" name="genre_mapper_first_match_only"> | ||
<property name="font"> | ||
<font> | ||
<weight>50</weight> | ||
<bold>false</bold> | ||
</font> | ||
</property> | ||
<property name="text"> | ||
<string>Apply only the first matching replacement</string> | ||
</property> | ||
<property name="checked"> | ||
<bool>false</bool> | ||
</property> | ||
</widget> | ||
</item> | ||
<item> | ||
<widget class="QPlainTextEdit" name="genre_mapper_replacement_pairs"> | ||
<property name="sizePolicy"> | ||
<sizepolicy hsizetype="Preferred" vsizetype="Minimum"> | ||
<horstretch>0</horstretch> | ||
<verstretch>0</verstretch> | ||
</sizepolicy> | ||
</property> | ||
<property name="minimumSize"> | ||
<size> | ||
<width>0</width> | ||
<height>50</height> | ||
</size> | ||
</property> | ||
<property name="font"> | ||
<font> | ||
<family>Courier New</family> | ||
<pointsize>10</pointsize> | ||
</font> | ||
</property> | ||
<property name="tabChangesFocus"> | ||
<bool>true</bool> | ||
</property> | ||
<property name="lineWrapMode"> | ||
<enum>QPlainTextEdit::NoWrap</enum> | ||
</property> | ||
<property name="placeholderText"> | ||
<string>Enter replacement pairs (one per line)</string> | ||
</property> | ||
</widget> | ||
</item> | ||
</layout> | ||
</widget> | ||
</item> | ||
</layout> | ||
</item> | ||
</layout> | ||
</widget> | ||
<tabstops> | ||
<tabstop>cb_enable_genre_mapping</tabstop> | ||
<tabstop>cb_use_regex</tabstop> | ||
<tabstop>genre_mapper_first_match_only</tabstop> | ||
<tabstop>genre_mapper_replacement_pairs</tabstop> | ||
</tabstops> | ||
<resources/> | ||
<connections/> | ||
</ui> |
Oops, something went wrong.