feat: implement librespot-powered search
This commit is contained in:
@@ -482,9 +482,22 @@ class Spo:
|
|||||||
@classmethod
|
@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):
|
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()
|
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)
|
req = SearchManager.SearchRequest(query).set_limit(limit)
|
||||||
# Country precedence: explicit country > session country
|
|
||||||
if country:
|
if country:
|
||||||
req.set_country(country)
|
req.set_country(country)
|
||||||
else:
|
else:
|
||||||
@@ -498,4 +511,6 @@ class Spo:
|
|||||||
if image_size:
|
if image_size:
|
||||||
req.set_image_size(image_size)
|
req.set_image_size(image_size)
|
||||||
res = cls.__session.search().request(req) # type: ignore[union-attr]
|
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
|
return res
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional, Union
|
|||||||
from google.protobuf.descriptor import FieldDescriptor
|
from google.protobuf.descriptor import FieldDescriptor
|
||||||
from google.protobuf.message import Message
|
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.metadata import AlbumId, ArtistId, PlaylistId, TrackId
|
||||||
from librespot import util
|
from librespot import util
|
||||||
from librespot.proto import Metadata_pb2 as Metadata
|
from librespot.proto import Metadata_pb2 as Metadata
|
||||||
@@ -68,6 +68,34 @@ class LibrespotClient:
|
|||||||
playlist_proto = self._session.api().get_playlist(playlist_id)
|
playlist_proto = self._session.api().get_playlist(playlist_id)
|
||||||
return self._playlist_proto_to_object(playlist_proto, include_track_objects=expand_items)
|
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 ----------
|
# ---------- ID parsing helpers ----------
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -118,6 +146,18 @@ class LibrespotClient:
|
|||||||
builder.stored_file(stored_credentials_path)
|
builder.stored_file(stored_credentials_path)
|
||||||
return builder.create()
|
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 ----------
|
# ---------- Private: ID coercion ----------
|
||||||
|
|
||||||
def _ensure_track_id(self, v: Union[str, TrackId]) -> TrackId:
|
def _ensure_track_id(self, v: Union[str, TrackId]) -> TrackId:
|
||||||
|
|||||||
@@ -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
|
## 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).
|
- When expanding tracks for albums/playlists, the client concurrently fetches missing track objects using a `ThreadPoolExecutor` with up to `max_workers` threads (default 16).
|
||||||
|
|||||||
Reference in New Issue
Block a user