fix: artist frontend rendering
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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%" }} />
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user