1111import inspect
1212import re
1313import warnings
14- from typing import TYPE_CHECKING , Generic , Literal , Sequence , TypedDict , TypeVar
14+ from typing import (
15+ TYPE_CHECKING ,
16+ Generic ,
17+ Literal ,
18+ NamedTuple ,
19+ TypedDict ,
20+ TypeVar ,
21+ )
1522
1623import unidecode
17- from typing_extensions import NotRequired
1824
1925from beets .util import cached_classproperty
2026from beets .util .id_extractors import extract_release_id
2127
2228from .plugins import BeetsPlugin , find_plugins , notify_info_yielded , send
2329
2430if TYPE_CHECKING :
25- from collections .abc import Iterable
31+ from collections .abc import Iterable , Sequence
2632
2733 from confuse import ConfigView
2834
2935 from .autotag import Distance
3036 from .autotag .hooks import AlbumInfo , Item , TrackInfo
3137
38+ QueryType = Literal ["album" , "track" ]
39+
3240
3341def find_metadata_source_plugins () -> list [MetadataSourcePlugin ]:
3442 """Returns a list of MetadataSourcePlugin subclass instances
@@ -203,7 +211,7 @@ def item_candidates(
203211 """
204212 raise NotImplementedError
205213
206- def albums_for_ids (self , ids : Sequence [str ]) -> Iterable [AlbumInfo | None ]:
214+ def albums_for_ids (self , ids : Iterable [str ]) -> Iterable [AlbumInfo | None ]:
207215 """Batch lookup of album metadata for a list of album IDs.
208216
209217 Given a list of album identifiers, yields corresponding AlbumInfo objects.
@@ -214,7 +222,7 @@ def albums_for_ids(self, ids: Sequence[str]) -> Iterable[AlbumInfo | None]:
214222
215223 return (self .album_for_id (id ) for id in ids )
216224
217- def tracks_for_ids (self , ids : Sequence [str ]) -> Iterable [TrackInfo | None ]:
225+ def tracks_for_ids (self , ids : Iterable [str ]) -> Iterable [TrackInfo | None ]:
218226 """Batch lookup of track metadata for a list of track IDs.
219227
220228 Given a list of track identifiers, yields corresponding TrackInfo objects.
@@ -320,12 +328,13 @@ class IDResponse(TypedDict):
320328 id : str
321329
322330
323- class SearchFilter (TypedDict ):
324- artist : NotRequired [str ]
325- album : NotRequired [str ]
331+ R = TypeVar ("R" , bound = IDResponse )
326332
327333
328- R = TypeVar ("R" , bound = IDResponse )
334+ class SearchParams (NamedTuple ):
335+ query_type : QueryType
336+ query : str
337+ filters : dict [str , str ]
329338
330339
331340class SearchApiMetadataSourcePlugin (
@@ -348,12 +357,26 @@ def __init__(self, *args, **kwargs) -> None:
348357 }
349358 )
350359
360+ def get_search_filters (
361+ self ,
362+ query_type : QueryType ,
363+ items : Sequence [Item ],
364+ artist : str ,
365+ name : str ,
366+ va_likely : bool ,
367+ ) -> tuple [str , dict [str , str ]]:
368+ query = f'album:"{ name } "' if query_type == "album" else name
369+ if query_type == "track" or not va_likely :
370+ query += f' artist:"{ artist } "'
371+
372+ return query , {}
373+
351374 @abc .abstractmethod
375+ def get_search_response (self , params : SearchParams ) -> Sequence [R ]:
376+ raise NotImplementedError
377+
352378 def _search_api (
353- self ,
354- query_type : Literal ["album" , "track" ],
355- filters : SearchFilter ,
356- query_string : str = "" ,
379+ self , query_type : QueryType , query : str , filters : dict [str , str ]
357380 ) -> Sequence [R ]:
358381 """Perform a search on the API.
359382
@@ -363,7 +386,28 @@ def _search_api(
363386
364387 Should return a list of identifiers for the requested type (album or track).
365388 """
366- raise NotImplementedError
389+ if self .config ["search_query_ascii" ].get ():
390+ query = unidecode .unidecode (query )
391+
392+ filters ["limit" ] = str (self .config ["search_limit" ].get ())
393+ params = SearchParams (query_type , query , filters )
394+
395+ self ._log .debug ("Searching for '{}' with {}" , query , filters )
396+ try :
397+ response_data = self .get_search_response (params )
398+ except Exception :
399+ self ._log .error ("Error fetching data" , exc_info = True )
400+ return ()
401+
402+ self ._log .debug ("Found {} result(s)" , len (response_data ))
403+ return response_data
404+
405+ def _get_candidates (
406+ self , query_type : QueryType , * args , ** kwargs
407+ ) -> Sequence [R ]:
408+ return self ._search_api (
409+ query_type , * self .get_search_filters (query_type , * args , ** kwargs )
410+ )
367411
368412 def candidates (
369413 self ,
@@ -372,55 +416,14 @@ def candidates(
372416 album : str ,
373417 va_likely : bool ,
374418 ) -> Iterable [AlbumInfo ]:
375- query_filters : SearchFilter = {"album" : album }
376- if not va_likely :
377- query_filters ["artist" ] = artist
378-
379- results = self ._search_api ("album" , query_filters )
380- if not results :
381- return []
382-
383- return filter (
384- None , self .albums_for_ids ([result ["id" ] for result in results ])
385- )
419+ results = self ._get_candidates ("album" , items , artist , album , va_likely )
420+ return filter (None , self .albums_for_ids (r ["id" ] for r in results ))
386421
387422 def item_candidates (
388423 self , item : Item , artist : str , title : str
389424 ) -> Iterable [TrackInfo ]:
390- results = self ._search_api (
391- "track" , {"artist" : artist }, query_string = title
392- )
393- if not results :
394- return []
395-
396- return filter (
397- None ,
398- self .tracks_for_ids ([result ["id" ] for result in results if result ]),
399- )
400-
401- def _construct_search_query (
402- self , filters : SearchFilter , query_string : str
403- ) -> str :
404- """Construct a query string with the specified filters and keywords to
405- be provided to the spotify (or similar) search API.
406-
407- The returned format was initially designed for spotify's search API but
408- we found is also useful with other APIs that support similar query structures.
409- see `spotify <https://developer.spotify.com/documentation/web-api/reference/search>`_
410- and `deezer <https://developers.deezer.com/api/search>`_.
411-
412- :param filters: Field filters to apply.
413- :param query_string: Query keywords to use.
414- :return: Query string to be provided to the search API.
415- """
416-
417- components = [query_string , * (f'{ k } :"{ v } "' for k , v in filters .items ())]
418- query = " " .join (filter (None , components ))
419-
420- if self .config ["search_query_ascii" ].get ():
421- query = unidecode .unidecode (query )
422-
423- return query
425+ results = self ._get_candidates ("track" , [item ], artist , title , False )
426+ return filter (None , self .tracks_for_ids (r ["id" ] for r in results ))
424427
425428
426429# Dynamically copy methods to BeetsPlugin for legacy support
0 commit comments