From 944299078cc5cba969dea45935d55ef6d1f8bcce Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 17 Oct 2025 19:32:49 -0700 Subject: [PATCH] working on field titlecasing --- beetsplug/titlecase.py | 93 ++++++++++++++++++++++++++++++---- test/plugins/test_titlecase.py | 2 + 2 files changed, 84 insertions(+), 11 deletions(-) diff --git a/beetsplug/titlecase.py b/beetsplug/titlecase.py index 43cb2d618..b57754aea 100644 --- a/beetsplug/titlecase.py +++ b/beetsplug/titlecase.py @@ -16,6 +16,7 @@ Title case logic is derived from the python-titlecase library.""" from beets.plugins import BeetsPlugin +from beets.dbcore import types from beets import ui from titlecase import titlecase from typing import TYPE_CHECKING @@ -27,8 +28,31 @@ if TYPE_CHECKING: __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([ + 'id', + 'mb_workid', + 'mb_trackid', + 'mb_albumid', + 'mb_artistid', + 'mb_albumartistid', + 'mb_albumartistids', + 'mb_releasetrackid', + 'acoustid_fingerprint', + 'acoustid_id', + 'mb_releasegroupid', + 'asin', + 'isrc', + 'bitrate_mode', + 'encoder_info', + 'encoder_settings' + ]) + class TitlecasePlugin(BeetsPlugin): preserve: dict[str, str] = {} + fields_to_process: set[str] = [] def __init__(self) -> None: super().__init__() @@ -45,10 +69,10 @@ class TitlecasePlugin(BeetsPlugin): preserve - provide a list of words/acronyms with specific case requirements small_first_last - if small characters should be title cased at beginning titlecase_metadata - if metadata fields should have title case applied + include_fields - fields to apply titlecase to, default is all, except select excluded fields exclude_fields - fields to exclude from titlecase to, default is none - include_fields - fields to apply titlecase to, default is all - titlecase will not interact with possibly case sensitive fields like id, - path or album_id + NOTE: titlecase will not interact with possibly case sensitive fields like id, + path or album_id. Paths are best modified in path config. """ # Register template function self.template_funcs["titlecase"] = self.titlecase @@ -61,11 +85,38 @@ class TitlecasePlugin(BeetsPlugin): self._command.parser.add_option( "-p", "--preserve", - help="preserve the case of the given word" + help="preserve the case of the given word, in addition to those configured." + ) + + self._command.parser.add_option( + "-f", + "--field", + help="apply to the following fields." ) for word in self.config["preserve"].as_str_seq(): self.preserve[word.upper()] = word + self.__init_field_list__() + + def __init_field_list__(self) -> None: + """ Creates the set for fields to process in tagging. + If we have include_fields from config, the shared fields will be used. + Then, any fields specified to be excluded will be removed. + This will result in exclude_fields overriding include_fields. + Last, the EXCLUDED_INFO_FIELDS are removed to prevent unitentional modification. + """ + initial_field_list = set([ + k for k, v in Item()._fields.items() if + isinstance(v, types.STRING) or + isinstance(v, types.SEMICOLONS_SPACE_DSV) or + isinstance(v, types.MULTI_VALUE_DSV) + ) + if (incl := self.config["include_fields"].as_str_seq()): + initial_field_list = initial_field_list.intersection(set(incl)) + if (excl := self.config["exclude_fields"].as_str_seq()): + initial_field_list -= set(excl) + initial_field_list -= set(EXCLUDED_INFO_FIELDS) + self.fields_to_process = basic_fields_list def __preserved__(self, word, **kwargs) -> str | None: """ Callback function for words to preserve case of.""" @@ -73,15 +124,35 @@ class TitlecasePlugin(BeetsPlugin): return preserved_word return None - def titlecase_fields(self, text: str) -> str: - return titlecase(text) + def commands(self) -> list[ui.Subcommand]: + def func(lib, opts, args): + self.config.set_args(opts) + + def titlecase_fields(self, item: Item): + """ Applies titlecase to fields, except + those excluded by the default exclusions and the + set exclude lists. + """ + for field in self.fields_to_process: + init_str = getattr(item, field, "") + if init_str: + cased = self.titlecase(old_str) + self._log.info(f"{field}: {init_str} -> {cased}") + setattr(item, field, cased) + else: + self._log.info(f"{field}: no string present") def titlecase(self, text: str) -> str: - """ Titlecase the given text """ + """ Titlecase the given text. """ return titlecase(text, - small_first_last=self.config["small_first_last"], - callback=self.__preserved__) + small_first_last=self.config["small_first_last"], + callback=self.__preserved__) def imported(self, session: ImportSession, task: ImportTask) -> None: - """ Import hook for titlecasing on import """ - return + """ Import hook for titlecasing on import. """ + for item in task.imported_items(): + self._log.info(f"titlecasing {item.title}:") + self.titlecase_fields(item) + item.store() + + diff --git a/test/plugins/test_titlecase.py b/test/plugins/test_titlecase.py index b17bc7bed..aa830bf99 100644 --- a/test/plugins/test_titlecase.py +++ b/test/plugins/test_titlecase.py @@ -60,3 +60,5 @@ class TitlecasePluginTest(BeetsTestCase): def test_imported(self): assert 1 == 3 + def test_init_field_list(self): +