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 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
def _split_countries(countries: str) -> List[str]:
if not countries:
@@ -531,10 +548,12 @@ class LibrespotClient:
picture_bytes = getattr(attrs, "picture", b"") if attrs else b""
images: List[Dict[str, Any]] = []
if isinstance(picture_bytes, (bytes, bytearray)) and len(picture_bytes) in (16, 20):
url = self._image_url_from_file_id(picture_bytes)
if url:
images.append({"url": url, "width": 0, "height": 0})
picture_url: Optional[str] = None
# Derive picture URL from attributes.picture with header-aware parsing
pic_url = self._get_playlist_picture_url(attrs)
if pic_url:
picture_url = pic_url
images.append({"url": pic_url, "width": 0, "height": 0})
owner_username = getattr(p, "owner_username", "") or ""
@@ -542,14 +561,15 @@ class LibrespotClient:
contents = getattr(p, "contents", None)
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] = []
if contents is not None:
for it in getattr(contents, "items", []):
uri = getattr(it, "uri", "") or ""
if uri.startswith("spotify:track:"):
b62 = uri.split(":")[-1]
to_fetch.append(b62)
if to_fetch:
if to_fetch and self._session is not None:
fetched_tracks = self._fetch_track_objects(to_fetch)
if contents is not None:
@@ -609,6 +629,22 @@ class LibrespotClient:
"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""
snapshot_b64 = base64.b64encode(rev_bytes).decode("ascii") if rev_bytes else None
@@ -616,7 +652,7 @@ class LibrespotClient:
"name": name or None,
"description": description or None,
"collaborative": collaborative or None,
"images": images or None,
"picture": picture_url or None,
"owner": self._prune_empty({
"id": owner_username,
"type": "user",
@@ -625,6 +661,7 @@ class LibrespotClient:
"display_name": owner_username or None,
}),
"snapshot_id": snapshot_b64,
"length": length_seconds,
"tracks": tracks_obj,
"type": "playlist",
}

View File

@@ -3,7 +3,7 @@
from dataclasses import dataclass, field
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
@@ -167,9 +167,10 @@ class Playlist:
name: Optional[str] = None
description: Optional[str] = None
collaborative: Optional[bool] = None
images: Optional[List[Image]] = None
picture: Optional[str] = None
owner: Owner = field(default_factory=Owner)
snapshot_id: Optional[str] = None
length: Optional[int] = None
tracks: TracksPage = field(default_factory=lambda: TracksPage(items=[]))
type: str = "playlist"
@@ -177,18 +178,14 @@ class Playlist:
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,
picture=_str(obj.get("picture")),
owner=Owner.from_dict(obj.get("owner", {})),
snapshot_id=_str(obj.get("snapshot_id")),
length=_int(obj.get("length")),
tracks=TracksPage.from_dict(obj.get("tracks", {})),
type=_str(obj.get("type")) or "playlist",
)
@@ -198,9 +195,10 @@ class Playlist:
"name": self.name,
"description": self.description,
"collaborative": self.collaborative,
"images": [im.to_dict() for im in (self.images or [])],
"picture": self.picture,
"owner": self.owner.to_dict(),
"snapshot_id": self.snapshot_id,
"length": self.length,
"tracks": self.tracks.to_dict(),
"type": self.type,
}