Added support for pre-tag selection stage

This commit is contained in:
Henry 2025-11-14 19:08:14 -08:00
parent c89d0c1637
commit a6bda748ce
3 changed files with 82 additions and 16 deletions

View file

@ -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}")

View file

@ -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
~~~~~~~~~~~~~~~~

View file

@ -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"]):