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
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user