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
uvicorn[standard]==0.35.0
celery==5.5.3
deezspot-spotizerr==3.1.0
deezspot-spotizerr==3.1.2
httpx==0.28.1
bcrypt==4.2.1
PyJWT==2.10.1

View File

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

View File

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