diff --git a/routes/content/playlist.py b/routes/content/playlist.py index 6b88f84..1d35d77 100755 --- a/routes/content/playlist.py +++ b/routes/content/playlist.py @@ -205,6 +205,9 @@ async def get_playlist_info( playlist_info = get_playlist(client, spotify_id, expand_items=False) finally: pass + # Ensure id field is present (librespot sometimes omits it) + if playlist_info and "id" not in playlist_info: + playlist_info["id"] = spotify_id return JSONResponse(content=playlist_info, status_code=200) except Exception as e: diff --git a/spotizerr-ui/src/routes/artist.tsx b/spotizerr-ui/src/routes/artist.tsx index 0c5d496..3dc3aa3 100644 --- a/spotizerr-ui/src/routes/artist.tsx +++ b/spotizerr-ui/src/routes/artist.tsx @@ -2,26 +2,15 @@ import { Link, useParams } from "@tanstack/react-router"; import { useEffect, useState, useContext, useRef, useCallback } from "react"; import { toast } from "sonner"; import apiClient from "../lib/api-client"; -import type { LibrespotAlbumType, LibrespotArtistType, LibrespotTrackType, LibrespotImage } from "@/types/librespot"; +import type { LibrespotAlbumType, LibrespotArtistType, LibrespotTrackType } from "@/types/librespot"; import { QueueContext, getStatus } from "../contexts/queue-context"; import { useSettings } from "../contexts/settings-context"; import { FaArrowLeft, FaBookmark, FaRegBookmark, FaDownload } from "react-icons/fa"; import { AlbumCard } from "../components/AlbumCard"; -// Narrow type for the artist info response additions -type ArtistInfoResponse = LibrespotArtistType & { - biography?: Array<{ text?: string; portrait_group?: { image?: LibrespotImage[] } }>; - portrait_group?: { image?: LibrespotImage[] }; - top_track?: Array<{ country: string; track: string[] }>; - album_group?: string[]; - single_group?: string[]; - compilation_group?: string[]; - appears_on_group?: string[]; -}; - export const Artist = () => { const { artistId } = useParams({ from: "/artist/$artistId" }); - const [artist, setArtist] = useState(null); + const [artist, setArtist] = useState(null); const [artistAlbums, setArtistAlbums] = useState([]); const [artistSingles, setArtistSingles] = useState([]); const [artistCompilations, setArtistCompilations] = useState([]); @@ -94,8 +83,8 @@ export const Artist = () => { setBannerUrl(null); // reset hero; will lazy-load below try { - const resp = await apiClient.get(`/artist/info?id=${artistId}`); - const data: ArtistInfoResponse = resp.data; + const resp = await apiClient.get(`/artist/info?id=${artistId}`); + const data: LibrespotArtistType = resp.data; if (cancelled) return; @@ -104,10 +93,10 @@ export const Artist = () => { setArtist(data); // Lazy-load banner image after render - const bioEntry = Array.isArray(data.biography) && data.biography.length > 0 ? data.biography[0] : undefined; - const portraitImages = data.portrait_group?.image ?? bioEntry?.portrait_group?.image ?? []; - const allImages = [...(portraitImages ?? []), ...((data.images as LibrespotImage[] | undefined) ?? [])]; - const candidateBanner = allImages.sort((a, b) => (b?.width ?? 0) - (a?.width ?? 0))[0]?.url || "/placeholder.jpg"; + const allImages = [...(data.portrait_group ?? []), ...(data.biography?.portrait_group ?? [])]; + const candidateBanner = allImages + .filter(img => img && typeof img === 'object' && 'url' in img) + .sort((a, b) => (b.width ?? 0) - (a.width ?? 0))[0]?.url || "/placeholder.jpg"; // Use async preload to avoid blocking initial paint setTimeout(() => { const img = new Image(); diff --git a/spotizerr-ui/src/routes/watchlist.tsx b/spotizerr-ui/src/routes/watchlist.tsx index 88ab4ff..4cede24 100644 --- a/spotizerr-ui/src/routes/watchlist.tsx +++ b/spotizerr-ui/src/routes/watchlist.tsx @@ -3,7 +3,7 @@ import apiClient from "../lib/api-client"; import { toast } from "sonner"; import { useSettings } from "../contexts/settings-context"; import { Link } from "@tanstack/react-router"; -import type { ArtistType, PlaylistType } from "../types/spotify"; +import type { LibrespotArtistType, LibrespotPlaylistType } from "../types/librespot"; import { FaRegTrashAlt, FaSearch } from "react-icons/fa"; // --- Type Definitions --- @@ -11,8 +11,8 @@ interface BaseWatched { itemType: "artist" | "playlist"; spotify_id: string; } -type WatchedArtist = ArtistType & { itemType: "artist" }; -type WatchedPlaylist = PlaylistType & { itemType: "playlist" }; +type WatchedArtist = LibrespotArtistType & { itemType: "artist" }; +type WatchedPlaylist = LibrespotPlaylistType & { itemType: "playlist" }; type WatchedItem = WatchedArtist | WatchedPlaylist; @@ -62,9 +62,9 @@ export const Watchlist = () => { }; // Fetch artist details in batches - await batchFetch( + await batchFetch( artistIds, - (id) => apiClient.get(`/artist/info?id=${id}`).then(res => res.data), + (id) => apiClient.get(`/artist/info?id=${id}`).then(res => res.data), 5, // batch size (results) => { const items: WatchedArtist[] = results.map((data) => ({ @@ -76,9 +76,9 @@ export const Watchlist = () => { ); // Fetch playlist details in batches - await batchFetch( + await batchFetch( playlistIds, - (id) => apiClient.get(`/playlist/info?id=${id}`).then(res => res.data), + (id) => apiClient.get(`/playlist/info?id=${id}`).then(res => res.data), 5, // batch size (results) => { const items: WatchedPlaylist[] = results.map((data) => ({ @@ -170,11 +170,15 @@ export const Watchlist = () => {
- {items.map((item) => ( + {items.map((item) => (
{item.name} diff --git a/spotizerr-ui/src/types/librespot.ts b/spotizerr-ui/src/types/librespot.ts index 57704b9..b94f299 100644 --- a/spotizerr-ui/src/types/librespot.ts +++ b/spotizerr-ui/src/types/librespot.ts @@ -6,8 +6,8 @@ export interface LibrespotExternalUrls { export interface LibrespotImage { url: string; - width?: number; - height?: number; + width: number; + height: number; } export interface LibrespotArtistStub { @@ -18,18 +18,23 @@ export interface LibrespotArtistStub { external_urls?: LibrespotExternalUrls; } +export interface LibrespotBiographyType { + text: string; + portrait_group: LibrespotImage[]; +} + +export interface LibrespotTopTrackType { + country: string; + track: string[]; +} // Full artist object (get_artist) export interface LibrespotArtistType { id: string; name: string; - images?: LibrespotImage[]; - external_urls?: LibrespotExternalUrls; - followers?: { total: number }; - genres?: string[]; - popularity?: number; - type?: "artist"; - uri?: string; - // Album groups: arrays of album IDs + top_track: LibrespotTopTrackType[]; + portrait_group: LibrespotImage[]; + popularity: number; + biography: LibrespotBiographyType; album_group?: string[]; single_group?: string[]; compilation_group?: string[]; @@ -64,24 +69,23 @@ export interface LibrespotTrackType { disc_number: number; duration_ms: number; explicit: boolean; - external_ids?: { isrc?: string }; + external_ids: { isrc?: string }; external_urls: LibrespotExternalUrls; id: string; name: string; - popularity?: number; + popularity: number; track_number: number; type: "track"; uri: string; - preview_url?: string; - has_lyrics?: boolean; - earliest_live_timestamp?: number; - licensor_uuid?: string; // when available + preview_url: string; + has_lyrics: boolean; + earliest_live_timestamp: number; + licensor_uuid: string; // when available } export interface LibrespotAlbumType { album_type: "album" | "single" | "compilation"; total_tracks: number; - available_markets?: string[]; external_urls: LibrespotExternalUrls; id: string; images: LibrespotImage[]; @@ -96,8 +100,8 @@ export interface LibrespotAlbumType { tracks: string[] | LibrespotTrackType[]; copyrights?: LibrespotCopyright[]; external_ids?: { upc?: string }; - label?: string; - popularity?: number; + label: string; + popularity: number; } // Playlist types @@ -135,14 +139,14 @@ export interface LibrespotPlaylistTracksPageType { export interface LibrespotPlaylistType { name: string; - description?: string | null; - collaborative?: boolean; - images?: Array & Partial>; + id: string; + description: string | null; + collaborative: boolean; owner: LibrespotPlaylistOwnerType; snapshot_id: string; tracks: LibrespotPlaylistTracksPageType; type: "playlist"; - picture?: string; + picture: string; } // Type guards