Compare commits

..

6 Commits

3 changed files with 152 additions and 36 deletions

View File

@@ -2,67 +2,102 @@ from picard import config, log
from picard.ui.options import OptionsPage, register_options_page
from picard.metadata import register_track_metadata_processor
from PyQt5 import QtWidgets
import re
from .constants import *
def process_track(_, metadata, track, __):
disambiguation = track["recording"]["disambiguation"] if "disambiguation" in track["recording"] else ""
def process_track(_, metadata, track, __):
disambiguation = track["recording"]["disambiguation"] if "recording" in track and "disambiguation" in track["recording"] else track["disambiguation"] if "disambiguation" in track else ""
album_disambiguation = metadata["~releasecomment"] if "~releasecomment" in metadata else ""
explicit_keywords = config.setting["ecd2itat_explicit_keywords"] or DEFAULT_EXPLICIT_KEYWORDS
if isinstance(explicit_keywords, str):
explicit_keywords = [kw.strip() for kw in explicit_keywords.split(",")]
explicit_keywords = sorted((kw for kw in explicit_keywords if kw), key=len, reverse=True)
clean_keywords = config.setting["ecd2itat_clean_keywords"] or DEFAULT_CLEAN_KEYWORDS
if isinstance(clean_keywords, str):
clean_keywords = [kw.strip() for kw in clean_keywords.split(",")]
clean_keywords = sorted((kw for kw in clean_keywords if kw), key=len, reverse=True)
explicit_match = next((kw for kw in explicit_keywords if kw.lower() in disambiguation.strip().lower()), None)
clean_match = next((kw for kw in clean_keywords if kw.lower() in disambiguation.strip().lower()), None)
disambiguation_lc = disambiguation.strip().lower()
album_disambiguation_lc = album_disambiguation.strip().lower()
explicit_match = next((kw for kw in explicit_keywords if kw.lower() in disambiguation_lc), None)
clean_match = next((kw for kw in clean_keywords if kw.lower() in disambiguation_lc), None)
album_explicit_match = next((kw for kw in explicit_keywords if kw.lower() in album_disambiguation_lc), None)
album_clean_match = next((kw for kw in clean_keywords if kw.lower() in album_disambiguation_lc), None)
stripped_disambiguation = disambiguation
stripped_album_disambiguation = album_disambiguation
if explicit_match:
metadata["itunesadvisory"] = iTunesAdvisory.EXPLICIT.value
if config.setting["ecd2itat_save_rtng"]:
metadata["rtng"] = rtng.EXPLICIT.value
if config.setting["ecd2itat_strip_keyword_from_disambiguation"]:
metadata["subtitle"] = strip_keyword_from_disambiguation(disambiguation, explicit_match)
metadata["musicbrainz_albumcomment"] = strip_keyword_from_disambiguation(album_disambiguation, explicit_match)
elif clean_match:
metadata["itunesadvisory"] = iTunesAdvisory.CLEAN.value
if (config.setting["ecd2itat_save_rtng"]):
if config.setting["ecd2itat_save_rtng"]:
metadata["rtng"] = rtng.CLEAN.value
elif album_explicit_match:
metadata["itunesadvisory"] = iTunesAdvisory.EXPLICIT.value
if config.setting["ecd2itat_save_rtng"]:
metadata["rtng"] = rtng.EXPLICIT.value
elif album_clean_match:
metadata["itunesadvisory"] = iTunesAdvisory.CLEAN.value
if config.setting["ecd2itat_save_rtng"]:
metadata["rtng"] = rtng.CLEAN.value
if config.setting["ecd2itat_strip_keyword_from_disambiguation"]:
metadata["subtitle"] = strip_keyword_from_disambiguation(disambiguation, clean_match)
metadata["musicbrainz_albumcomment"] = strip_keyword_from_disambiguation(album_disambiguation, clean_match)
if config.setting["ecd2itat_strip_keyword_from_disambiguation"]:
if explicit_match:
stripped_disambiguation = strip_keyword_from_disambiguation(disambiguation, explicit_match)
elif clean_match:
stripped_disambiguation = strip_keyword_from_disambiguation(disambiguation, clean_match)
if album_explicit_match:
stripped_album_disambiguation = strip_keyword_from_disambiguation(album_disambiguation, album_explicit_match)
elif album_clean_match:
stripped_album_disambiguation = strip_keyword_from_disambiguation(album_disambiguation, album_clean_match)
metadata["~recordingcomment"] = stripped_disambiguation
metadata["~releasecomment"] = stripped_album_disambiguation
def strip_keyword_from_disambiguation(disambiguation, keyword):
# If the keyword is the entire disambiguation, return an empty string (e,g. "explicit" becomes "")
if disambiguation.strip().lower() == keyword.lower():
disambiguation_stripped = disambiguation.strip()
keyword_stripped = keyword.strip()
if not disambiguation_stripped or not keyword_stripped:
return disambiguation
keyword_pattern = re.compile(rf"(?<!\\w){re.escape(keyword_stripped)}(?!\\w)", re.IGNORECASE)
if not keyword_pattern.search(disambiguation_stripped):
log.debug(f"Keyword '{keyword}' not found in disambiguation '{disambiguation}' for stripping")
return disambiguation
parts = re.split(r"\s*(,|\||-)\s*", disambiguation_stripped)
terms = parts[::2]
separators = parts[1::2]
cleaned_terms = []
for term in terms:
cleaned_term = keyword_pattern.sub("", term)
cleaned_term = re.sub(r"\(\s*\)", "", cleaned_term)
cleaned_term = re.sub(r"\s{2,}", " ", cleaned_term).strip()
cleaned_terms.append(cleaned_term)
non_empty_indexes = [i for i, term in enumerate(cleaned_terms) if term]
if not non_empty_indexes:
return ""
# keyword is at the start of the disambiguation (e.g. "explicit, original mix" becomes "original mix")
if disambiguation.strip().lower().startswith(keyword.lower() + ","):
return disambiguation[len(keyword)+1:].strip()
result = cleaned_terms[non_empty_indexes[0]]
for index in non_empty_indexes[1:]:
separator = separators[index - 1] if index - 1 < len(separators) else ","
if separator == ",":
result += f", {cleaned_terms[index]}"
else:
result += f" {separator} {cleaned_terms[index]}"
# keyword is at the end of the disambiguation (e.g. "original mix,explicit" becomes "original mix")
if disambiguation.strip().lower().endswith("," + keyword.lower()):
return disambiguation[:-len(keyword)-1].strip()
# keyword is at the end with a preceding comma and space (e.g. "original mix, explicit" becomes "original mix")
if disambiguation.strip().lower().endswith(", " + keyword.lower()):
return disambiguation[:-len(keyword)-2].strip()
# keyword is in the middle of the disambiguation (e.g. "album version, explicit, remix" becomes "album version, remix")
if "," + keyword.lower() + "," in disambiguation.strip().lower():
return disambiguation.replace("," + keyword + ",", ",").strip()
# Return the disambiguation unchanged if the keyword is not found or cannot be stripped
log.debug(f"Keyword '{keyword}' not found in disambiguation '{disambiguation}' for stripping")
return disambiguation
return result
class ECD2ITatOptionsPage(OptionsPage):
NAME = "ecd2itat"
@@ -86,7 +121,7 @@ class ECD2ITatOptionsPage(OptionsPage):
self.save_rtng_checkbox.setToolTip("Save the rtng tag")
self.ecd2itat_strip_keyword_from_disambiguation_checkbox = QtWidgets.QCheckBox("Strip keywords from disambiguation", self)
self.ecd2itat_strip_keyword_from_disambiguation_checkbox.setToolTip("Try to remove the keyword from the disambiguation after processing, enable this if you don't want the keywords to be visible in the disambiguation and save it under \"subtitle\" (applies to albums too under \"musicbrainz_albumcomment\")")
self.ecd2itat_strip_keyword_from_disambiguation_checkbox.setToolTip("Try to remove the keyword from the disambiguation after processing, enable this if you don't want the keywords to be visible in the disambiguation (applies to albums too)")
options_layout.addWidget(self.save_rtng_checkbox)
options_layout.addWidget(self.ecd2itat_strip_keyword_from_disambiguation_checkbox)

