feat: implement public librespot api class, see docs

This commit is contained in:
Xoconoch
2025-08-26 22:02:38 -06:00
parent 0d2607e263
commit c38f10957c
11 changed files with 1761 additions and 337 deletions

View File

@@ -0,0 +1,23 @@
#!/usr/bin/python3
from .types import Image, ExternalUrls, ArtistRef, AlbumRef
from .track import Track
from .album import Album
from .playlist import Playlist, PlaylistItem, TrackStub, TracksPage, Owner, UserMini
from .artist import Artist
__all__ = [
"Image",
"ExternalUrls",
"ArtistRef",
"AlbumRef",
"Track",
"Album",
"Playlist",
"PlaylistItem",
"TrackStub",
"TracksPage",
"Owner",
"UserMini",
"Artist",
]

View File

@@ -0,0 +1,92 @@
#!/usr/bin/python3
from dataclasses import dataclass, field
from typing import Optional, Dict, Any, List, Union
from .types import ExternalUrls, Image, ArtistRef, _str, _int
from .track import Track as TrackModel
@dataclass
class Album:
id: Optional[str] = None
name: Optional[str] = None
uri: Optional[str] = None
type: str = "album"
album_type: Optional[str] = None
release_date: Optional[str] = None
release_date_precision: Optional[str] = None
total_tracks: Optional[int] = None
label: Optional[str] = None
popularity: Optional[int] = None
external_urls: ExternalUrls = field(default_factory=ExternalUrls)
external_ids: Dict[str, str] = field(default_factory=dict)
available_markets: Optional[List[str]] = None
images: Optional[List[Image]] = None
artists: List[ArtistRef] = field(default_factory=list)
tracks: Optional[List[Union[str, TrackModel]]] = None
copyrights: Optional[List[Dict[str, Any]]] = None
@staticmethod
def from_dict(obj: Any) -> "Album":
if not isinstance(obj, dict):
return Album()
imgs: List[Image] = []
for im in obj.get("images", []) or []:
im_obj = Image.from_dict(im)
if im_obj:
imgs.append(im_obj)
artists: List[ArtistRef] = []
for a in obj.get("artists", []) or []:
artists.append(ArtistRef.from_dict(a))
# Tracks can be base62 strings or full track dicts
tracks_in: List[Union[str, TrackModel]] = []
if isinstance(obj.get("tracks"), list):
for t in obj.get("tracks"):
if isinstance(t, dict):
tracks_in.append(TrackModel.from_dict(t))
else:
ts = _str(t)
if ts:
tracks_in.append(ts)
return Album(
id=_str(obj.get("id")),
name=_str(obj.get("name")),
uri=_str(obj.get("uri")),
type=_str(obj.get("type")) or "album",
album_type=_str(obj.get("album_type")),
release_date=_str(obj.get("release_date")),
release_date_precision=_str(obj.get("release_date_precision")),
total_tracks=_int(obj.get("total_tracks")),
label=_str(obj.get("label")),
popularity=_int(obj.get("popularity")),
external_urls=ExternalUrls.from_dict(obj.get("external_urls", {})),
external_ids=dict(obj.get("external_ids", {}) or {}),
available_markets=list(obj.get("available_markets", []) or []),
images=imgs or None,
artists=artists,
tracks=tracks_in or None,
copyrights=list(obj.get("copyrights", []) or []),
)
def to_dict(self) -> Dict[str, Any]:
out = {
"id": self.id,
"name": self.name,
"uri": self.uri,
"type": self.type,
"album_type": self.album_type,
"release_date": self.release_date,
"release_date_precision": self.release_date_precision,
"total_tracks": self.total_tracks,
"label": self.label,
"popularity": self.popularity,
"external_urls": self.external_urls.to_dict(),
"external_ids": self.external_ids or {},
"available_markets": self.available_markets or [],
"images": [im.to_dict() for im in (self.images or [])],
"artists": [a.to_dict() for a in (self.artists or [])],
"tracks": [t.to_dict() if isinstance(t, TrackModel) else t for t in (self.tracks or [])],
"copyrights": self.copyrights or [],
}
return {k: v for k, v in out.items() if v not in (None, {}, [], "")}

