fix: artist frontend rendering

This commit is contained in:
Xoconoch
2025-08-28 07:16:05 -06:00
parent 84b93f900e
commit 4476d39d39
4 changed files with 95 additions and 38 deletions

0
log.txt Normal file
View File

View File

@@ -1,7 +1,7 @@
fastapi==0.116.1 fastapi==0.116.1
uvicorn[standard]==0.35.0 uvicorn[standard]==0.35.0
celery==5.5.3 celery==5.5.3
deezspot-spotizerr==3.1.0 deezspot-spotizerr==3.1.2
httpx==0.28.1 httpx==0.28.1
bcrypt==4.2.1 bcrypt==4.2.1
PyJWT==2.10.1 PyJWT==2.10.1

View File

@@ -15,6 +15,7 @@ type ArtistInfoResponse = LibrespotArtistType & {
top_track?: Array<{ country: string; track: string[] }>; top_track?: Array<{ country: string; track: string[] }>;
album_group?: string[]; album_group?: string[];
single_group?: string[]; single_group?: string[];
compilation_group?: string[];
appears_on_group?: string[]; appears_on_group?: string[];
}; };
@@ -23,6 +24,7 @@ export const Artist = () => {
const [artist, setArtist] = useState<ArtistInfoResponse | null>(null); const [artist, setArtist] = useState<ArtistInfoResponse | null>(null);
const [artistAlbums, setArtistAlbums] = useState<LibrespotAlbumType[]>([]); const [artistAlbums, setArtistAlbums] = useState<LibrespotAlbumType[]>([]);
const [artistSingles, setArtistSingles] = useState<LibrespotAlbumType[]>([]); const [artistSingles, setArtistSingles] = useState<LibrespotAlbumType[]>([]);
const [artistCompilations, setArtistCompilations] = useState<LibrespotAlbumType[]>([]);
const [artistAppearsOn, setArtistAppearsOn] = useState<LibrespotAlbumType[]>([]); const [artistAppearsOn, setArtistAppearsOn] = useState<LibrespotAlbumType[]>([]);
const [topTracks, setTopTracks] = useState<LibrespotTrackType[]>([]); const [topTracks, setTopTracks] = useState<LibrespotTrackType[]>([]);
const [bannerUrl, setBannerUrl] = useState<string | null>(null); const [bannerUrl, setBannerUrl] = useState<string | null>(null);
@@ -38,6 +40,7 @@ export const Artist = () => {
const ALBUM_BATCH = 12; const ALBUM_BATCH = 12;
const [albumOffset, setAlbumOffset] = useState<number>(0); const [albumOffset, setAlbumOffset] = useState<number>(0);
const [singleOffset, setSingleOffset] = useState<number>(0); const [singleOffset, setSingleOffset] = useState<number>(0);
const [compOffset, setCompOffset] = useState<number>(0);
const [appearsOffset, setAppearsOffset] = useState<number>(0); const [appearsOffset, setAppearsOffset] = useState<number>(0);
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [loadingMore, setLoadingMore] = useState<boolean>(false); const [loadingMore, setLoadingMore] = useState<boolean>(false);
@@ -81,9 +84,11 @@ export const Artist = () => {
setError(null); setError(null);
setArtistAlbums([]); setArtistAlbums([]);
setArtistSingles([]); setArtistSingles([]);
setArtistCompilations([]);
setArtistAppearsOn([]); setArtistAppearsOn([]);
setAlbumOffset(0); setAlbumOffset(0);
setSingleOffset(0); setSingleOffset(0);
setCompOffset(0);
setAppearsOffset(0); setAppearsOffset(0);
setHasMore(true); setHasMore(true);
setBannerUrl(null); // reset hero; will lazy-load below setBannerUrl(null); // reset hero; will lazy-load below
@@ -123,46 +128,61 @@ export const Artist = () => {
if (!cancelled) setTopTracks([]); if (!cancelled) setTopTracks([]);
} }
// Progressive album loading: album -> single -> appears_on // Progressive album loading: album -> single -> compilation -> appears_on
const albumIds = data.album_group ?? []; const albumIds = data.album_group ?? [];
const singleIds = data.single_group ?? []; const singleIds = data.single_group ?? [];
const compIds = data.compilation_group ?? [];
const appearsIds = data.appears_on_group ?? []; const appearsIds = data.appears_on_group ?? [];
// Determine initial number based on screen size: 4 on small screens // Determine initial number based on screen size: 4 on small screens
const isSmallScreen = typeof window !== "undefined" && !window.matchMedia("(min-width: 640px)").matches; const isSmallScreen = typeof window !== "undefined" && !window.matchMedia("(min-width: 640px)").matches;
const initialTarget = isSmallScreen ? 4 : ALBUM_BATCH; const initialTarget = isSmallScreen ? 4 : ALBUM_BATCH;
// Load initial batch from albumIds, then if needed from singles, then appears // Load initial sets from each group in order until initialTarget reached
const initialBatch: LibrespotAlbumType[] = []; let aOff = 0, sOff = 0, cOff = 0, apOff = 0;
let aOff = 0, sOff = 0, apOff = 0; let loaded = 0;
if (albumIds.length > 0) { let aList: LibrespotAlbumType[] = [];
const take = albumIds.slice(0, initialTarget); let sList: LibrespotAlbumType[] = [];
initialBatch.push(...await fetchAlbumsByIds(take)); let cList: LibrespotAlbumType[] = [];
let apList: LibrespotAlbumType[] = [];
if (albumIds.length > 0 && loaded < initialTarget) {
const take = albumIds.slice(0, initialTarget - loaded);
aList = await fetchAlbumsByIds(take);
aOff = take.length; aOff = take.length;
loaded += aList.length;
} }
if (initialBatch.length < initialTarget && singleIds.length > 0) { if (singleIds.length > 0 && loaded < initialTarget) {
const remaining = initialTarget - initialBatch.length; const take = singleIds.slice(0, initialTarget - loaded);
const take = singleIds.slice(0, remaining); sList = await fetchAlbumsByIds(take);
initialBatch.push(...await fetchAlbumsByIds(take));
sOff = take.length; sOff = take.length;
loaded += sList.length;
} }
if (initialBatch.length < initialTarget && appearsIds.length > 0) { if (compIds.length > 0 && loaded < initialTarget) {
const remaining = initialTarget - initialBatch.length; const take = compIds.slice(0, initialTarget - loaded);
const take = appearsIds.slice(0, remaining); cList = await fetchAlbumsByIds(take);
initialBatch.push(...await fetchAlbumsByIds(take)); cOff = take.length;
loaded += cList.length;
}
if (appearsIds.length > 0 && loaded < initialTarget) {
const take = appearsIds.slice(0, initialTarget - loaded);
apList = await fetchAlbumsByIds(take);
apOff = take.length; apOff = take.length;
loaded += apList.length;
} }
if (!cancelled) { if (!cancelled) {
setArtistAlbums(initialBatch.filter(a => a.album_type === "album")); setArtistAlbums(aList);
setArtistSingles(initialBatch.filter(a => a.album_type === "single")); setArtistSingles(sList);
setArtistAppearsOn([]); // placeholder; appears_on grouping not explicitly typed setArtistCompilations(cList);
setArtistAppearsOn(apList);
// Store offsets for next loads // Store offsets for next loads
setAlbumOffset(aOff); setAlbumOffset(aOff);
setSingleOffset(sOff); setSingleOffset(sOff);
setCompOffset(cOff);
setAppearsOffset(apOff); setAppearsOffset(apOff);
// Determine if more remain // Determine if more remain
setHasMore((albumIds.length > aOff) || (singleIds.length > sOff) || (appearsIds.length > apOff)); setHasMore((albumIds.length > aOff) || (singleIds.length > sOff) || (compIds.length > cOff) || (appearsIds.length > apOff));
} }
} else { } else {
setError("Could not load artist data."); setError("Could not load artist data.");
@@ -201,34 +221,54 @@ export const Artist = () => {
try { try {
const albumIds = artist.album_group ?? []; const albumIds = artist.album_group ?? [];
const singleIds = artist.single_group ?? []; const singleIds = artist.single_group ?? [];
const compIds = artist.compilation_group ?? [];
const appearsIds = artist.appears_on_group ?? []; const appearsIds = artist.appears_on_group ?? [];
const nextBatch: LibrespotAlbumType[] = []; const nextA: LibrespotAlbumType[] = [];
let aOff = albumOffset, sOff = singleOffset, apOff = appearsOffset; const nextS: LibrespotAlbumType[] = [];
if (aOff < albumIds.length) { const nextC: LibrespotAlbumType[] = [];
const take = albumIds.slice(aOff, aOff + ALBUM_BATCH - nextBatch.length); const nextAp: LibrespotAlbumType[] = [];
nextBatch.push(...await fetchAlbumsByIds(take));
let aOff = albumOffset, sOff = singleOffset, cOff = compOffset, apOff = appearsOffset;
const totalLoaded = () => nextA.length + nextS.length + nextC.length + nextAp.length;
if (aOff < albumIds.length && totalLoaded() < ALBUM_BATCH) {
const remaining = ALBUM_BATCH - totalLoaded();
const take = albumIds.slice(aOff, aOff + remaining);
nextA.push(...await fetchAlbumsByIds(take));
aOff += take.length; aOff += take.length;
} }
if (nextBatch.length < ALBUM_BATCH && sOff < singleIds.length) { if (sOff < singleIds.length && totalLoaded() < ALBUM_BATCH) {
const remaining = ALBUM_BATCH - nextBatch.length; const remaining = ALBUM_BATCH - totalLoaded();
const take = singleIds.slice(sOff, sOff + remaining); const take = singleIds.slice(sOff, sOff + remaining);
nextBatch.push(...await fetchAlbumsByIds(take)); nextS.push(...await fetchAlbumsByIds(take));
sOff += take.length; sOff += take.length;
} }
if (nextBatch.length < ALBUM_BATCH && apOff < appearsIds.length) { if (cOff < compIds.length && totalLoaded() < ALBUM_BATCH) {
const remaining = ALBUM_BATCH - nextBatch.length; const remaining = ALBUM_BATCH - totalLoaded();
const take = compIds.slice(cOff, cOff + remaining);
nextC.push(...await fetchAlbumsByIds(take));
cOff += take.length;
}
if (apOff < appearsIds.length && totalLoaded() < ALBUM_BATCH) {
const remaining = ALBUM_BATCH - totalLoaded();
const take = appearsIds.slice(apOff, apOff + remaining); const take = appearsIds.slice(apOff, apOff + remaining);
nextBatch.push(...await fetchAlbumsByIds(take)); nextAp.push(...await fetchAlbumsByIds(take));
apOff += take.length; apOff += take.length;
} }
setArtistAlbums((cur) => cur.concat(nextBatch.filter(a => a.album_type === "album"))); setArtistAlbums((cur) => cur.concat(nextA));
setArtistSingles((cur) => cur.concat(nextBatch.filter(a => a.album_type === "single"))); setArtistSingles((cur) => cur.concat(nextS));
setAppearsOffset(apOff); setArtistCompilations((cur) => cur.concat(nextC));
setArtistAppearsOn((cur) => cur.concat(nextAp));
setAlbumOffset(aOff); setAlbumOffset(aOff);
setSingleOffset(sOff); setSingleOffset(sOff);
setHasMore((albumIds.length > aOff) || (singleIds.length > sOff) || (appearsIds.length > apOff)); setCompOffset(cOff);
setAppearsOffset(apOff);
setHasMore((albumIds.length > aOff) || (singleIds.length > sOff) || (compIds.length > cOff) || (appearsIds.length > apOff));
} catch (err) { } catch (err) {
console.error("Failed to load more albums", err); console.error("Failed to load more albums", err);
toast.error("Failed to load more albums"); toast.error("Failed to load more albums");
@@ -236,7 +276,7 @@ export const Artist = () => {
} finally { } finally {
setLoadingMore(false); setLoadingMore(false);
} }
}, [artistId, loadingMore, loading, hasMore, artist, albumOffset, singleOffset, appearsOffset, fetchAlbumsByIds]); }, [artistId, loadingMore, loading, hasMore, artist, albumOffset, singleOffset, compOffset, appearsOffset, fetchAlbumsByIds]);
// IntersectionObserver to trigger fetchMoreAlbums when sentinel is visible // IntersectionObserver to trigger fetchMoreAlbums when sentinel is visible
useEffect(() => { useEffect(() => {
@@ -474,6 +514,18 @@ export const Artist = () => {
</div> </div>
)} )}
{/* Compilations */}
{artistCompilations.length > 0 && (
<div className="mb-12">
<h2 className="text-3xl font-bold mb-6 text-content-primary dark:text-content-primary-dark">Compilations</h2>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
{artistCompilations.map((album) => (
<AlbumCard key={album.id} album={album} onDownload={() => handleDownloadAlbum(album)} />
))}
</div>
</div>
)}
{/* Appears On */} {/* Appears On */}
{artistAppearsOn.length > 0 && ( {artistAppearsOn.length > 0 && (
<div className="mb-12"> <div className="mb-12">
@@ -494,9 +546,9 @@ export const Artist = () => {
{hasMore && !loadingMore && ( {hasMore && !loadingMore && (
<button <button
onClick={() => fetchMoreAlbums()} onClick={() => fetchMoreAlbums()}
className="px-4 py-2 mb-6 rounded bg-surface-muted hover:bg-surface-muted-dark" className="px-4 py-2 mb-6 rounded"
> >
Load more Loading...
</button> </button>
)} )}
<div ref={sentinelRef} style={{ height: 1, width: "100%" }} /> <div ref={sentinelRef} style={{ height: 1, width: "100%" }} />

View File

@@ -29,6 +29,11 @@ export interface LibrespotArtistType {
popularity?: number; popularity?: number;
type?: "artist"; type?: "artist";
uri?: string; uri?: string;
// Album groups: arrays of album IDs
album_group?: string[];
single_group?: string[];
compilation_group?: string[];
appears_on_group?: string[];
} }
export interface LibrespotCopyright { export interface LibrespotCopyright {