mirror of
https://github.com/beetbox/beets.git
synced 2025-12-31 13:02:47 +01:00
add the_artist
This commit is contained in:
parent
631485c55b
commit
5628232bc4
3 changed files with 144 additions and 76 deletions
|
|
@ -29,38 +29,13 @@ from beets.plugins import BeetsPlugin
|
|||
__author__ = "henryoberholtzer@gmail.com"
|
||||
__version__ = "1.0"
|
||||
|
||||
# These fields are excluded to avoid modifying anything
|
||||
# that may be case sensistive, or important to database
|
||||
# function
|
||||
EXCLUDED_INFO_FIELDS: set[str] = {
|
||||
"acoustid_fingerprint",
|
||||
"acoustid_id",
|
||||
"artists_ids",
|
||||
"asin",
|
||||
"deezer_track_id",
|
||||
"format",
|
||||
"id",
|
||||
"isrc",
|
||||
"mb_workid",
|
||||
"mb_trackid",
|
||||
"mb_albumid",
|
||||
"mb_artistid",
|
||||
"mb_artistids",
|
||||
"mb_albumartistid",
|
||||
"mb_albumartistids",
|
||||
"mb_releasetrackid",
|
||||
"mb_releasegroupid",
|
||||
"bitrate_mode",
|
||||
"encoder_info",
|
||||
"encoder_settings",
|
||||
}
|
||||
|
||||
|
||||
class TitlecasePlugin(BeetsPlugin):
|
||||
preserve: dict[str, str] = {}
|
||||
preserve_phrases: dict[str, Pattern[str]] = {}
|
||||
force_lowercase: bool = True
|
||||
fields_to_process: set[str]
|
||||
fields_to_process: set[str] = set()
|
||||
the_artist: bool = True
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
|
@ -75,16 +50,21 @@ class TitlecasePlugin(BeetsPlugin):
|
|||
"fields": [],
|
||||
"force_lowercase": False,
|
||||
"small_first_last": True,
|
||||
"the_artist": True,
|
||||
}
|
||||
)
|
||||
|
||||
"""
|
||||
auto - Automatically apply titlecase to new import metadata.
|
||||
preserve - Provide a list of words/acronyms with specific case requirements.
|
||||
fields - Fields to apply titlecase to, default is all.
|
||||
preserve - Provide a list of strings with specific case requirements.
|
||||
fields - Fields to apply titlecase to.
|
||||
force_lowercase - Lowercases the string before titlecasing.
|
||||
small_first_last - If small characters should be cased at the start of strings.
|
||||
NOTE: Titlecase will not interact with possibly case sensitive fields.
|
||||
the_artist - If the plugin infers the field to be an artist field
|
||||
(e.g. the field contains "artist")
|
||||
It will capitalize a lowercase The, helpful for the artist names
|
||||
that start with 'The', like 'The Who' or 'The Talking Heads' when
|
||||
they are not at the start of a string. Superceded by preserved phrases.
|
||||
"""
|
||||
|
||||
# Register UI subcommands
|
||||
|
|
@ -100,19 +80,20 @@ class TitlecasePlugin(BeetsPlugin):
|
|||
def __get_config_file__(self):
|
||||
self.force_lowercase = self.config["force_lowercase"].get(bool)
|
||||
self.__preserve_words__(self.config["preserve"].as_str_seq())
|
||||
self.the_artist = self.config["the_artist"].get(bool)
|
||||
self.__init_fields_to_process__(
|
||||
self.config["fields"].as_str_seq(),
|
||||
)
|
||||
|
||||
def __init_fields_to_process__(self, fields: list[str]) -> None:
|
||||
"""Creates the set for fields to process in tagging.
|
||||
Only uses fields included.
|
||||
Last, the EXCLUDED_INFO_FIELDS are removed to prevent unitentional modification.
|
||||
"""
|
||||
"""Creates the set for fields to process in tagging."""
|
||||
if fields:
|
||||
initial_field_list = set(fields)
|
||||
initial_field_list -= set(EXCLUDED_INFO_FIELDS)
|
||||
self.fields_to_process = initial_field_list
|
||||
self.fields_to_process = set(fields)
|
||||
self._log.info(
|
||||
f"set fields to process: {', '.join(self.fields_to_process)}"
|
||||
)
|
||||
else:
|
||||
self._log.info("no fields specified!")
|
||||
|
||||
def __preserve_words__(self, preserve: list[str]) -> None:
|
||||
for word in preserve:
|
||||
|
|
@ -156,27 +137,31 @@ class TitlecasePlugin(BeetsPlugin):
|
|||
cased_list: list[str] = [
|
||||
self.titlecase(i) for i in init_field
|
||||
]
|
||||
setattr(item, field, cased_list)
|
||||
self._log.info(
|
||||
(
|
||||
f"{field}: {', '.join(init_field)} -> "
|
||||
f"{', '.join(cased_list)}"
|
||||
)
|
||||
)
|
||||
setattr(item, field, cased_list)
|
||||
elif isinstance(init_field, str):
|
||||
cased: str = self.titlecase(init_field)
|
||||
self._log.info(f"{field}: {init_field} -> {cased}")
|
||||
setattr(item, field, cased)
|
||||
self._log.info(f"{field}: {init_field} -> {cased}")
|
||||
else:
|
||||
self._log.info(f"{field}: no string present")
|
||||
else:
|
||||
self._log.info(f"{field}: does not exist on {item}")
|
||||
|
||||
def titlecase(self, text: str) -> str:
|
||||
def titlecase(self, text: str, field: str = "") -> str:
|
||||
"""Titlecase the given text."""
|
||||
titlecased = titlecase(
|
||||
text.lower() if self.force_lowercase else text,
|
||||
small_first_last=self.config["small_first_last"],
|
||||
callback=self.__preserved__,
|
||||
)
|
||||
if self.the_artist and "artist" in field:
|
||||
titlecased = titlecased.replace("the", "The")
|
||||
for phrase, regexp in self.preserve_phrases.items():
|
||||
titlecased = regexp.sub(phrase, titlecased)
|
||||
return titlecased
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ Default
|
|||
preserve:
|
||||
force_lowercase: no
|
||||
small_first_last: yes
|
||||
the_artist: yes
|
||||
|
||||
.. conf:: auto
|
||||
:default: yes
|
||||
|
|
@ -69,6 +70,26 @@ Default
|
|||
A list of fields to apply the titlecase logic to. You must specify the fields
|
||||
you want to have modified in order for titlecase to apply changes to metadata.
|
||||
|
||||
A good starting point is below, which will titlecase artists, album and track titles.
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
fields:
|
||||
- album
|
||||
- albumartist
|
||||
- albumartist_credit
|
||||
- albumartist_sort
|
||||
- albumartists
|
||||
- albumartists_credit
|
||||
- albumartists_sort
|
||||
- artist
|
||||
- artist_credit
|
||||
- artist_sort
|
||||
- artists
|
||||
- artists_credit
|
||||
- artists_sort
|
||||
- title
|
||||
|
||||
.. conf:: preserve
|
||||
|
||||
List of words and phrases to preserve the case of. Without specifying ``DJ`` on
|
||||
|
|
@ -87,13 +108,21 @@ Default
|
|||
of a sentence. With this turned off ``a`` and similar words will not be capitalized
|
||||
under any circumstance.
|
||||
|
||||
Excluded Fields
|
||||
~~~~~~~~~~~~~~~
|
||||
.. conf:: the_artist
|
||||
|
||||
``titlecase`` only ever modifies string fields, and will never interact with
|
||||
fields that it considers to be case sensitive.
|
||||
If a field name contains ``artist``, then any lowercase ``the`` will be
|
||||
capitalized. Useful for bands with `The` as part of the proper name,
|
||||
like ``Amyl and The Sniffers``.
|
||||
|
||||
For reference, the string fields ``titlecase`` ignores:
|
||||
Dangerous Fields
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
``titlecase`` only ever modifies string fields, however, this doesn't prevent
|
||||
you from selecting a case sensitive field that another plugin or feature may
|
||||
rely on.
|
||||
|
||||
In particular, including any of the following in your configuration could lead
|
||||
to unintended behavior:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
|
|
|
|||
|
|
@ -16,10 +16,9 @@
|
|||
|
||||
import pytest
|
||||
|
||||
from beets import config
|
||||
from beets.library import Item
|
||||
from beets.test.helper import PluginTestCase
|
||||
from beetsplug.titlecase import EXCLUDED_INFO_FIELDS, TitlecasePlugin
|
||||
from beetsplug.titlecase import TitlecasePlugin
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
|
@ -60,7 +59,7 @@ titlecase_test_cases = [
|
|||
),
|
||||
"expected": Item(
|
||||
artist="D'Angelo and the Vanguard",
|
||||
mb_albumid="ab140e13-7b36-402a-a528-b69e3dee38a8",
|
||||
mb_albumid="Ab140e13-7b36-402a-A528-B69e3dee38a8",
|
||||
albumartist="D'Angelo",
|
||||
format="CD",
|
||||
album="the black messiah",
|
||||
|
|
@ -101,7 +100,7 @@ titlecase_test_cases = [
|
|||
{
|
||||
"config": {
|
||||
"preserve": [""],
|
||||
"fields": ["artists", "artists_ids", "discogs_artistid"],
|
||||
"fields": ["artists", "discogs_artistid"],
|
||||
"force_lowercase": False,
|
||||
"small_first_last": True,
|
||||
},
|
||||
|
|
@ -116,6 +115,68 @@ titlecase_test_cases = [
|
|||
discogs_artistid=21,
|
||||
),
|
||||
},
|
||||
{
|
||||
"config": {
|
||||
"the_artist": True,
|
||||
"preserve": ["A Day in the Park"],
|
||||
"fields": [
|
||||
"artists",
|
||||
"artist",
|
||||
"artists_sorttitle",
|
||||
"artists_ids",
|
||||
],
|
||||
},
|
||||
"item": Item(
|
||||
artists_sort=["b-52s, the"],
|
||||
artist="a day in the park",
|
||||
artists=[
|
||||
"vinylgroover & the red head",
|
||||
"a day in the park",
|
||||
"amyl and the sniffers",
|
||||
],
|
||||
artists_ids=["aBcDeF32", "aBcDeF12"],
|
||||
),
|
||||
"expected": Item(
|
||||
artists_sort=["B-52s, The"],
|
||||
artist="A Day in the Park",
|
||||
artists=[
|
||||
"Vinylgroover & The Red Head",
|
||||
"A Day in The ParkAmyl and The Sniffers",
|
||||
],
|
||||
artists_ids=["ABcDeF32", "ABcDeF12"],
|
||||
),
|
||||
},
|
||||
{
|
||||
"config": {
|
||||
"the_artist": False,
|
||||
"preserve": ["A Day in the Park"],
|
||||
"fields": [
|
||||
"artists",
|
||||
"artist",
|
||||
"artists_sorttitle",
|
||||
"artists_ids",
|
||||
],
|
||||
},
|
||||
"item": Item(
|
||||
artists_sort=["b-52s, the"],
|
||||
artist="a day in the park",
|
||||
artists=[
|
||||
"vinylgroover & the red head",
|
||||
"a day in the park",
|
||||
"amyl and the sniffers",
|
||||
],
|
||||
artists_ids=["aBcDeF32", "aBcDeF12"],
|
||||
),
|
||||
"expected": Item(
|
||||
artists_sort=["B-52s, The"],
|
||||
artist="A Day in the Park",
|
||||
artists=[
|
||||
"Vinylgroover & the Red Head",
|
||||
"A Day in the ParkAmyl and the Sniffers",
|
||||
],
|
||||
artists_ids=["ABcDeF32", "ABcDeF12"],
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -137,17 +198,10 @@ class TitlecasePluginTest(PluginTestCase):
|
|||
|
||||
def test_field_list(self):
|
||||
fields = ["album", "albumartist"]
|
||||
config["titlecase"]["fields"] = fields
|
||||
t = TitlecasePlugin()
|
||||
for field in fields:
|
||||
assert field in t.fields_to_process
|
||||
|
||||
def test_field_list_default_excluded(self):
|
||||
excluded = list(EXCLUDED_INFO_FIELDS)
|
||||
config["titlecase"]["fields"] = excluded
|
||||
t = TitlecasePlugin()
|
||||
for field in excluded:
|
||||
assert field not in t.fields_to_process
|
||||
with self.configure_plugin({"fields": fields}):
|
||||
t = TitlecasePlugin()
|
||||
for field in fields:
|
||||
assert field in t.fields_to_process
|
||||
|
||||
def test_preserved_words(self):
|
||||
"""Test using given strings to preserve case"""
|
||||
|
|
@ -159,28 +213,28 @@ class TitlecasePluginTest(PluginTestCase):
|
|||
"ABBA",
|
||||
"LaTeX",
|
||||
]
|
||||
config["titlecase"]["preserve"] = names_to_preserve
|
||||
for name in names_to_preserve:
|
||||
assert TitlecasePlugin().titlecase(name.lower()) == name
|
||||
assert TitlecasePlugin().titlecase(name.upper()) == name
|
||||
with self.configure_plugin({"preserve": names_to_preserve}):
|
||||
for name in names_to_preserve:
|
||||
assert TitlecasePlugin().titlecase(name.lower()) == name
|
||||
assert TitlecasePlugin().titlecase(name.upper()) == name
|
||||
|
||||
def test_preserved_phrases(self):
|
||||
phrases_to_preserve = ["The Beatles", "The Red Hed"]
|
||||
test_strings = ["Vinylgroover & The Red Hed", "With The Beatles"]
|
||||
config["titlecase"]["preserve"] = phrases_to_preserve
|
||||
t = TitlecasePlugin()
|
||||
for phrase in test_strings:
|
||||
assert t.titlecase(phrase.lower()) == phrase
|
||||
phrases_to_preserve = ["The Beatles", "The Red Hed"]
|
||||
with self.configure_plugin({"preserve": phrases_to_preserve}):
|
||||
t = TitlecasePlugin()
|
||||
for phrase in test_strings:
|
||||
assert t.titlecase(phrase.lower()) == phrase
|
||||
|
||||
def test_titlecase_fields(self):
|
||||
for tc in titlecase_test_cases:
|
||||
item = tc["item"]
|
||||
expected = tc["expected"]
|
||||
config["titlecase"] = tc["config"]
|
||||
TitlecasePlugin().titlecase_fields(item)
|
||||
for key, value in vars(item).items():
|
||||
if isinstance(value, str):
|
||||
assert getattr(item, key) == getattr(expected, key)
|
||||
with self.configure_plugin(tc["config"]):
|
||||
item = tc["item"]
|
||||
expected = tc["expected"]
|
||||
TitlecasePlugin().titlecase_fields(item)
|
||||
for key, value in vars(item).items():
|
||||
if isinstance(value, str):
|
||||
assert getattr(item, key) == getattr(expected, key)
|
||||
|
||||
def test_cli(self):
|
||||
for tc in titlecase_test_cases:
|
||||
|
|
|
|||
Loading…
Reference in a new issue