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