Add album template value in ftintitle plugin (#6164)

## Description

I was hoping to use the functionality from `ftintitle` to set the path's
album artist as the main artist, but that wasn't possible, so I added a
template value `album_artist_no_feat`.
This commit is contained in:
Sebastian Mohr 2025-11-21 18:42:06 +01:00 committed by GitHub
commit d446e10fb0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 46 additions and 23 deletions

View file

@ -151,9 +151,9 @@ class BeetsPlugin(metaclass=abc.ABCMeta):
list
)
listeners: ClassVar[dict[EventType, list[Listener]]] = defaultdict(list)
template_funcs: TFuncMap[str] | None = None
template_fields: TFuncMap[Item] | None = None
album_template_fields: TFuncMap[Album] | None = None
template_funcs: ClassVar[TFuncMap[str]] | TFuncMap[str] = {} # type: ignore[valid-type]
template_fields: ClassVar[TFuncMap[Item]] | TFuncMap[Item] = {} # type: ignore[valid-type]
album_template_fields: ClassVar[TFuncMap[Album]] | TFuncMap[Album] = {} # type: ignore[valid-type]
name: str
config: ConfigView
@ -219,8 +219,8 @@ class BeetsPlugin(metaclass=abc.ABCMeta):
self.name = name or self.__module__.split(".")[-1]
self.config = beets.config[self.name]
# Set class attributes if they are not already set
# for the type of plugin.
# If the class attributes are not set, initialize as instance attributes.
# TODO: Revise with v3.0.0, see also type: ignore[valid-type] above
if not self.template_funcs:
self.template_funcs = {}
if not self.template_fields:
@ -368,8 +368,6 @@ class BeetsPlugin(metaclass=abc.ABCMeta):
"""
def helper(func: TFunc[str]) -> TFunc[str]:
if cls.template_funcs is None:
cls.template_funcs = {}
cls.template_funcs[name] = func
return func
@ -384,8 +382,6 @@ class BeetsPlugin(metaclass=abc.ABCMeta):
"""
def helper(func: TFunc[Item]) -> TFunc[Item]:
if cls.template_fields is None:
cls.template_fields = {}
cls.template_fields[name] = func
return func
@ -565,8 +561,7 @@ def template_funcs() -> TFuncMap[str]:
"""
funcs: TFuncMap[str] = {}
for plugin in find_plugins():
if plugin.template_funcs:
funcs.update(plugin.template_funcs)
funcs.update(plugin.template_funcs)
return funcs
@ -592,21 +587,20 @@ F = TypeVar("F")
def _check_conflicts_and_merge(
plugin: BeetsPlugin, plugin_funcs: dict[str, F] | None, funcs: dict[str, F]
plugin: BeetsPlugin, plugin_funcs: dict[str, F], funcs: dict[str, F]
) -> None:
"""Check the provided template functions for conflicts and merge into funcs.
Raises a `PluginConflictError` if a plugin defines template functions
for fields that another plugin has already defined template functions for.
"""
if plugin_funcs:
if not plugin_funcs.keys().isdisjoint(funcs.keys()):
conflicted_fields = ", ".join(plugin_funcs.keys() & funcs.keys())
raise PluginConflictError(
f"Plugin {plugin.name} defines template functions for "
f"{conflicted_fields} that conflict with another plugin."
)
funcs.update(plugin_funcs)
if not plugin_funcs.keys().isdisjoint(funcs.keys()):
conflicted_fields = ", ".join(plugin_funcs.keys() & funcs.keys())
raise PluginConflictError(
f"Plugin {plugin.name} defines template functions for "
f"{conflicted_fields} that conflict with another plugin."
)
funcs.update(plugin_funcs)
def item_field_getters() -> TFuncMap[Item]:

View file

@ -19,11 +19,11 @@ from __future__ import annotations
import re
from typing import TYPE_CHECKING
from beets import plugins, ui
from beets import config, plugins, ui
if TYPE_CHECKING:
from beets.importer import ImportSession, ImportTask
from beets.library import Item
from beets.library import Album, Item
def split_on_feat(
@ -98,6 +98,11 @@ def find_feat_part(
return feat_part
def _album_artist_no_feat(album: Album) -> str:
custom_words = config["ftintitle"]["custom_words"].as_str_seq()
return split_on_feat(album["albumartist"], False, list(custom_words))[0]
class FtInTitlePlugin(plugins.BeetsPlugin):
def __init__(self) -> None:
super().__init__()
@ -129,6 +134,10 @@ class FtInTitlePlugin(plugins.BeetsPlugin):
if self.config["auto"]:
self.import_stages = [self.imported]
self.album_template_fields["album_artist_no_feat"] = (
_album_artist_no_feat
)
def commands(self) -> list[ui.Subcommand]:
def func(lib, opts, args):
self.config.set_args(opts)

View file

@ -13,6 +13,7 @@ been dropped.
New features:
- :doc:`plugins/ftintitle`: Added argument for custom feat. words in ftintitle.
- :doc:`plugins/ftintitle`: Added album template value ``album_artist_no_feat``.
- :doc:`plugins/musicbrainz`: Allow selecting tags or genres to populate the
genres tag.
- :doc:`plugins/ftintitle`: Added argument to skip the processing of artist and

View file

@ -33,6 +33,14 @@ file. The available options are:
- **custom_words**: List of additional words that will be treated as a marker
for artist features. Default: ``[]``.
Path Template Values
--------------------
This plugin provides the ``album_artist_no_feat`` :ref:`template value
<templ_plugins>` that you can use in your :ref:`path-format-config` in
``paths.default``. Any ``custom_words`` in the configuration are taken into
account.
Running Manually
----------------

View file

@ -281,6 +281,8 @@ constructs include:
- ``$missing`` by :doc:`/plugins/missing`: The number of missing tracks per
album.
- ``$album_artist_no_feat`` by :doc:`/plugins/ftintitle`: The album artist
without any featured artists
- ``%bucket{text}`` by :doc:`/plugins/bucket`: Substitute a string by the range
it belongs to.
- ``%the{text}`` by :doc:`/plugins/the`: Moves English articles to ends of

View file

@ -18,7 +18,7 @@ from collections.abc import Generator
import pytest
from beets.library.models import Item
from beets.library.models import Album, Item
from beets.test.helper import PluginTestCase
from beetsplug import ftintitle
@ -364,3 +364,12 @@ def test_custom_words(
if custom_words is None:
custom_words = []
assert ftintitle.contains_feat(given, custom_words) is expected
def test_album_template_value():
album = Album()
album["albumartist"] = "Foo ft. Bar"
assert ftintitle._album_artist_no_feat(album) == "Foo"
album["albumartist"] = "Foobar"
assert ftintitle._album_artist_no_feat(album) == "Foobar"