mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 08:39:17 +01:00
Merge branch 'master' into titlecase
This commit is contained in:
commit
327d237d9e
9 changed files with 83 additions and 26 deletions
|
|
@ -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,7 +561,6 @@ def template_funcs() -> TFuncMap[str]:
|
|||
"""
|
||||
funcs: TFuncMap[str] = {}
|
||||
for plugin in find_plugins():
|
||||
if plugin.template_funcs:
|
||||
funcs.update(plugin.template_funcs)
|
||||
return funcs
|
||||
|
||||
|
|
@ -592,14 +587,13 @@ 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(
|
||||
|
|
|
|||
|
|
@ -27,7 +27,16 @@ import gi
|
|||
|
||||
from beets import ui
|
||||
|
||||
gi.require_version("Gst", "1.0")
|
||||
try:
|
||||
gi.require_version("Gst", "1.0")
|
||||
except ValueError as e:
|
||||
# on some scenarios, gi may be importable, but we get a ValueError when
|
||||
# trying to specify the required version. This is problematic in the test
|
||||
# suite where test_bpd.py has a call to
|
||||
# pytest.importorskip("beetsplug.bpd"). Re-raising as an ImportError
|
||||
# makes it so the test collector functions as inteded.
|
||||
raise ImportError from e
|
||||
|
||||
from gi.repository import GLib, Gst # noqa: E402
|
||||
|
||||
Gst.init(None)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -17,9 +17,10 @@
|
|||
import base64
|
||||
import json
|
||||
import os
|
||||
import typing as t
|
||||
|
||||
import flask
|
||||
from flask import g, jsonify
|
||||
from flask import jsonify
|
||||
from unidecode import unidecode
|
||||
from werkzeug.routing import BaseConverter, PathConverter
|
||||
|
||||
|
|
@ -28,6 +29,17 @@ from beets import ui, util
|
|||
from beets.dbcore.query import PathQuery
|
||||
from beets.plugins import BeetsPlugin
|
||||
|
||||
# Type checking hacks
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
|
||||
class LibraryCtx(flask.ctx._AppCtxGlobals):
|
||||
lib: beets.library.Library
|
||||
|
||||
g = LibraryCtx()
|
||||
else:
|
||||
from flask import g
|
||||
|
||||
# Utilities.
|
||||
|
||||
|
||||
|
|
@ -232,7 +244,7 @@ def _get_unique_table_field_values(model, field, sort_field):
|
|||
raise KeyError
|
||||
with g.lib.transaction() as tx:
|
||||
rows = tx.query(
|
||||
f"SELECT DISTINCT '{field}' FROM '{model._table}' ORDER BY '{sort_field}'"
|
||||
f"SELECT DISTINCT {field} FROM {model._table} ORDER BY {sort_field}"
|
||||
)
|
||||
return [row[0] for row in rows]
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -45,6 +46,10 @@ Bug fixes:
|
|||
accepted a list of strings). :bug:`5962`
|
||||
- Fix a bug introduced in release 2.4.0 where import from any valid
|
||||
import-log-file always threw a "none of the paths are importable" error.
|
||||
- :doc:`/plugins/web`: repair broken `/item/values/…` and `/albums/values/…`
|
||||
endpoints. Previously, due to single-quotes (ie. string literal) in the SQL
|
||||
query, the query eg. `GET /item/values/albumartist` would return the literal
|
||||
"albumartist" instead of a list of unique album artists.
|
||||
|
||||
For plugin developers:
|
||||
|
||||
|
|
@ -64,6 +69,8 @@ Other changes:
|
|||
- Refactored the ``beets/ui/commands.py`` monolithic file (2000+ lines) into
|
||||
multiple modules within the ``beets/ui/commands`` directory for better
|
||||
maintainability.
|
||||
- :doc:`plugins/bpd`: Raise ImportError instead of ValueError when GStreamer is
|
||||
unavailable, enabling ``importorskip`` usage in pytest setup.
|
||||
|
||||
2.5.1 (October 14, 2025)
|
||||
------------------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
----------------
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -118,6 +118,13 @@ class WebPluginTest(ItemInDBTestCase):
|
|||
assert response.status_code == 200
|
||||
assert len(res_json["items"]) == 3
|
||||
|
||||
def test_get_unique_item_artist(self):
|
||||
response = self.client.get("/item/values/artist")
|
||||
res_json = json.loads(response.data.decode("utf-8"))
|
||||
|
||||
assert response.status_code == 200
|
||||
assert res_json["values"] == ["", "AAA Singers"]
|
||||
|
||||
def test_get_single_item_by_id(self):
|
||||
response = self.client.get("/item/1")
|
||||
res_json = json.loads(response.data.decode("utf-8"))
|
||||
|
|
|
|||
Loading…
Reference in a new issue