feat: include image to playlist internal librespot api

This commit is contained in:
Xoconoch
2025-08-28 08:19:53 -06:00
parent 73919dbddb
commit 4e2eec3bd2
2 changed files with 53 additions and 18 deletions

View File

@@ -278,6 +278,23 @@ class LibrespotClient:
return None return None
return f"https://i.scdn.co/image/{util.bytes_to_hex(file_id)}" return f"https://i.scdn.co/image/{util.bytes_to_hex(file_id)}"
def _get_playlist_picture_url(self, attrs: Any) -> Optional[str]:
pic = getattr(attrs, "picture", b"") if attrs else b""
try:
if not pic:
return None
data = bytes(pic)
image_id: Optional[bytes] = None
if len(data) >= 26 and data[0] == 0xAB and data[1:4] == b"gpl" and data[4:6] == b"\x00\x00":
image_id = data[6:26]
elif len(data) >= 20:
image_id = data[:20]
if image_id:
return f"https://i.scdn.co/image/{util.bytes_to_hex(image_id)}"
except Exception:
pass
return None
@staticmethod @staticmethod
def _split_countries(countries: str) -> List[str]: def _split_countries(countries: str) -> List[str]:
if not countries: if not countries:
@@ -531,10 +548,12 @@ class LibrespotClient:
picture_bytes = getattr(attrs, "picture", b"") if attrs else b"" picture_bytes = getattr(attrs, "picture", b"") if attrs else b""
images: List[Dict[str, Any]] = [] images: List[Dict[str, Any]] = []
if isinstance(picture_bytes, (bytes, bytearray)) and len(picture_bytes) in (16, 20): picture_url: Optional[str] = None
url = self._image_url_from_file_id(picture_bytes) # Derive picture URL from attributes.picture with header-aware parsing
if url: pic_url = self._get_playlist_picture_url(attrs)
images.append({"url": url, "width": 0, "height": 0}) if pic_url:
picture_url = pic_url
images.append({"url": pic_url, "width": 0, "height": 0})
owner_username = getattr(p, "owner_username", "") or "" owner_username = getattr(p, "owner_username", "") or ""
@@ -542,14 +561,15 @@ class LibrespotClient:
contents = getattr(p, "contents", None) contents = getattr(p, "contents", None)
fetched_tracks: Dict[str, Optional[Dict[str, Any]]] = {} fetched_tracks: Dict[str, Optional[Dict[str, Any]]] = {}
if include_track_objects and self._session is not None and contents is not None: # Collect all track ids to fetch durations for length computation and, if requested, for expansion
to_fetch: List[str] = [] to_fetch: List[str] = []
if contents is not None:
for it in getattr(contents, "items", []): for it in getattr(contents, "items", []):
uri = getattr(it, "uri", "") or "" uri = getattr(it, "uri", "") or ""
if uri.startswith("spotify:track:"): if uri.startswith("spotify:track:"):
b62 = uri.split(":")[-1] b62 = uri.split(":")[-1]
to_fetch.append(b62) to_fetch.append(b62)
if to_fetch: if to_fetch and self._session is not None:
fetched_tracks = self._fetch_track_objects(to_fetch) fetched_tracks = self._fetch_track_objects(to_fetch)
if contents is not None: if contents is not None:
@@ -609,6 +629,22 @@ class LibrespotClient:
"items": items, "items": items,
}) })
# Compute playlist length (in seconds) by summing track durations
length_seconds: Optional[int] = None
try:
if to_fetch and fetched_tracks:
total_ms = 0
for b62 in to_fetch:
obj = fetched_tracks.get(b62)
if obj is None:
continue
dur = obj.get("duration_ms")
if isinstance(dur, int) and dur > 0:
total_ms += dur
length_seconds = (total_ms // 1000) if total_ms > 0 else 0
except Exception:
length_seconds = None
rev_bytes = getattr(p, "revision", b"") if hasattr(p, "revision") else b"" rev_bytes = getattr(p, "revision", b"") if hasattr(p, "revision") else b""
snapshot_b64 = base64.b64encode(rev_bytes).decode("ascii") if rev_bytes else None snapshot_b64 = base64.b64encode(rev_bytes).decode("ascii") if rev_bytes else None
@@ -616,7 +652,7 @@ class LibrespotClient:
"name": name or None, "name": name or None,
"description": description or None, "description": description or None,
"collaborative": collaborative or None, "collaborative": collaborative or None,
"images": images or None, "picture": picture_url or None,
"owner": self._prune_empty({ "owner": self._prune_empty({
"id": owner_username, "id": owner_username,
"type": "user", "type": "user",
@@ -625,6 +661,7 @@ class LibrespotClient:
"display_name": owner_username or None, "display_name": owner_username or None,
}), }),
"snapshot_id": snapshot_b64, "snapshot_id": snapshot_b64,
"length": length_seconds,
"tracks": tracks_obj, "tracks": tracks_obj,
"type": "playlist", "type": "playlist",
} }

