diff --git a/deezspot/easy_spoty.py b/deezspot/easy_spoty.py index 512c68a..e52dd21 100644 --- a/deezspot/easy_spoty.py +++ b/deezspot/easy_spoty.py @@ -482,9 +482,22 @@ class Spo: @classmethod def search(cls, query, search_type='track', limit=10, country: Optional[str] = None, locale: Optional[str] = None, catalogue: Optional[str] = None, image_size: Optional[str] = None, client_id=None, client_secret=None): cls.__check_initialized() - # Map simple type value; librespot returns a combined JSON-like response + # Preferred path: use LibrespotClient for consistent defaults and options + if cls.__client is not None: + res = cls.__client.search( + query=query, + limit=limit, + country=country or cls.__get_session_country_code(), + locale=locale, + catalogue=catalogue, + image_size=image_size, + ) + # Optionally filter by type if requested (best-effort; librespot returns mixed) + if search_type and isinstance(res, dict) and search_type in res: + return {search_type: res.get(search_type)} + return res + # Fallback: direct SearchManager req = SearchManager.SearchRequest(query).set_limit(limit) - # Country precedence: explicit country > session country if country: req.set_country(country) else: @@ -498,4 +511,6 @@ class Spo: if image_size: req.set_image_size(image_size) res = cls.__session.search().request(req) # type: ignore[union-attr] + if search_type and isinstance(res, dict) and search_type in res: + return {search_type: res.get(search_type)} return res diff --git a/deezspot/libutils/librespot.py b/deezspot/libutils/librespot.py index e5e4266..14afbd8 100644 --- a/deezspot/libutils/librespot.py +++ b/deezspot/libutils/librespot.py @@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional, Union from google.protobuf.descriptor import FieldDescriptor from google.protobuf.message import Message -from librespot.core import Session +from librespot.core import Session, SearchManager from librespot.metadata import AlbumId, ArtistId, PlaylistId, TrackId from librespot import util from librespot.proto import Metadata_pb2 as Metadata @@ -68,6 +68,34 @@ class LibrespotClient: playlist_proto = self._session.api().get_playlist(playlist_id) return self._playlist_proto_to_object(playlist_proto, include_track_objects=expand_items) + def search( + self, + query: str, + limit: int = 10, + country: Optional[str] = None, + locale: Optional[str] = None, + catalogue: Optional[str] = None, + image_size: Optional[str] = None, + ) -> Dict[str, Any]: + """Perform a full-featured search using librespot's SearchManager. + + - country precedence: explicit country > session country code > unset + - returns the raw JSON-like mapping response provided by librespot + """ + req = SearchManager.SearchRequest(query).set_limit(limit) + # Country precedence + cc = country or self._get_session_country_code() + if cc: + req.set_country(cc) + if locale: + req.set_locale(locale) + if catalogue: + req.set_catalogue(catalogue) + if image_size: + req.set_image_size(image_size) + res = self._session.search().request(req) + return res + # ---------- ID parsing helpers ---------- @staticmethod @@ -118,6 +146,18 @@ class LibrespotClient: builder.stored_file(stored_credentials_path) return builder.create() + def _get_session_country_code(self) -> str: + try: + cc = getattr(self._session, "_Session__country_code", None) + if isinstance(cc, str) and len(cc) == 2: + return cc + cc2 = getattr(self._session, "country_code", None) + if isinstance(cc2, str) and len(cc2) == 2: + return cc2 + except Exception: + pass + return "" + # ---------- Private: ID coercion ---------- def _ensure_track_id(self, v: Union[str, TrackId]) -> TrackId: diff --git a/docs/librespot_client.md b/docs/librespot_client.md index eb2d4ee..7793e09 100644 --- a/docs/librespot_client.md +++ b/docs/librespot_client.md @@ -189,6 +189,24 @@ playlist_expanded = client.get_playlist("spotify:playlist:...", expand_items=Tru ``` +### search(query, limit=10, country=None, locale=None, catalogue=None, image_size=None) -> dict +Performs a full-featured search using librespot's SearchManager. + +- Country precedence: explicit `country` > session country code > unset +- Returns librespot's JSON-like mapping (tracks, albums, artists, playlists, etc.) + +Usage: + +```python +res = client.search( + "artist:daft punk track:one more time", + limit=10, + country="US", + locale="en_US" +) +tracks = res.get("tracks", {}).get("items", []) +``` + ## Concurrency and caching - When expanding tracks for albums/playlists, the client concurrently fetches missing track objects using a `ThreadPoolExecutor` with up to `max_workers` threads (default 16).