View File

@@ -5,7 +5,7 @@ from picard.config import BoolOption, TextOption, Option
PLUGIN_NAME = "ecd2iTat"
PLUGIN_AUTHOR = "cy1der"
PLUGIN_DESCRIPTION = "Convert disambiguations containing \"explicit\"/\"clean\" (and others) keywords to proper tags so clients can display the 🅴/🅲 symbol"
PLUGIN_VERSION = "1.0.0"
PLUGIN_VERSION = "1.0.3"
PLUGIN_API_VERSIONS = ["2.7", "2.8", "2.9", "2.10", "2.11", "2.12", "2.13"]
PLUGIN_LICENSE = "GPL-2.0-or-later"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"

81
tests.py Normal file
View File

@@ -0,0 +1,81 @@
import ast
from pathlib import Path
from types import SimpleNamespace
from typing import Callable, cast
import pytest
def _load_strip_keyword_function():
module_path = Path(__file__).resolve().parent / "__init__.py"
source = module_path.read_text(encoding="utf-8")
tree = ast.parse(source, filename=str(module_path))
function_node = next(
node
for node in tree.body
if isinstance(node, ast.FunctionDef) and node.name == "strip_keyword_from_disambiguation"
)
isolated_module = ast.Module(
body=[
ast.Import(names=[ast.alias(name="re")]),
function_node,
],
type_ignores=[],
)
ast.fix_missing_locations(isolated_module)
namespace: dict[str, object] = {
"log": SimpleNamespace(debug=lambda *args, **kwargs: None),
}
exec(compile(isolated_module, str(module_path), "exec"), namespace)
return cast(Callable[[str, str], str], namespace["strip_keyword_from_disambiguation"])
strip_keyword_from_disambiguation = _load_strip_keyword_function()
@pytest.mark.parametrize(
"disambiguation, keyword, expected",
[
("explicit", "explicit", ""),
("original mix, explicit", "explicit", "original mix"),
("original mix explicit", "explicit", "original mix"),
("explicit, original mix", "explicit", "original mix"),
("explicit album version", "explicit", "album version"),
("explicit - original mix", "explicit", "original mix"),
("original mix,explicit", "explicit", "original mix"),
("original mix - explicit", "explicit", "original mix"),
("original mix (explicit)", "explicit", "original mix"),
("original mix ( explicit )", "explicit", "original mix"),
("original explicit mix", "explicit", "original mix"),
("album version, explicit, remix", "explicit", "album version, remix"),
("album version - explicit - remix", "explicit", "album version - remix"),
("album version | explicit | remix", "explicit", "album version | remix"),
],
)
def test_strip_keyword_supported_patterns(disambiguation, keyword, expected):
assert strip_keyword_from_disambiguation(disambiguation, keyword) == expected
def test_strip_keyword_is_case_insensitive():
result = strip_keyword_from_disambiguation("Original Mix, EXPLICIT", "explicit")
assert result == "Original Mix"
def test_strip_keyword_returns_original_when_not_found():
original = "original mix, clean"
assert strip_keyword_from_disambiguation(original, "explicit") == original
@pytest.mark.parametrize(
"disambiguation, keyword",
[
("", "explicit"),
("original mix", ""),
("", ""),
],
)
def test_strip_keyword_handles_empty_inputs(disambiguation, keyword):
assert strip_keyword_from_disambiguation(disambiguation, keyword) == disambiguation