From 0670611d7a8a7870f5eea91e1c1eacb586489eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0ar=C5=ABnas=20Nejus?= Date: Sat, 7 Mar 2026 00:10:10 +0000 Subject: [PATCH] Document new methods --- beets/metadata_plugins.py | 41 ++++++++++++++++++++++++++++++----- beetsplug/deezer.py | 2 ++ beetsplug/discogs/__init__.py | 8 ++++++- beetsplug/musicbrainz.py | 8 +++---- beetsplug/spotify.py | 9 +++----- 5 files changed, 50 insertions(+), 18 deletions(-) diff --git a/beets/metadata_plugins.py b/beets/metadata_plugins.py index ec1239f5c..6805ebd49 100644 --- a/beets/metadata_plugins.py +++ b/beets/metadata_plugins.py @@ -291,6 +291,12 @@ class IDResponse(TypedDict): class SearchParams(NamedTuple): + """Bundle normalized search context passed to provider search hooks. + + Shared search orchestration constructs this value so plugin hooks receive + one object describing search intent, query text, and provider filters. + """ + query_type: QueryType query: str filters: dict[str, str] @@ -328,6 +334,20 @@ class SearchApiMetadataSourcePlugin( name: str, va_likely: bool, ) -> tuple[str, dict[str, str]]: + """Build query text and API filters for a provider search. + + Subclasses can override this hook when their API requires a query format + or filter set that differs from the default text-based construction. + + :param query_type: The type of query to perform. Either *album* or *track* + :param items: List of items the search is being performed for + :param artist: Artist name + :param name: Album or track name, depending on ``query_type`` + :param va_likely: Whether the search is likely to be for various artists + :return: Tuple of (``query`` text, ``filters`` dict) to use for the + search API call + """ + query = f'album:"{name}"' if query_type == "album" else name if query_type == "track" or not va_likely: query += f' artist:"{artist}"' @@ -336,18 +356,25 @@ class SearchApiMetadataSourcePlugin( @abc.abstractmethod def get_search_response(self, params: SearchParams) -> Sequence[R]: + """Fetch raw search results for a provider request. + + Implementations should return records containing source IDs so shared + candidate resolution can perform ID-based album and track lookups. + + :param params: :py:namedtuple:`~SearchParams` named tuple + :return: Sequence of IDResponse dicts containing at least an "id" key for each + """ + raise NotImplementedError def _search_api( self, query_type: QueryType, query: str, filters: dict[str, str] ) -> Sequence[R]: - """Perform a search on the API. + """Run shared provider search orchestration and return ID-bearing results. - :param query_type: The type of query to perform. - :param filters: A dictionary of filters to apply to the search. - :param query_string: Additional query to include in the search. - - Should return a list of identifiers for the requested type (album or track). + This path applies optional query normalization and default limits, then + delegates API access to provider hooks with consistent logging and + failure handling. """ if self.config["search_query_ascii"].get(): query = unidecode.unidecode(query) @@ -372,6 +399,8 @@ class SearchApiMetadataSourcePlugin( def _get_candidates( self, query_type: QueryType, *args, **kwargs ) -> Sequence[R]: + """Resolve query hooks and execute one provider search request.""" + return self._search_api( query_type, *self.get_search_query_with_filters(query_type, *args, **kwargs), diff --git a/beetsplug/deezer.py b/beetsplug/deezer.py index 7f00cda87..d459006e6 100644 --- a/beetsplug/deezer.py +++ b/beetsplug/deezer.py @@ -218,6 +218,8 @@ class DeezerPlugin(SearchApiMetadataSourcePlugin[IDResponse]): ) def get_search_response(self, params: SearchParams) -> list[IDResponse]: + """Search Deezer and return the raw result payload entries.""" + response = requests.get( f"{self.search_url}{params.query_type}", params={ diff --git a/beetsplug/discogs/__init__.py b/beetsplug/discogs/__init__.py index c524c0c68..f09ed74be 100644 --- a/beetsplug/discogs/__init__.py +++ b/beetsplug/discogs/__init__.py @@ -240,6 +240,12 @@ class DiscogsPlugin(SearchApiMetadataSourcePlugin[IDResponse]): name: str, va_likely: bool, ) -> tuple[str, dict[str, str]]: + """Build a Discogs release query and fixed release-type filter. + + The query is normalized to improve hit rates for punctuation-heavy album + names and medium suffixes that can reduce recall. + """ + query = f"{artist} {name}" if va_likely else name # Strip non-word characters from query. Things like "!" and "-" can # cause a query to return no results, even if they match the artist or @@ -253,7 +259,7 @@ class DiscogsPlugin(SearchApiMetadataSourcePlugin[IDResponse]): return query, {"type": "release"} def get_search_response(self, params: SearchParams) -> Sequence[IDResponse]: - """Returns a list of AlbumInfo objects for a discogs search query.""" + """Search Discogs releases and return raw result mappings with IDs.""" results = self.discogs_client.search(params.query, **params.filters) results.per_page = params.limit return [r.data for r in results.page(1)] diff --git a/beetsplug/musicbrainz.py b/beetsplug/musicbrainz.py index 00cc54a3c..416f42c38 100644 --- a/beetsplug/musicbrainz.py +++ b/beetsplug/musicbrainz.py @@ -728,6 +728,8 @@ class MusicBrainzPlugin( name: str, va_likely: bool, ) -> tuple[str, dict[str, str]]: + """Build MusicBrainz criteria filters for album and recording search.""" + if query_type == "album": criteria = self.get_album_criteria(items, artist, name, va_likely) else: @@ -738,12 +740,8 @@ class MusicBrainzPlugin( } def get_search_response(self, params: SearchParams) -> Sequence[IDResponse]: - """Perform MusicBrainz API search and return results. + """Search MusicBrainz and return release or recording result mappings.""" - Execute a search against the MusicBrainz API for recordings or releases - using the provided criteria. Handles API errors by converting them into - MusicBrainzAPIError exceptions with contextual information. - """ mb_entity: Literal["release", "recording"] = ( "release" if params.query_type == "album" else "recording" ) diff --git a/beetsplug/spotify.py b/beetsplug/spotify.py index 53eae5785..9cf855977 100644 --- a/beetsplug/spotify.py +++ b/beetsplug/spotify.py @@ -470,13 +470,10 @@ class SpotifyPlugin( def get_search_response( self, params: SearchParams ) -> Sequence[SearchResponseAlbums | SearchResponseTracks]: - """Query the Spotify Search API for the specified ``query_string``, - applying the provided ``filters``. + """Search Spotify and return raw album or track result items. - :param query_type: Item type to search across. Valid types are: 'album', - 'artist', 'playlist', and 'track'. - :param filters: Field filters to apply. - :param query_string: Additional query to include in the search. + Unauthorized responses trigger one token refresh attempt before the + method gives up and falls back to an empty result set. """ for _ in range(2): response = requests.get(