View File

@@ -0,0 +1,63 @@
#!/usr/bin/python3
from dataclasses import dataclass, field
from typing import Optional, Dict, Any, List
from .types import ExternalUrls, Image, _str, _int
@dataclass
class Artist:
id: Optional[str] = None
name: Optional[str] = None
uri: Optional[str] = None
type: str = "artist"
genres: List[str] = field(default_factory=list)
images: Optional[List[Image]] = None
popularity: Optional[int] = None
external_urls: ExternalUrls = field(default_factory=ExternalUrls)
album_group: List[str] = field(default_factory=list)
single_group: List[str] = field(default_factory=list)
compilation_group: List[str] = field(default_factory=list)
appears_on_group: List[str] = field(default_factory=list)
@staticmethod
def from_dict(obj: Any) -> "Artist":
if not isinstance(obj, dict):
return Artist()
imgs: List[Image] = []
for im in obj.get("images", []) or []:
im_obj = Image.from_dict(im)
if im_obj:
imgs.append(im_obj)
return Artist(
id=_str(obj.get("id")),
name=_str(obj.get("name")),
uri=_str(obj.get("uri")),
type=_str(obj.get("type")) or "artist",
genres=list(obj.get("genres", []) or []),
images=imgs or None,
popularity=_int(obj.get("popularity")),
external_urls=ExternalUrls.from_dict(obj.get("external_urls", {})),
album_group=list(obj.get("album_group", []) or []),
single_group=list(obj.get("single_group", []) or []),
compilation_group=list(obj.get("compilation_group", []) or []),
appears_on_group=list(obj.get("appears_on_group", []) or []),
)
def to_dict(self) -> Dict[str, Any]:
out = {
"id": self.id,
"name": self.name,
"uri": self.uri,
"type": self.type,
"genres": self.genres or [],
"images": [im.to_dict() for im in (self.images or [])],
"popularity": self.popularity,
"external_urls": self.external_urls.to_dict(),
"album_group": self.album_group or [],
"single_group": self.single_group or [],
"compilation_group": self.compilation_group or [],
"appears_on_group": self.appears_on_group or [],
}
return {k: v for k, v in out.items() if v not in (None, {}, [], "")}

View File

