fix: images and id not loading for playlists in watchlist

This commit is contained in:
Xoconoch
2025-08-30 06:58:46 -06:00
parent 9e4b2fcd01
commit 5942e6ea36
4 changed files with 51 additions and 51 deletions

View File

@@ -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();

View File

@@ -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"
/>

View File

@@ -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