Skip to content

Commit

Permalink
Upload new plugin enhanced titles
Browse files Browse the repository at this point in the history
  • Loading branch information
twodoorcoupe committed Jan 18, 2024
1 parent 0900edb commit b4cdc8a
Show file tree
Hide file tree
Showing 3 changed files with 593 additions and 0 deletions.
331 changes: 331 additions & 0 deletions plugins/enhanced_titles/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2024 Giorgio Fontanive (twodoorcoupe)
#
# 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.

PLUGIN_NAME = "Enhanced Titles"
PLUGIN_AUTHOR = "Giorgio Fontanive"
PLUGIN_DESCRIPTION = """
This plugin sets the albumsort and titlesort tags. It also provides the script
functions $swapprefix_lang, $delprefix_lang and $title_lang. The languages
included at the moment are English, Spanish, Italian, German, French and Portuguese.
The functions do the same thing as their original counterparts, but take
multiple languages as parameters. If no languages are provided, all the available ones are
included. Languages are provided with ISO 639-3 codes: eng, spa, ita, fra, deu, por.
Tagging and checking aliases can be disabled in the plugin's options page, found
under "plugins". Checking aliases will slow down processing.
"""
PLUGIN_VERSION = "0.1"
PLUGIN_API_VERSIONS = ["2.10"]
PLUGIN_LICENSE = "GPL-2.0"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"

from picard import log, config
from picard.plugin import PluginPriority
from picard.metadata import register_album_metadata_processor, register_track_metadata_processor
from picard.script import register_script_function
from picard.script.functions import func_swapprefix, func_delprefix
from picard.webservice.api_helpers import MBAPIHelper

from picard.ui.options import OptionsPage, register_options_page
from .ui_options_enhanced_titles import Ui_EnhancedTitlesOptions

from functools import partial
import re

# Options.
KEEP_ALLCAPS = "et_keep_allcaps"
ENABLE_TAGGING = "et_enable_tagging"
CHECK_ALBUM = "et_check_album_aliases"
CHECK_TRACK = "et_check_track_aliases"

# ISO 639-3 language codes.
ADDED_LANGUAGES = {"eng", "spa", "ita", "fra", "deu", "por"}

_articles = {
"eng" : ["the", "a", "an"],
"spa" : ["el", "los", "la", "las", "lo", "un", "unos", "una", "unas"],
"ita" : ["il", "l'", "la", "i", "gli", "le", "un", "uno", "una", "un'"],
"fra" : ["le", "la", "les", "un", "une", "des", "l'"],
"deu" : ["der", "den", "die", "das", "dem", "des", "den"],
"por" : ["o", "os", "a", "as", "um", "uns", "uma", "umas"]
}

# Prepositions and conjunctions with 3 letters or less.
_other_minor_words = {
"eng" : ["so", "yet", "as", "at", "by", "for", "in", "of", "off", "on", "per",
"to", "up", "via", "and", "as", "but", "for", "if", "nor", "or"],
"spa" : ["mas", "que", "que", "en", "con", "por", "de", "y", "e", "o", "u", "si", "ni"],
"ita" : ["di", "a", "da", "in", "con", "su", "per", "tra", "fra", "e", "o", "ma", "se"],
"fra" : ["à", "de", "en", "par", "sur", "et", "ou", "que", "si"],
"deu" : ["bis", "für", "um", "an", "auf", "in", "vor", "aus", "bei", "mit",
"von", "zu", "la", "so", "daß", "als", "ob", "ehe"],
"por" : ["dem", "em", "por", "ao", "à", "aos", "às", "do", "da", "dos", "das",
"no", "na", "nos", "nas", "num", "dum", "e", "mas", "até", "em", "ou",
"que", "se", "por"]
}


class ReleaseGroupHelper(MBAPIHelper):
"""API Helper to retreive release group information.
"""

Check notice on line 83 in plugins/enhanced_titles/__init__.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

plugins/enhanced_titles/__init__.py#L83

Trailing whitespace
def get_release_group_by_id(self, release_id, handler, inc = None):
"""Gets the information for a release group.
"""
return self._get_by_id("release-group", release_id, handler, inc)


class SortTagger:
"""Sets the titlesort and albumsort tags.
First, it checks if there is already a sort name available in one of the
aliases. If it does not find any, it swaps the prefix if there is one in
the title.
"""