@@ -0,0 +1,207 @@
#!/usr/bin/python3
from dataclasses import dataclass, field
from typing import Optional, Dict, Any, List, Union
from .types import ExternalUrls, Image, _str, _int
from .track import Track as TrackModel
@dataclass
class UserMini:
id: Optional[str] = None
type: str = "user"
uri: Optional[str] = None
display_name: Optional[str] = None
external_urls: ExternalUrls = field(default_factory=ExternalUrls)
@staticmethod
def from_dict(obj: Any) -> "UserMini":
if not isinstance(obj, dict):
return UserMini()
return UserMini(
id=_str(obj.get("id")),
type=_str(obj.get("type")) or "user",
uri=_str(obj.get("uri")),
display_name=_str(obj.get("display_name")),
external_urls=ExternalUrls.from_dict(obj.get("external_urls", {})),
)
def to_dict(self) -> Dict[str, Any]:
out = {
"id": self.id,
"type": self.type,
"uri": self.uri,
"display_name": self.display_name,
"external_urls": self.external_urls.to_dict(),
}
return {k: v for k, v in out.items() if v not in (None, {}, [], "")}
@dataclass
class TrackStub:
id: Optional[str] = None
type: str = "track"
uri: Optional[str] = None
external_urls: ExternalUrls = field(default_factory=ExternalUrls)
@staticmethod
def from_dict(obj: Any) -> "TrackStub":
if not isinstance(obj, dict):
return TrackStub()
return TrackStub(
id=_str(obj.get("id")),
type=_str(obj.get("type")) or "track",
uri=_str(obj.get("uri")),
external_urls=ExternalUrls.from_dict(obj.get("external_urls", {})),
)
def to_dict(self) -> Dict[str, Any]:
out = {
"id": self.id,
"type": self.type,
"uri": self.uri,
"external_urls": self.external_urls.to_dict(),
}
return {k: v for k, v in out.items() if v not in (None, {}, [], "")}
@dataclass
class PlaylistItem:
added_at: Optional[str] = None
added_by: UserMini = field(default_factory=UserMini)
is_local: bool = False
track: Optional[Union[TrackModel, TrackStub]] = None
item_id: Optional[str] = None
@staticmethod
def from_dict(obj: Any) -> "PlaylistItem":
if not isinstance(obj, dict):
return PlaylistItem()
track_obj = None
trk = obj.get("track")
if isinstance(trk, dict):
if trk.get("duration_ms") is not None:
track_obj = TrackModel.from_dict(trk)
else:
track_obj = TrackStub.from_dict(trk)
return PlaylistItem(
added_at=_str(obj.get("added_at")),
added_by=UserMini.from_dict(obj.get("added_by", {})),
is_local=bool(obj.get("is_local", False)),
track=track_obj,
item_id=_str(obj.get("item_id")),
)
def to_dict(self) -> Dict[str, Any]:
out = {
"added_at": self.added_at,
"added_by": self.added_by.to_dict(),
"is_local": self.is_local,
"track": self.track.to_dict() if hasattr(self.track, 'to_dict') and self.track else None,
"item_id": self.item_id,
}
return {k: v for k, v in out.items() if v not in (None, {}, [], "")}
@dataclass
class TracksPage:
offset: int = 0
total: int = 0
items: List[PlaylistItem] = field(default_factory=list)
@staticmethod
def from_dict(obj: Any) -> "TracksPage":
if not isinstance(obj, dict):
return TracksPage(items=[])
items: List[PlaylistItem] = []
for it in obj.get("items", []) or []:
items.append(PlaylistItem.from_dict(it))
return TracksPage(
offset=_int(obj.get("offset")) or 0,
total=_int(obj.get("total")) or len(items),
items=items,
)
def to_dict(self) -> Dict[str, Any]:
return {
"offset": self.offset,
"total": self.total,
"items": [it.to_dict() for it in (self.items or [])]
}
@dataclass
class Owner:
id: Optional[str] = None
type: str = "user"
uri: Optional[str] = None
display_name: Optional[str] = None
external_urls: ExternalUrls = field(default_factory=ExternalUrls)
@staticmethod
def from_dict(obj: Any) -> "Owner":
if not isinstance(obj, dict):
return Owner()
return Owner(
id=_str(obj.get("id")),
type=_str(obj.get("type")) or "user",
uri=_str(obj.get("uri")),
display_name=_str(obj.get("display_name")),
external_urls=ExternalUrls.from_dict(obj.get("external_urls", {})),
)
def to_dict(self) -> Dict[str, Any]:
out = {
"id": self.id,
"type": self.type,
"uri": self.uri,
"display_name": self.display_name,
"external_urls": self.external_urls.to_dict(),
}
return {k: v for k, v in out.items() if v not in (None, {}, [], "")}
@dataclass
class Playlist:
name: Optional[str] = None
description: Optional[str] = None
collaborative: Optional[bool] = None
images: Optional[List[Image]] = None
owner: Owner = field(default_factory=Owner)
snapshot_id: Optional[str] = None
tracks: TracksPage = field(default_factory=lambda: TracksPage(items=[]))
type: str = "playlist"
@staticmethod
def from_dict(obj: Any) -> "Playlist":
if not isinstance(obj, dict):
return Playlist(tracks=TracksPage(items=[]))
imgs: List[Image] = []
for im in obj.get("images", []) or []:
im_obj = Image.from_dict(im)
if im_obj:
imgs.append(im_obj)
return Playlist(
name=_str(obj.get("name")),
description=_str(obj.get("description")),
collaborative=bool(obj.get("collaborative")) if obj.get("collaborative") is not None else None,
images=imgs or None,
owner=Owner.from_dict(obj.get("owner", {})),
snapshot_id=_str(obj.get("snapshot_id")),
tracks=TracksPage.from_dict(obj.get("tracks", {})),
type=_str(obj.get("type")) or "playlist",
)
def to_dict(self) -> Dict[str, Any]:
out = {
"name": self.name,
"description": self.description,
"collaborative": self.collaborative,
"images": [im.to_dict() for im in (self.images or [])],
"owner": self.owner.to_dict(),
"snapshot_id": self.snapshot_id,
"tracks": self.tracks.to_dict(),
"type": self.type,
}
return {k: v for k, v in out.items() if v not in (None, {}, [], "")}

