diff --git a/.gitignore b/.gitignore index 36b13f1..ba0430d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,176 +1 @@ -# ---> Python -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -#uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/latest/usage/project/#working-with-version-control -.pdm.toml -.pdm-python -.pdm-build/ - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - -# Ruff stuff: -.ruff_cache/ - -# PyPI configuration file -.pypirc - +__pycache__/ \ No newline at end of file diff --git a/README.md b/README.md index 1c3cb12..e055353 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ # ecd2iTat -Convert disambiguations containing "explicit"/"clean" (and others) to proper tags so clients can display the 🅴/🅲 symbol +Convert disambiguations containing "explicit"/"clean" (and others) keywords to proper tags so clients can display the 🅴/🅲 symbol ## What the hell is that name? -(E)xplicit/(c)lean (d)isambiguation [to] (iT)unes (a)dvisory (t)ag* +(E)xplicit/(c)lean (d)isambiguation [to] (iT)unes (a)dvisory (t)ag\* -* `itunesadvisory` (and `rtng` because why not) +\* `itunesadvisory` (and `rtng` because why not) ## Getting Started 1. Clone this repository to your Picard plugins folder 2. Enable the plugin -3. Configure it to your liking (optional) \ No newline at end of file +3. Configure it to your liking (optional) diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..357a1fc --- /dev/null +++ b/__init__.py @@ -0,0 +1,138 @@ +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 + +from .constants import * + +def process_track(_, metadata, track, __): + disambiguation = track["recording"]["disambiguation"] if "disambiguation" in track["recording"] 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(",")] + + 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(",")] + + 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) + + 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"]): + 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) + +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(): + 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() + + # 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 + +class ECD2ITatOptionsPage(OptionsPage): + NAME = "ecd2itat" + TITLE = "ecd2iTat" + PARENT = "plugins" + + options = CONFIG_OPTIONS + + def __init__(self, parent=None) -> None: + super().__init__(parent) + self.setup_ui() + + def setup_ui(self): + layout = QtWidgets.QVBoxLayout(self) + + options_group = QtWidgets.QGroupBox("Options", self) + options_group.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) + options_layout = QtWidgets.QVBoxLayout(options_group) + + self.save_rtng_checkbox = QtWidgets.QCheckBox("Save rtng", self) + 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\")") + + options_layout.addWidget(self.save_rtng_checkbox) + options_layout.addWidget(self.ecd2itat_strip_keyword_from_disambiguation_checkbox) + + layout.addWidget(options_group) + + keywords_group = QtWidgets.QGroupBox("Keywords", self) + keywords_group.setSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Minimum) + keywords_layout = QtWidgets.QVBoxLayout(keywords_group) + + explicit_keywords_layout = QtWidgets.QHBoxLayout() + explicit_keywords_label = QtWidgets.QLabel("Explicit:", self) + self.explicit_keywords_input = QtWidgets.QLineEdit(self) + self.explicit_keywords_input.setPlaceholderText("Comma-separated list of keywords to consider as explicit") + self.explicit_keywords_input.setToolTip("Comma-separated list of keywords to consider as explicit") + + explicit_keywords_layout.addWidget(explicit_keywords_label) + explicit_keywords_layout.addWidget(self.explicit_keywords_input) + + clean_keywords_layout = QtWidgets.QHBoxLayout() + clean_keywords_label = QtWidgets.QLabel("Clean:", self) + self.clean_keywords_input = QtWidgets.QLineEdit(self) + self.clean_keywords_input.setPlaceholderText("Comma-separated list of keywords to consider as clean") + self.clean_keywords_input.setToolTip("Comma-separated list of keywords to consider as clean") + + clean_keywords_layout.addWidget(clean_keywords_label) + clean_keywords_layout.addWidget(self.clean_keywords_input) + + keywords_layout.addLayout(explicit_keywords_layout) + keywords_layout.addLayout(clean_keywords_layout) + + layout.addWidget(keywords_group) + + layout.addStretch() + + def load(self): + self.save_rtng_checkbox.setChecked(config.setting["ecd2itat_save_rtng"] or False) + self.ecd2itat_strip_keyword_from_disambiguation_checkbox.setChecked(config.setting["ecd2itat_strip_keyword_from_disambiguation"] or False) + self.explicit_keywords_input.setText(config.setting["ecd2itat_explicit_keywords"] or ", ".join(DEFAULT_EXPLICIT_KEYWORDS)) + self.clean_keywords_input.setText(config.setting["ecd2itat_clean_keywords"] or ", ".join(DEFAULT_CLEAN_KEYWORDS)) + + def save(self): + config.setting["ecd2itat_save_rtng"] = self.save_rtng_checkbox.isChecked() + config.setting["ecd2itat_strip_keyword_from_disambiguation"] = self.ecd2itat_strip_keyword_from_disambiguation_checkbox.isChecked() + config.setting["ecd2itat_explicit_keywords"] = self.explicit_keywords_input.text() + config.setting["ecd2itat_clean_keywords"] = self.clean_keywords_input.text() + +register_options_page(ECD2ITatOptionsPage) +register_track_metadata_processor(process_track) \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..aa380d1 --- /dev/null +++ b/constants.py @@ -0,0 +1,45 @@ +from typing import List +from enum import Enum +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_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" +PLUGIN_USER_GUIDE_URL = "https://git.altaiar.dev/ahmed/ecd2iTat" + +class iTunesAdvisory(Enum): + EXPLICIT = 1 + CLEAN = 2 + +class rtng(Enum): + EXPLICIT = 4 + CLEAN = 2 + +DEFAULT_EXPLICIT_KEYWORDS: List[str] = [ + "explicit release version", + "dirty release version", + "explicit version", + "dirty version", + "explicit", + "dirty" +] + +DEFAULT_CLEAN_KEYWORDS: List[str] = [ + "censored release version", + "clean release version", + "censored version", + "clean version", + "censored", + "clean" +] + +CONFIG_OPTIONS: List[Option] = [ + TextOption("setting", "ecd2itat_explicit_keywords", ", ".join(DEFAULT_EXPLICIT_KEYWORDS)), + TextOption("setting", "ecd2itat_clean_keywords", ", ".join(DEFAULT_CLEAN_KEYWORDS)), + BoolOption("setting", "ecd2itat_save_rtng", False), + BoolOption("setting", "ecd2itat_strip_keyword_from_disambiguation", False), +] \ No newline at end of file