copied more generic implementation from snejus, and updated the tests and docs accordingly

This commit is contained in:
jochem 2026-03-21 13:02:55 +01:00 committed by Šarūnas Nejus
parent d4dc46591d
commit 55b6fbe77d
4 changed files with 71 additions and 146 deletions

View file

@ -24,7 +24,7 @@ from contextlib import contextmanager, suppress
from dataclasses import dataclass
from functools import cached_property, partial, total_ordering
from html import unescape
from itertools import groupby
from itertools import groupby, filterfalse
from pathlib import Path
from typing import TYPE_CHECKING, ClassVar, NamedTuple
from urllib.parse import quote, quote_plus, urlencode, urlparse
@ -38,6 +38,8 @@ from beets.autotag.distance import string_dist
from beets.dbcore import types
from beets.util.config import sanitize_choices
from beets.util.lyrics import INSTRUMENTAL_LYRICS, Lyrics
from beets.dbcore.query import FalseQuery
from beets.library import Item, parse_query_string
from ._utils.requests import HTTPNotFoundError, RequestHandler
@ -47,7 +49,7 @@ if TYPE_CHECKING:
import confuse
from beets.importer import ImportTask
from beets.library import Item, Library
from beets.library import Library
from beets.logging import BeetsLogger as Logger
from ._typing import (
@ -978,8 +980,7 @@ class LyricsPlugin(LyricsRequestHandler, plugins.BeetsPlugin):
self.config.add(
{
"auto": True,
"exclude_albums": [],
"exclude_songs": [],
"auto_ignore": None,
"translate": {
"api_key": None,
"from_languages": [],
@ -1065,13 +1066,14 @@ class LyricsPlugin(LyricsRequestHandler, plugins.BeetsPlugin):
cmd.func = func
return [cmd]
def imported(self, _, task: ImportTask) -> None:
"""Import hook for fetching lyrics automatically."""
for item in task.imported_items():
if self.is_excluded(item):
self.info("Skipping excluded item: {}", item)
continue
self.add_item_lyrics(item, False)
def imported(self, _, task: ImportTask) -> None:
if query_str := self.config["auto_ignore"].get():
query, _ = parse_query_string(query_str, Item)
else:
query = FalseQuery() # matches nothing, so all items proceed normally
for item in filterfalse(query.match, task.imported_items()):
self.add_item_lyrics(item, False)
def find_lyrics(self, item: Item) -> Lyrics | None:
"""Return the first lyrics match from the configured source search."""
@ -1137,17 +1139,3 @@ class LyricsPlugin(LyricsRequestHandler, plugins.BeetsPlugin):
return None
def is_excluded(self, item: Item) -> bool:
"""Return True if the item matches an exclusion rule."""
exclude_albums = {
a.lower() for a in self.config["exclude_albums"].as_str_seq()
}
exclude_songs = {
s.lower() for s in self.config["exclude_songs"].as_str_seq()
}
if item.album and item.album.lower() in exclude_albums:
return True
if item.title and item.title.lower() in exclude_songs:
return True
return False

View file

@ -26,9 +26,8 @@ New features
:bug:`2661`
- :doc:`plugins/play`: Added ``-R``/``--randomize`` flag to shuffle the playlist
order before passing it to the player.
- :doc:`plugins/lyrics`: Add ``exclude_albums`` and ``exclude_songs``
configuration options to skip fetching lyrics for specific albums or songs
during auto import.
- :doc:`plugins/lyrics`: Add ``auto_ignore`` configuration option to skip
fetching lyrics for items matching a beets query during auto import.
Bug fixes
~~~~~~~~~

View file

@ -47,8 +47,7 @@ Default configuration:
lyrics:
auto: yes
exclude_albums: []
exclude_songs: []
auto_ignore: null
translate:
api_key:
from_languages: []
@ -65,12 +64,20 @@ Default configuration:
The available options are:
- **auto**: Fetch lyrics automatically during import.
- **exclude_albums**: A list of album titles to skip when fetching lyrics during
auto import. Matching is case-insensitive. Default: ``[]`` (no albums
excluded).
- **exclude_songs**: A list of song titles to skip when fetching lyrics during
auto import. Matching is case-insensitive. Default: ``[]`` (no songs
excluded).
- **auto_ignore**: A beets query string of items to skip when fetching lyrics
during auto import. For example, to skip tracks from Bandcamp or with a
Techno genre:
.. code-block:: yaml
lyrics:
auto_ignore: |
data_source:bandcamp
,
genre:techno
Default: ``null`` (nothing is ignored). See :doc:`/reference/query` for
the query syntax.
- **translate**:
- **api_key**: Api key to access your Azure Translator resource. (see

View file

@ -368,149 +368,80 @@ class TestLyricsPlugin(LyricsPluginMixin):
item.lyrics_translation_language
@pytest.mark.parametrize(
"config_key, exclusions, item, expected",
"auto_ignore, items, expected_titles",
[
pytest.param(
None,
[],
Item(title="Come Together", album="Abbey Road"),
False,
id="default",
),
pytest.param(
"exclude_albums",
["Greatest Hits"],
Item(title="Come Together", album="Greatest Hits"),
True,
id="exact-album",
),
pytest.param(
"exclude_songs",
[Item(title="Come Together", album="Abbey Road", genre="Rock")],
["Come Together"],
Item(title="Come Together", album="Abbey Road"),
True,
id="exact-song",
id="fetch-when-no-ignore",
),
pytest.param(
"exclude_albums",
["greatest hits"],
Item(title="Come Together", album="Greatest Hits"),
True,
id="album-case-insensitive",
),
pytest.param(
"exclude_songs",
["COME TOGETHER"],
Item(title="Come Together", album="Abbey Road"),
True,
id="song-case-insensitive",
),
pytest.param(
"exclude_albums",
["Greatest"],
Item(title="Come Together", album="Greatest Hits"),
False,
id="album-no-partial-match",
),
pytest.param(
"exclude_songs",
["Come"],
Item(title="Come Together", album="Abbey Road"),
False,
id="song-no-partial-match",
),
pytest.param(
"exclude_songs",
["Come Together", "Let It Be"],
Item(title="Come Together"),
True,
id="first-of-many",
),
pytest.param(
"exclude_songs",
["Come Together", "Let It Be"],
Item(title="Let It Be"),
True,
id="second-of-many",
),
pytest.param(
"exclude_songs",
["Come Together", "Let It Be"],
Item(title="Hey Jude"),
False,
id="not-in-many",
),
],
)
def test_is_excluded(
self, lyrics_plugin, config_key, exclusions, item, expected
):
if config_key:
lyrics_plugin.config[config_key].set(exclusions)
assert lyrics_plugin.is_excluded(item) is expected
@pytest.mark.parametrize(
"config_key, exclusions, items, expected_titles",
[
pytest.param(
None,
"album:Greatest Hits",
[Item(title="Come Together", album="Greatest Hits", genre="Rock")],
[],
[Item(title="Come Together", album="Abbey Road")],
["Come Together"],
id="fetch-normal-item",
id="skip-matching-album",
),
pytest.param(
"exclude_albums",
["Greatest Hits"],
[Item(title="Come Together", album="Greatest Hits")],
"album:Greatest Hits",
[Item(title="Come Together", album="Abbey Road", genre="Rock")],
["Come Together"],
id="fetch-non-matching-album",
),
pytest.param(
"genre:rock",
[Item(title="Come Together", album="Abbey Road", genre="Rock")],
[],
id="skip-excluded-album",
id="query-case-insensitive",
),
pytest.param(
"exclude_songs",
["Come Together"],
[Item(title="Come Together", album="Abbey Road")],
[],
id="skip-excluded-song",
),
pytest.param(
"exclude_songs",
["Come Together"],
"genre:Techno",
[
Item(title="Hey Jude", album="Abbey Road"),
Item(title="Come Together", album="Abbey Road"),
Item(title="Hey Jude", album="Abbey Road", genre="Rock"),
Item(title="Techno Song", album="Club Hits", genre="Techno"),
],
["Hey Jude"],
id="mixed-task",
),
pytest.param(None, [], [], [], id="empty-task"),
pytest.param(
"album:Greatest Hits , genre:Techno",
[
Item(title="Old Song", album="Greatest Hits", genre="Rock"),
Item(title="Techno Song", album="Club Hits", genre="Techno"),
Item(title="Come Together", album="Abbey Road", genre="Rock"),
],
["Come Together"],
id="multiple-queries",
),
pytest.param(
"album:Greatest Hits",
[],
[],
id="empty-task",
),
],
)
def test_imported(
self,
lyrics_plugin,
monkeypatch,
config_key,
exclusions,
auto_ignore,
items,
expected_titles,
):
if config_key:
lyrics_plugin.config[config_key].set(exclusions)
if auto_ignore:
lyrics_plugin.config["auto_ignore"].set(auto_ignore)
calls = []
monkeypatch.setattr(
lyrics_plugin,
"add_item_lyrics",
lambda current_item, write: calls.append(
(current_item.title, write)
),
lambda current_item, write: calls.append((current_item.title, write)),
)
task = SimpleNamespace(imported_items=lambda: items)
lyrics_plugin.imported(None, task)
assert calls == [(title, False) for title in expected_titles]