View File

@@ -0,0 +1,82 @@
#!/usr/bin/python3
from dataclasses import dataclass, field
from typing import Optional, Dict, Any, List
from .types import ExternalUrls, Image, ArtistRef, AlbumRef, _str, _int, _bool
@dataclass
class Track:
id: Optional[str] = None
name: Optional[str] = None
uri: Optional[str] = None
type: str = "track"
duration_ms: Optional[int] = None
explicit: Optional[bool] = None
track_number: Optional[int] = None
disc_number: Optional[int] = None
popularity: Optional[int] = None
preview_url: Optional[str] = None
earliest_live_timestamp: Optional[int] = None
has_lyrics: Optional[bool] = None
licensor_uuid: Optional[str] = None
external_urls: ExternalUrls = field(default_factory=ExternalUrls)
external_ids: Dict[str, str] = field(default_factory=dict)
available_markets: Optional[List[str]] = None
artists: List[ArtistRef] = field(default_factory=list)
album: Optional[AlbumRef] = None
@staticmethod
def from_dict(obj: Any) -> "Track":
if not isinstance(obj, dict):
return Track()
artists: List[ArtistRef] = []
for a in obj.get("artists", []) or []:
artists.append(ArtistRef.from_dict(a))
album_ref = None
if isinstance(obj.get("album"), dict):
album_ref = AlbumRef.from_dict(obj.get("album"))
return Track(
id=_str(obj.get("id")),
name=_str(obj.get("name")),
uri=_str(obj.get("uri")),
type=_str(obj.get("type")) or "track",
duration_ms=_int(obj.get("duration_ms")),
explicit=_bool(obj.get("explicit")),
track_number=_int(obj.get("track_number")),
disc_number=_int(obj.get("disc_number")),
popularity=_int(obj.get("popularity")),
preview_url=_str(obj.get("preview_url")),
earliest_live_timestamp=_int(obj.get("earliest_live_timestamp")),
has_lyrics=_bool(obj.get("has_lyrics")),
licensor_uuid=_str(obj.get("licensor_uuid")),
external_urls=ExternalUrls.from_dict(obj.get("external_urls", {})),
external_ids=dict(obj.get("external_ids", {}) or {}),
available_markets=list(obj.get("available_markets", []) or []),
artists=artists,
album=album_ref,
)
def to_dict(self) -> Dict[str, Any]:
out = {
"id": self.id,
"name": self.name,
"uri": self.uri,
"type": self.type,
"duration_ms": self.duration_ms,
"explicit": self.explicit,
"track_number": self.track_number,
"disc_number": self.disc_number,
"popularity": self.popularity,
"preview_url": self.preview_url,
"earliest_live_timestamp": self.earliest_live_timestamp,
"has_lyrics": self.has_lyrics,
"licensor_uuid": self.licensor_uuid,
"external_urls": self.external_urls.to_dict(),
"external_ids": self.external_ids or {},
"available_markets": self.available_markets or [],
"artists": [a.to_dict() for a in (self.artists or [])],
"album": self.album.to_dict() if self.album else None,
}
return {k: v for k, v in out.items() if v not in (None, {}, [], "")}

