From a6bda748ce6e8233080dbb5ce96903bda667d49b Mon Sep 17 00:00:00 2001 From: Henry Date: Fri, 14 Nov 2025 19:08:14 -0800 Subject: [PATCH] Added support for pre-tag selection stage --- beetsplug/titlecase.py | 40 +++++++++++++++++++++---------- docs/plugins/titlecase.rst | 14 ++++++++--- test/plugins/test_titlecase.py | 44 +++++++++++++++++++++++++++++++++- 3 files changed, 82 insertions(+), 16 deletions(-) diff --git a/beetsplug/titlecase.py b/beetsplug/titlecase.py index 94481ea9a..51cc8a12a 100644 --- a/beetsplug/titlecase.py +++ b/beetsplug/titlecase.py @@ -22,6 +22,7 @@ from typing import Optional, Pattern from titlecase import titlecase from beets import ui +from beets.autotag.hooks import AlbumInfo, Info, TrackInfo from beets.importer import ImportSession, ImportTask from beets.library import Item from beets.plugins import BeetsPlugin @@ -52,6 +53,7 @@ class TitlecasePlugin(BeetsPlugin): "force_lowercase": False, "small_first_last": True, "the_artist": True, + "after_choice": False, } ) @@ -77,7 +79,15 @@ class TitlecasePlugin(BeetsPlugin): self.__get_config_file__() if self.config["auto"]: - self.import_stages = [self.imported] + if self.config["after_choice"].get(bool): + self.import_stages = [self.imported] + else: + self.register_listener( + "trackinfo_received", self.received_info_handler + ) + self.register_listener( + "albuminfo_received", self.received_info_handler + ) def __get_config_file__(self): self.force_lowercase = self.config["force_lowercase"].get(bool) @@ -92,11 +102,11 @@ class TitlecasePlugin(BeetsPlugin): """Creates the set for fields to process in tagging.""" if fields: self.fields_to_process = set(fields) - self._log.info( + self._log.debug( f"set fields to process: {', '.join(self.fields_to_process)}" ) else: - self._log.info("no fields specified!") + self._log.debug("no fields specified!") def __preserve_words__(self, preserve: list[str]) -> None: for word in preserve: @@ -113,11 +123,17 @@ class TitlecasePlugin(BeetsPlugin): return preserved_word return None + def received_info_handler(self, info: AlbumInfo | TrackInfo): + self.titlecase_fields(info) + if isinstance(info, AlbumInfo): + for track in info.tracks: + self.titlecase_fields(track) + def commands(self) -> list[ui.Subcommand]: def func(lib, opts, args): write = ui.should_write() for item in lib.items(args): - self._log.info(f"titlecasing {item.title}:") + self._log.debug(f"titlecasing {item.title}:") self.titlecase_fields(item) item.store() if write: @@ -126,7 +142,7 @@ class TitlecasePlugin(BeetsPlugin): self._command.func = func return [self._command] - def titlecase_fields(self, item: Item): + def titlecase_fields(self, item: Item | Info): """Applies titlecase to fields, except those excluded by the default exclusions and the set exclude lists. @@ -141,20 +157,20 @@ class TitlecasePlugin(BeetsPlugin): self.titlecase(i) for i in init_field ] setattr(item, field, cased_list) - self._log.info( + self._log.debug( ( f"{field}: {', '.join(init_field)} -> " f"{', '.join(cased_list)}" ) ) elif isinstance(init_field, str): - cased: str = self.titlecase(init_field) + cased: str = self.titlecase(init_field, field) setattr(item, field, cased) - self._log.info(f"{field}: {init_field} -> {cased}") + self._log.debug(f"{field}: {init_field} -> {cased}") else: - self._log.info(f"{field}: no string present") + self._log.debug(f"{field}: no string present") else: - self._log.info(f"{field}: does not exist on {item}") + self._log.debug(f"{field}: does not exist on {type(item)}") def titlecase(self, text: str, field: str = "") -> str: """Titlecase the given text.""" @@ -180,8 +196,8 @@ class TitlecasePlugin(BeetsPlugin): """Import hook for titlecasing on import.""" for item in task.imported_items(): try: - self._log.info(f"titlecasing {item.title}:") + self._log.debug(f"titlecasing {item.title}:") self.titlecase_fields(item) item.store() except Exception as e: - self._log.info(f"titlecasing exception {e}") + self._log.debug(f"titlecasing exception {e}") diff --git a/docs/plugins/titlecase.rst b/docs/plugins/titlecase.rst index e812f77ed..ab624ef87 100644 --- a/docs/plugins/titlecase.rst +++ b/docs/plugins/titlecase.rst @@ -60,6 +60,7 @@ Default force_lowercase: no small_first_last: yes the_artist: yes + after_choice: no .. conf:: auto :default: yes @@ -99,9 +100,9 @@ Default .. conf:: replace - The replace function takes place before any titlecasing occurs, and is intended to - help normalize differences in puncuation styles. It accepts a list of tuples, with - the first being the target, and the second being the replacement + The replace function takes place before any titlecasing occurs, and is intended to + help normalize differences in puncuation styles. It accepts a list of tuples, with + the first being the target, and the second being the replacement .. conf:: force_lowercase :default: no @@ -121,6 +122,13 @@ Default capitalized. Useful for bands with `The` as part of the proper name, like ``Amyl and The Sniffers``. +.. conf:: after_choice + + By default, titlecase runs on the candidates that are received, adjusting them before + you make your selection and creating different weight calculations. If you'd rather + see the data as recieved from the database, set this to true to run after you make + your tag choice. + Dangerous Fields ~~~~~~~~~~~~~~~~ diff --git a/test/plugins/test_titlecase.py b/test/plugins/test_titlecase.py index c2576ebcb..d641c5b5d 100644 --- a/test/plugins/test_titlecase.py +++ b/test/plugins/test_titlecase.py @@ -16,6 +16,7 @@ import pytest +from beets.autotag.hooks import AlbumInfo, TrackInfo from beets.library import Item from beets.test.helper import PluginTestCase from beetsplug.titlecase import TitlecasePlugin @@ -59,7 +60,7 @@ titlecase_test_cases = [ title="Till It's Done (Tutu)", ), "expected": Item( - artist="D'Angelo and the Vanguard", + artist="D'Angelo and The Vanguard", mb_albumid="Ab140e13-7b36-402a-A528-B69e3dee38a8", albumartist="D'Angelo", format="CD", @@ -237,6 +238,47 @@ class TitlecasePluginTest(PluginTestCase): if isinstance(value, str): assert getattr(item, key) == getattr(expected, key) + def test_recieved_info_handler(self): + test_track_info = TrackInfo( + album="test album", + artist_credit="test artist credit", + artists=["artist one", "artist two"], + ) + expected_track_info = TrackInfo( + album="Test Album", + artist_credit="Test Artist Credit", + artists=["Artist One", "Artist Two"], + ) + test_album_info = AlbumInfo( + tracks=[test_track_info], + album="test album", + artist_credit="test artist credit", + artists=["artist one", "artist two"], + ) + expected_album_info = AlbumInfo( + tracks=[expected_track_info], + album="Test Album", + artist_credit="Test Artist Credit", + artists=["Artist One", "Artist Two"], + ) + with self.configure_plugin( + {"fields": ["album", "artist_credit", "artists"]} + ): + TitlecasePlugin().received_info_handler(test_track_info) + assert test_track_info.album == expected_track_info.album + assert ( + test_track_info.artist_credit + == expected_track_info.artist_credit + ) + assert test_track_info.artists == expected_track_info.artists + TitlecasePlugin().received_info_handler(test_album_info) + assert test_album_info.album == expected_album_info.album + assert ( + test_album_info.artist_credit + == expected_album_info.artist_credit + ) + assert test_album_info.artists == expected_album_info.artists + def test_cli(self): for tc in titlecase_test_cases: with self.configure_plugin(tc["config"]):