def _select_alias(self, aliases, name):
"""Selects the first alias where the names match and the sort name is
different.
Args:
aliases (list): One dictionary for each alias available.
name (str): Title of the album/track.
Returns:
(str): The sort name of the first useful alias it finds. None if
none are found.
For example, "The Beatles" has alias "Le Double Blanc" with sort name
"Double Blanc, Le", so it's not considered. But it also has alias
"The Beatles" with sort name "Beatles, The", so this one is chosen.
Another example, "The Continuing Story of Bungalow Bill" has an alias
with the sort name equal to the title, this is not considered because
it makes more sense to swap the prefix.
"""
for alias in aliases:
sortname = alias["sort-name"]
if (alias["name"].casefold() == name.casefold() and

Check notice on line 119 in plugins/enhanced_titles/__init__.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

plugins/enhanced_titles/__init__.py#L119

Trailing whitespace
not sortname.casefold() == name.casefold()):
log.info("Enhanced Titles: sort name found for \"" + name + "\", \"" + sortname + "\".")
return sortname
log.info("Enhanced Titles: no proper sort name found for \"" + name + "\".")
return None

def _response_handler(self, document, reply, error, metadata = None, field = None):
"""Handles the response from MusicBrainz.
Args:
metadata (MetaData): The object that needs to be updated.
field (str): Either "title" or "album", depending on what is being
updated.
"""
sortname = ""
try:
if document:
if error:
log.error("Enhanced Titles: information retrieval error.")
if document["aliases"]:
sortname = self._select_alias(document["aliases"], metadata[field])
else:
log.info("Enhanced Titles: no aliases found for \"" + metadata[field] + "\".")
finally:
if sortname:
sortfield = field + "sort"
metadata[sortfield] = sortname
else:
self._swapprefix(metadata, field)

def _swapprefix(self, metadata, field):
"""Swaps the prefix of the title based on the album/track language."
If no language information is found, then it uses all languages available.
Otherwise, if none of the languages are available, it just copies the title.
Otherwise it uses exclusively the languages that are also available.
"""
sortfield = field + "sort"
languages = [metadata["language"], metadata["_releaselanguage"]]
languages = [language for language in languages if language]
if not languages:
metadata[sortfield] = func_swapprefix(None, metadata[field], *_create_prefixes_list())
else:
languages = [language for language in languages if language in ADDED_LANGUAGES]
if not languages:
metadata[sortfield] = metadata[field]
else:
metadata[sortfield] = func_swapprefix(None, metadata[field], *_create_prefixes_list(languages))

def set_track_titlesort(self, album, metadata, track, release):
"""Sets the track's titlesort field.
"""
if config.setting[ENABLE_TAGGING]:
handler = partial(self._response_handler, metadata = metadata, field = "title")
if config.setting[CHECK_TRACK]:
MBAPIHelper(album.tagger.webservice).get_track_by_id(
metadata["musicbrainz_recordingid"],

Check notice on line 176 in plugins/enhanced_titles/__init__.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

plugins/enhanced_titles/__init__.py#L176

Trailing whitespace
handler,

Check notice on line 177 in plugins/enhanced_titles/__init__.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

plugins/enhanced_titles/__init__.py#L177

Trailing whitespace
inc = ["aliases"]
)
else:
self._swapprefix(metadata, "title")

def set_album_titlesort(self, album, metadata, release):
"""Sets the album's albumsort field.
"""
if config.setting[ENABLE_TAGGING]:
handler = partial(self._response_handler, metadata = metadata, field = "album")
if config.setting[CHECK_ALBUM]:
ReleaseGroupHelper(album.tagger.webservice).get_release_group_by_id(
metadata["musicbrainz_releasegroupid"],

Check notice on line 190 in plugins/enhanced_titles/__init__.py

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

plugins/enhanced_titles/__init__.py#L190

Trailing whitespace
handler,
inc = ["aliases"]
)
else:
self._swapprefix(metadata, "album")


def _create_prefixes_list(languages = None, is_title = False):
"""Creates a list of all the prefixes or minor words for all the given languages.
Args:
languages (list): All the languages to be considered.
is_title (bool): If true, only articles are included, with a lower case
and a capitalized copy for each. This is because the
swapprefix and delprefix functions are case sensitive.
If false, also prepositions and conjunctions are included,
all in lowercase.
Returns:
(set): The set of prefixes or minor words.
The available languages are saved with ISO 639-3 codes.
"""
prefixes = set()
if not languages:
languages = ADDED_LANGUAGES
else:
languages = [lang.lower()[:3] for lang in languages]
languages = [lang for lang in languages if lang in ADDED_LANGUAGES]
for language in languages:
prefixes.update(_articles[language])
if is_title:
prefixes.update(_other_minor_words[language])
else:
prefixes.update([article.capitalize() for article in _articles[language]])
return prefixes

