Compare commits

...

4 Commits

3 changed files with 143 additions and 33 deletions

View File

@@ -2,73 +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, __):
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"]:
stripped_disambiguation = strip_keyword_from_disambiguation(disambiguation, explicit_match)
stripped_album_disambiguation = 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"]:
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)
stripped_album_disambiguation = strip_keyword_from_disambiguation(album_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"

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.1"
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