diff --git a/beetsplug/lyrics.py b/beetsplug/lyrics.py index e173bb319..069daa15b 100644 --- a/beetsplug/lyrics.py +++ b/beetsplug/lyrics.py @@ -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 diff --git a/docs/changelog.rst b/docs/changelog.rst index 5c76c7ae1..85b7523a5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -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 ~~~~~~~~~ diff --git a/docs/plugins/lyrics.rst b/docs/plugins/lyrics.rst index 2a7b7a679..1a40da59d 100644 --- a/docs/plugins/lyrics.rst +++ b/docs/plugins/lyrics.rst @@ -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 diff --git a/test/plugins/test_lyrics.py b/test/plugins/test_lyrics.py index 3f55674d9..8e24e752f 100644 --- a/test/plugins/test_lyrics.py +++ b/test/plugins/test_lyrics.py @@ -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]