View File

@@ -3,7 +3,7 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional, Dict, Any, List, Union from typing import Optional, Dict, Any, List, Union
from .types import ExternalUrls, Image, _str, _int from .types import ExternalUrls, _str, _int
from .track import Track as TrackModel from .track import Track as TrackModel
@@ -167,9 +167,10 @@ class Playlist:
name: Optional[str] = None name: Optional[str] = None
description: Optional[str] = None description: Optional[str] = None
collaborative: Optional[bool] = None collaborative: Optional[bool] = None
images: Optional[List[Image]] = None picture: Optional[str] = None
owner: Owner = field(default_factory=Owner) owner: Owner = field(default_factory=Owner)
snapshot_id: Optional[str] = None snapshot_id: Optional[str] = None
length: Optional[int] = None
tracks: TracksPage = field(default_factory=lambda: TracksPage(items=[])) tracks: TracksPage = field(default_factory=lambda: TracksPage(items=[]))
type: str = "playlist" type: str = "playlist"
@@ -177,18 +178,14 @@ class Playlist:
def from_dict(obj: Any) -> "Playlist": def from_dict(obj: Any) -> "Playlist":
if not isinstance(obj, dict): if not isinstance(obj, dict):
return Playlist(tracks=TracksPage(items=[])) 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( return Playlist(
name=_str(obj.get("name")), name=_str(obj.get("name")),
description=_str(obj.get("description")), description=_str(obj.get("description")),
collaborative=bool(obj.get("collaborative")) if obj.get("collaborative") is not None else None, collaborative=bool(obj.get("collaborative")) if obj.get("collaborative") is not None else None,
images=imgs or None, picture=_str(obj.get("picture")),
owner=Owner.from_dict(obj.get("owner", {})), owner=Owner.from_dict(obj.get("owner", {})),
snapshot_id=_str(obj.get("snapshot_id")), snapshot_id=_str(obj.get("snapshot_id")),
length=_int(obj.get("length")),
tracks=TracksPage.from_dict(obj.get("tracks", {})), tracks=TracksPage.from_dict(obj.get("tracks", {})),
type=_str(obj.get("type")) or "playlist", type=_str(obj.get("type")) or "playlist",
) )
@@ -198,9 +195,10 @@ class Playlist:
"name": self.name, "name": self.name,
"description": self.description, "description": self.description,
"collaborative": self.collaborative, "collaborative": self.collaborative,
"images": [im.to_dict() for im in (self.images or [])], "picture": self.picture,
"owner": self.owner.to_dict(), "owner": self.owner.to_dict(),
"snapshot_id": self.snapshot_id, "snapshot_id": self.snapshot_id,
"length": self.length,
"tracks": self.tracks.to_dict(), "tracks": self.tracks.to_dict(),
"type": self.type, "type": self.type,
} }