def _title_case(text, lower_case_words):
"""Returns the text in titlecase.
If a word has an apostrophe and the segment to its left is an article,
it capitalizes only the word on the right. Otherwise it capitalizes
only the word on the left.
For example, "let's groove" becomes "Let's Groove", but "voglio l'anima"
becomes "Voglio l'Anima".
"""
new_text = []
words = re.split(r"([\w']+)", text.strip().lower().replace("’", "'"))
words = [word for word in words if word]
for word in words:
if "'" in word: # Apply the rule described above
split = word.split("'")
if split[0] + "'" in lower_case_words:
split[1] = split[1].capitalize()
else:
split[0] = split[0].capitalize()
word = "'".join(split)
elif not word in lower_case_words:
word = word.capitalize()
new_text.append(word)
if new_text:
new_text[0] = new_text[0].capitalize()
return "".join(new_text)
else:
return ""

swapprefix_lang_documentation = N_(
"""`$swapprefix_lang(text,language1,language2,...)`
Moves the prefix to the end of the text. It uses a list prefixes
taken from the specified languages.
Multiple languages can be added as seperate parameters.
If none are provided, it uses all the available ones.
""")
def swapprefix_lang(parser, text, *languages):
return func_swapprefix(parser, text, *_create_prefixes_list(languages)) if text else ""

delprefix_lang_documentation = N_(
"""`$delprefix_lang(text,language1,language2,...)`
Deletes the prefix from the text. It uses a list prefixes
taken from the specified languages.
Multiple languages can be added as seperate parameters.
If none are provided, it uses all the available ones.
""")
def delprefix_lang(parser, text, *languages):
return func_delprefix(parser, text, *_create_prefixes_list(languages)) if text else ""

title_lang_documentation = N_(
"""`$title_lang(text,language1,language2,...)`
Makes the text title case based on the minor words of the specified languages.
Multiple languages can be added as seperate parameters.
If none are provided, it uses all the available ones.
""")
def title_lang(parser, text, *languages):
if text.upper() == text and config.setting[KEEP_ALLCAPS]:
return text
return _title_case(text, _create_prefixes_list(languages, True)) if text else ""


class EnhancedTitlesOptions(OptionsPage):
"""Options page found under the "plugins" page.
"""

NAME = "enhanced_titles"
TITLE = "Enhanced Titles"
PARENT = "plugins"

options = [
config.BoolOption("setting", KEEP_ALLCAPS, False),
config.BoolOption("setting", ENABLE_TAGGING, True),
config.BoolOption("setting", CHECK_ALBUM, True),
config.BoolOption("setting", CHECK_TRACK, False),
]

def __init__(self, parent = None):
super(EnhancedTitlesOptions, self).__init__(parent)
self.ui = Ui_EnhancedTitlesOptions()
self.ui.setupUi(self)

def load(self):
self.ui.check_allcaps.setChecked(config.setting[KEEP_ALLCAPS])
self.ui.check_tagging.setChecked(config.setting[ENABLE_TAGGING])
self.ui.check_album_aliases.setChecked(config.setting[CHECK_ALBUM])
self.ui.check_track_aliases.setChecked(config.setting[CHECK_TRACK])

def save(self):
config.setting[KEEP_ALLCAPS] = self.ui.check_allcaps.isChecked()
config.setting[ENABLE_TAGGING] = self.ui.check_tagging.isChecked()
config.setting[CHECK_ALBUM] = self.ui.check_album_aliases.isChecked()
config.setting[CHECK_TRACK] = self.ui.check_track_aliases.isChecked()


sort_tagger = SortTagger()
register_track_metadata_processor(sort_tagger.set_track_titlesort, priority = PluginPriority.LOW)
register_album_metadata_processor(sort_tagger.set_album_titlesort, priority = PluginPriority.LOW)
register_script_function(swapprefix_lang, check_argcount = False, documentation = swapprefix_lang_documentation)
register_script_function(delprefix_lang, check_argcount = False, documentation = delprefix_lang_documentation)
register_script_function(title_lang, check_argcount = False, documentation = title_lang_documentation)
register_options_page(EnhancedTitlesOptions)
Loading

0 comments on commit b4cdc8a

Please sign in to comment.