View File

@@ -0,0 +1,153 @@
#!/usr/bin/python3
from dataclasses import dataclass, field
from typing import Optional, Dict, Any, List
def _str(v: Any) -> Optional[str]:
if v is None:
return None
try:
s = str(v)
return s
except Exception:
return None
def _int(v: Any) -> Optional[int]:
try:
if v is None:
return None
return int(v)
except Exception:
return None
def _bool(v: Any) -> Optional[bool]:
if isinstance(v, bool):
return v
if v in ("true", "True", "1", 1):
return True
if v in ("false", "False", "0", 0):
return False
return None
def _list_str(v: Any) -> Optional[List[str]]:
if v is None:
return []
if isinstance(v, list):
out: List[str] = []
for it in v:
s = _str(it)
if s is not None:
out.append(s)
return out
return []
@dataclass
class ExternalUrls:
spotify: Optional[str] = None
@staticmethod
def from_dict(obj: Any) -> "ExternalUrls":
if not isinstance(obj, dict):
return ExternalUrls()
return ExternalUrls(
spotify=_str(obj.get("spotify"))
)
def to_dict(self) -> Dict[str, Any]:
return {"spotify": self.spotify} if self.spotify else {}
@dataclass
class Image:
url: str
width: int = 0
height: int = 0
@staticmethod
def from_dict(obj: Any) -> Optional["Image"]:
if not isinstance(obj, dict):
return None
url = _str(obj.get("url"))
if not url:
return None
w = _int(obj.get("width")) or 0
h = _int(obj.get("height")) or 0
return Image(url=url, width=w, height=h)
def to_dict(self) -> Dict[str, Any]:
return {"url": self.url, "width": self.width, "height": self.height}
@dataclass
class ArtistRef:
id: Optional[str] = None
name: str = ""
type: str = "artist"
uri: Optional[str] = None
external_urls: ExternalUrls = field(default_factory=ExternalUrls)
@staticmethod
def from_dict(obj: Any) -> "ArtistRef":
if not isinstance(obj, dict):
return ArtistRef()
return ArtistRef(
id=_str(obj.get("id")),
name=_str(obj.get("name")) or "",
type=_str(obj.get("type")) or "artist",
uri=_str(obj.get("uri")),
external_urls=ExternalUrls.from_dict(obj.get("external_urls", {})),
)
def to_dict(self) -> Dict[str, Any]:
out = {
"id": self.id,
"name": self.name,
"type": self.type,
"uri": self.uri,
"external_urls": self.external_urls.to_dict()
}
return {k: v for k, v in out.items() if v not in (None, {}, [], "")}
@dataclass
class AlbumRef:
id: Optional[str] = None
name: Optional[str] = None
type: str = "album"
uri: Optional[str] = None
external_urls: ExternalUrls = field(default_factory=ExternalUrls)
images: Optional[List[Image]] = None
@staticmethod
def from_dict(obj: Any) -> "AlbumRef":
if not isinstance(obj, dict):
return AlbumRef()
imgs: List[Image] = []
for im in obj.get("images", []) or []:
im_obj = Image.from_dict(im)
if im_obj:
imgs.append(im_obj)
return AlbumRef(
id=_str(obj.get("id")),
name=_str(obj.get("name")),
type=_str(obj.get("type")) or "album",
uri=_str(obj.get("uri")),
external_urls=ExternalUrls.from_dict(obj.get("external_urls", {})),
images=imgs or None,
)
def to_dict(self) -> Dict[str, Any]:
out = {
"id": self.id,
"name": self.name,
"type": self.type,
"uri": self.uri,
"external_urls": self.external_urls.to_dict(),
"images": [im.to_dict() for im in (self.images or [])]
}
return {k: v for k, v in out.items() if v not in (None, {}, [], "")}