Files
R3ap3r 3178b8f4f7 Aurora SpotiRip desktop and web app
- Spotify playlist, library, album and artist browsing with in-app playback
- Song, playlist and album downloads via spotdl with YouTube and Bandcamp fallbacks
- Electron desktop app (castLabs Widevine) with a permanent sidebar layout
- Linux and Windows packaging scripts
2026-09-25 23:20:57 -05:00

146 lines
5.0 KiB
TypeScript

import { useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
import { Album, ArrowLeft, ExternalLink, LoaderCircle } from "lucide-react";
import { Button } from "../components/Button";
import { DownloadButton } from "../components/DownloadButton";
import { useSpotifyDownload } from "../helpers/useSpotifyDownload";
import {
getSpotifyArtistAlbums,
type SpotifyArtistAlbum,
type OutputType as ArtistAlbumsResult,
} from "../endpoints/spotify-artist-albums_GET.schema";
import styles from "./artist.$id.module.css";
import { MobileNav } from "../components/MobileNav";
export default function ArtistDiscographyPage() {
const params = useParams();
const artistId = params.id || "";
const [data, setData] = useState<ArtistAlbumsResult>();
const downloads = useSpotifyDownload();
const [albums, setAlbums] = useState<SpotifyArtistAlbum[]>([]);
const [nextOffset, setNextOffset] = useState<number>();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string>();
useEffect(() => {
if (!artistId) return;
void load(0, false);
}, [artistId]);
async function load(offset: number, append: boolean) {
if (!artistId) return;
setLoading(true);
setError(undefined);
try {
const result = await getSpotifyArtistAlbums(artistId, offset);
setData(result);
setAlbums((current) => append ? [...current, ...result.albums] : result.albums);
setNextOffset(result.nextOffset);
} catch (reason) {
setError(reason instanceof Error ? reason.message : "Unable to load artist discography.");
} finally {
setLoading(false);
}
}
return (
<main className={styles.page}>
<header className={styles.topbar}>
<Link to="/spotify-library" className={styles.backLink}>
<ArrowLeft size={16} /> My Spotify
</Link>
<span>Artist Discography</span>
</header>
{error ? <div className={styles.noticeError}>{error}</div> : null}
<section className={styles.artistHeader}>
<div className={styles.artistImage}>
{data?.artist.imageUrl ? (
<img src={data.artist.imageUrl} alt="" />
) : (
<Album size={34} />
)}
</div>
<div>
<span>SPOTIFY ARTIST</span>
<h1>{data?.artist.name || "Artist discography"}</h1>
<p>
{typeof data?.total === "number"
? data.total + " releases returned by Spotify"
: "Albums, singles and compilations"}
</p>
{data?.artist.externalUrl ? (
<a
className={styles.spotifyLink}
href={data.artist.externalUrl}
target="_blank"
rel="noreferrer"
>
Spotify <ExternalLink size={13} /> Open artist
</a>
) : null}
</div>
</section>
<section className={styles.grid}>
{albums.map((album) => (
<article className={styles.card} key={album.id}>
<div className={styles.cover}>
{album.imageUrl ? <img src={album.imageUrl} alt="" /> : <Album size={30} />}
</div>
<div className={styles.cardBody}>
<span className={styles.type}>{(album.albumType || "release").toUpperCase()}</span>
<strong>{album.name}</strong>
<small>
{album.releaseDate || "Date unknown"}
{album.totalTracks ? " · " + album.totalTracks + " tracks" : ""}
</small>
</div>
<div className={styles.cardActions}>
<Button size="sm" asChild>
<Link to={"/album/" + album.id}>Browse album</Link>
</Button>
{album.externalUrl ? (
<DownloadButton
downloads={downloads}
busyKey={album.externalUrl}
label={`“${album.name}”`}
request={{
kind: "playlist",
urls: [album.externalUrl],
name: data?.artist.name ? `${data.artist.name} - ${album.name}` : album.name,
}}
text="Download"
size="sm"
variant="outline"
/>
) : null}
{album.externalUrl ? (
<Button variant="outline" size="sm" asChild>
<a href={album.externalUrl} target="_blank" rel="noreferrer">
Open Spotify <ExternalLink size={13} />
</a>
</Button>
) : null}
</div>
</article>
))}
</section>
{loading ? (
<div className={styles.loading}>
<LoaderCircle size={18} /> Loading releases…
</div>
) : null}
{!loading && nextOffset !== undefined ? (
<div className={styles.loadMore}>
<Button variant="outline" onClick={() => load(nextOffset, true)}>Load more releases</Button>
</div>
) : null}
<MobileNav />
</main>
);
}