feat: implement public librespot api class, see docs
This commit is contained in:
241
docs/librespot_client.md
Normal file
241
docs/librespot_client.md
Normal file
@@ -0,0 +1,241 @@
|
||||
### LibrespotClient wrapper
|
||||
|
||||
A thin, high-level wrapper over the internal librespot API that returns Web API-like dictionaries for albums, tracks, artists, and playlists. Use this to standardize access to Spotify metadata throughout the codebase.
|
||||
|
||||
- Import path: `from deezspot.libutils import LibrespotClient`
|
||||
- Backed by: `librespot` internal API (Session + Metadata/Playlist protos)
|
||||
- Thread-safe for read operations; uses an internal in-memory cache for track object expansions
|
||||
|
||||
|
||||
## Initialization
|
||||
|
||||
```python
|
||||
from deezspot.libutils import LibrespotClient
|
||||
|
||||
# 1) Create with stored credentials file (recommended)
|
||||
client = LibrespotClient(stored_credentials_path="/absolute/path/to/credentials.json")
|
||||
|
||||
# 2) Or reuse an existing librespot Session
|
||||
# from librespot.core import Session
|
||||
# session = ...
|
||||
# client = LibrespotClient(session=session)
|
||||
```
|
||||
|
||||
- **stored_credentials_path**: path to JSON created by librespot credential flow
|
||||
- **session**: optional existing `librespot.core.Session` (if provided, `stored_credentials_path` is not required)
|
||||
- **max_workers**: optional concurrency cap for track expansion (default 16, bounded [1, 32])
|
||||
|
||||
Always dispose when done:
|
||||
|
||||
```python
|
||||
client.close()
|
||||
```
|
||||
|
||||
|
||||
## ID/URI inputs
|
||||
|
||||
All data-fetching methods accept either:
|
||||
- A Spotify URI (e.g., `spotify:album:...`, `spotify:track:...`, etc)
|
||||
- A base62 ID (e.g., `3KuXEGcqLcnEYWnn3OEGy0`)
|
||||
- A public `open.spotify.com/...` URL (album/track/artist/playlist)
|
||||
- Or the corresponding `librespot.metadata.*Id` class
|
||||
|
||||
You can also use the helper if needed:
|
||||
|
||||
```python
|
||||
# kind in {"track", "album", "artist", "playlist"}
|
||||
track_id = LibrespotClient.parse_input_id("track", "https://open.spotify.com/track/...")
|
||||
```
|
||||
|
||||
|
||||
## Public API
|
||||
|
||||
### get_album(album, include_tracks=False) -> dict
|
||||
Fetches album metadata.
|
||||
|
||||
- **album**: URI/base62/URL or `AlbumId`
|
||||
- **include_tracks**: when True, expands the album's tracks to full track objects using concurrent fetches; when False, returns `tracks` as an array of track base62 IDs
|
||||
|
||||
Return shape (subset):
|
||||
|
||||
```json
|
||||
{
|
||||
"album_type": "album",
|
||||
"total_tracks": 10,
|
||||
"available_markets": ["US", "GB"],
|
||||
"external_urls": {"spotify": "https://open.spotify.com/album/{id}"},
|
||||
"id": "{base62}",
|
||||
"images": [{"url": "https://...", "width": 640, "height": 640}],
|
||||
"name": "...",
|
||||
"release_date": "2020-05-01",
|
||||
"release_date_precision": "day",
|
||||
"type": "album",
|
||||
"uri": "spotify:album:{base62}",
|
||||
"artists": [{"id": "...", "name": "...", "type": "artist", "uri": "..."}],
|
||||
"tracks": [
|
||||
// include_tracks=False -> ["trackBase62", ...]
|
||||
// include_tracks=True -> [{ track object }, ...]
|
||||
],
|
||||
"copyrights": [{"text": "...", "type": "..."}],
|
||||
"external_ids": {"upc": "..."},
|
||||
"label": "...",
|
||||
"popularity": 57
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```python
|
||||
album = client.get_album("spotify:album:...", include_tracks=True)
|
||||
```
|
||||
|
||||
|
||||
### get_track(track) -> dict
|
||||
Fetches track metadata.
|
||||
|
||||
- **track**: URI/base62/URL or `TrackId`
|
||||
|
||||
Return shape (subset):
|
||||
|
||||
```json
|
||||
{
|
||||
"album": { /* embedded album (no tracks) */ },
|
||||
"artists": [{"id": "...", "name": "..."}],
|
||||
"available_markets": ["US", "GB"],
|
||||
"disc_number": 1,
|
||||
"duration_ms": 221000,
|
||||
"explicit": false,
|
||||
"external_ids": {"isrc": "..."},
|
||||
"external_urls": {"spotify": "https://open.spotify.com/track/{id}"},
|
||||
"id": "{base62}",
|
||||
"name": "...",
|
||||
"popularity": 65,
|
||||
"track_number": 1,
|
||||
"type": "track",
|
||||
"uri": "spotify:track:{base62}",
|
||||
"preview_url": "https://p.scdn.co/mp3-preview/{hex}",
|
||||
"has_lyrics": true,
|
||||
"earliest_live_timestamp": 0,
|
||||
"licensor_uuid": "{hex}" // when available
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```python
|
||||
track = client.get_track("3KuXEGcqLcnEYWnn3OEGy0")
|
||||
```
|
||||
|
||||
|
||||
### get_artist(artist) -> dict
|
||||
Fetches artist metadata and returns a full JSON-like mapping of the protobuf (pruned of empty fields).
|
||||
|
||||
- **artist**: URI/base62/URL or `ArtistId`
|
||||
|
||||
Usage:
|
||||
|
||||
```python
|
||||
artist = client.get_artist("https://open.spotify.com/artist/...")
|
||||
```
|
||||
|
||||
|
||||
### get_playlist(playlist, expand_items=False) -> dict
|
||||
Fetches playlist contents.
|
||||
|
||||
- **playlist**: URI/URL/ID or `PlaylistId` (Spotify uses non-base62 playlist IDs)
|
||||
- **expand_items**: when True, playlist items containing tracks are expanded to full track objects (concurrent fetch with caching); otherwise, items contain minimal track stubs with `id`, `uri`, `type`, and `external_urls`
|
||||
|
||||
Return shape (subset):
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "My Playlist",
|
||||
"description": "...",
|
||||
"collaborative": false,
|
||||
"images": [{"url": "https://..."}],
|
||||
"owner": {
|
||||
"id": "username",
|
||||
"type": "user",
|
||||
"uri": "spotify:user:username",
|
||||
"external_urls": {"spotify": "https://open.spotify.com/user/username"},
|
||||
"display_name": "username"
|
||||
},
|
||||
"snapshot_id": "base64Revision==",
|
||||
"tracks": {
|
||||
"offset": 0,
|
||||
"total": 42,
|
||||
"items": [
|
||||
{
|
||||
"added_at": "2023-01-01T12:34:56Z",
|
||||
"added_by": {"id": "...", "type": "user", "uri": "...", "external_urls": {"spotify": "..."}, "display_name": "..."},
|
||||
"is_local": false,
|
||||
"track": {
|
||||
// expand_items=False -> {"id": "...", "uri": "spotify:track:...", "type": "track", "external_urls": {"spotify": "..."}}
|
||||
// expand_items=True -> full track object
|
||||
},
|
||||
"item_id": "{hex}" // additional reference, not a Web API field
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "playlist"
|
||||
}
|
||||
```
|
||||
|
||||
Usage:
|
||||
|
||||
```python
|
||||
playlist = client.get_playlist("spotify:playlist:...")
|
||||
playlist_expanded = client.get_playlist("spotify:playlist:...", expand_items=True)
|
||||
```
|
||||
|
||||
|
||||
## 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).
|
||||
- A per-instance in-memory cache stores fetched track objects keyed by base62 ID to avoid duplicate network calls in the same process.
|
||||
|
||||
|
||||
## Error handling
|
||||
|
||||
- Underlying network/protobuf errors are not swallowed; wrap your calls if you need custom handling.
|
||||
- Empty/missing fields are pruned from output structures where appropriate.
|
||||
|
||||
Example:
|
||||
|
||||
```python
|
||||
try:
|
||||
data = client.get_album("spotify:album:...")
|
||||
except Exception as exc:
|
||||
# handle failure (retry/backoff/logging)
|
||||
raise
|
||||
```
|
||||
|
||||
|
||||
## Migration guide (from direct librespot usage)
|
||||
|
||||
Before (direct protobuf access):
|
||||
|
||||
```python
|
||||
album_id = AlbumId.from_base62(base62)
|
||||
proto = session.api().get_metadata_4_album(album_id)
|
||||
# manual traversal over `proto`...
|
||||
```
|
||||
|
||||
After (wrapper):
|
||||
|
||||
```python
|
||||
from deezspot.libutils import LibrespotClient
|
||||
|
||||
client = LibrespotClient(stored_credentials_path="/path/to/credentials.json")
|
||||
try:
|
||||
album = client.get_album(base62, include_tracks=True)
|
||||
finally:
|
||||
client.close()
|
||||
```
|
||||
|
||||
|
||||
## Notes
|
||||
|
||||
- Image URLs are derived from internal `file_id` bytes using the public Spotify image host.
|
||||
- Playlist IDs are not base62; pass the raw ID, URI, or URL.
|
||||
- For performance-critical paths, reuse a single `LibrespotClient` instance (and its cache) per worker.
|
||||
Reference in New Issue
Block a user