fix: images and id not loading for playlists in watchlist
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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<ArtistInfoResponse | null>(null);
|
||||
const [artist, setArtist] = useState<LibrespotArtistType | null>(null);
|
||||
const [artistAlbums, setArtistAlbums] = useState<LibrespotAlbumType[]>([]);
|
||||
const [artistSingles, setArtistSingles] = useState<LibrespotAlbumType[]>([]);
|
||||
const [artistCompilations, setArtistCompilations] = useState<LibrespotAlbumType[]>([]);
|
||||
@@ -94,8 +83,8 @@ export const Artist = () => {
|
||||
setBannerUrl(null); // reset hero; will lazy-load below
|
||||
|
||||
try {
|
||||
const resp = await apiClient.get<ArtistInfoResponse>(`/artist/info?id=${artistId}`);
|
||||
const data: ArtistInfoResponse = resp.data;
|
||||
const resp = await apiClient.get<LibrespotArtistType>(`/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();
|
||||
|
||||
@@ -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<ArtistType>(
|
||||
await batchFetch<LibrespotArtistType>(
|
||||
artistIds,
|
||||
(id) => apiClient.get<ArtistType>(`/artist/info?id=${id}`).then(res => res.data),
|
||||
(id) => apiClient.get<LibrespotArtistType>(`/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<PlaylistType>(
|
||||
await batchFetch<LibrespotPlaylistType>(
|
||||
playlistIds,
|
||||
(id) => apiClient.get<PlaylistType>(`/playlist/info?id=${id}`).then(res => res.data),
|
||||
(id) => apiClient.get<LibrespotPlaylistType>(`/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 = () => {
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
|
||||
{items.map((item) => (
|
||||
{items.map((item) => (
|
||||
<div key={item.id} className="bg-surface dark:bg-surface-secondary-dark p-4 rounded-lg shadow space-y-2 flex flex-col">
|
||||
<a href={`/${item.itemType}/${item.id}`} className="flex-grow">
|
||||
<img
|
||||
src={item.images?.[0]?.url || "/images/placeholder.jpg"}
|
||||
src={
|
||||
item.itemType === "artist"
|
||||
? (item as WatchedArtist).portrait_group[0]?.url || "/images/placeholder.jpg"
|
||||
: (item as WatchedPlaylist).picture || "/images/placeholder.jpg"
|
||||
}
|
||||
alt={item.name}
|
||||
className="w-full h-auto object-cover rounded-md aspect-square"
|
||||
/>
|
||||
|
||||
@@ -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<Pick<LibrespotImage, "url"> & Partial<LibrespotImage>>;
|
||||
id: string;
|
||||
description: string | null;
|
||||
collaborative: boolean;
|
||||
owner: LibrespotPlaylistOwnerType;
|
||||
snapshot_id: string;
|
||||
tracks: LibrespotPlaylistTracksPageType;
|
||||
type: "playlist";
|
||||
picture?: string;
|
||||
picture: string;
|
||||
}
|
||||
|
||||
// Type guards
|
||||
|
||||
Reference in New Issue
Block a user