Files
Lyra/web/src/app/discover/artist/[mbid]/preview-client.tsx
T
2026-07-12 01:46:13 +02:00

157 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useCallback, useEffect, useState } from "react";
type Release = {
rgMbid: string;
album: string;
primaryType: string | null;
secondaryTypes: string[];
firstReleaseDate: string | null;
monitored: boolean;
have: boolean;
};
type Preview = { artistName: string; followed: boolean; releases: Release[] };
type Track = { position: number; title: string; lengthMs: number | null };
function fmt(ms: number | null): string {
if (ms == null) return "";
const s = Math.round(ms / 1000);
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
}
export function PreviewClient({ mbid, initialName }: { mbid: string; initialName: string }) {
const [data, setData] = useState<Preview | null>(null);
const [showAll, setShowAll] = useState(false);
const [followed, setFollowed] = useState(false);
const [tracks, setTracks] = useState<Record<string, Track[] | "loading" | "error">>({});
const [error, setError] = useState(false);
const load = useCallback(async () => {
setError(false);
const qs = new URLSearchParams();
if (initialName) qs.set("name", initialName);
if (showAll) qs.set("all", "true");
const res = await fetch(`/api/discover/preview/${mbid}?${qs.toString()}`);
if (!res.ok) {
setError(true);
return;
}
const d: Preview = await res.json();
setData(d);
setFollowed(d.followed);
}, [mbid, initialName, showAll]);
useEffect(() => {
load();
}, [load]);
// Scroll to the album an album-suggestion deep-linked to (#rg-<rgMbid>) once data is in.
useEffect(() => {
if (!data) return;
const hash = window.location.hash;
if (!hash) return;
document.getElementById(hash.slice(1))?.scrollIntoView();
}, [data]);
async function follow() {
const name = data?.artistName || initialName;
const res = await fetch("/api/artists", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ mbid, name }),
});
if (res.ok || res.status === 409) setFollowed(true);
}
async function want(r: Release) {
await fetch("/api/discover/want", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
rgMbid: r.rgMbid,
artistMbid: mbid,
artistName: data?.artistName || initialName,
album: r.album,
primaryType: r.primaryType,
secondaryTypes: r.secondaryTypes,
firstReleaseDate: r.firstReleaseDate,
}),
});
setData((d) =>
d ? { ...d, releases: d.releases.map((x) => (x.rgMbid === r.rgMbid ? { ...x, monitored: true } : x)) } : d,
);
}
async function toggleTracks(rgMbid: string) {
if (tracks[rgMbid]) {
setTracks((t) => {
const n = { ...t };
delete n[rgMbid];
return n;
});
return;
}
setTracks((t) => ({ ...t, [rgMbid]: "loading" }));
const res = await fetch(`/api/mb/release-groups/${rgMbid}/tracks`);
if (!res.ok) {
setTracks((t) => ({ ...t, [rgMbid]: "error" }));
return;
}
const { tracks: ts } = await res.json();
setTracks((t) => ({ ...t, [rgMbid]: ts }));
}
if (error)
return (
<p>
Couldnt load this artist. <button onClick={load}>Retry</button>
</p>
);
if (!data) return <p>Loading</p>;
return (
<div>
<h1>{data.artistName || "Artist"}</h1>
<p>
{followed ? <span>Following</span> : <button onClick={follow}>Follow</button>}
{" · "}
<a href={`https://musicbrainz.org/artist/${mbid}`} target="_blank" rel="noreferrer">
MusicBrainz
</a>
</p>
<label>
<input type="checkbox" checked={showAll} onChange={(e) => setShowAll(e.target.checked)} /> Show all release
types
</label>
<ul>
{data.releases.map((r) => (
<li key={r.rgMbid} id={`rg-${r.rgMbid}`}>
<strong>{r.album}</strong>{" "}
<small>
{r.primaryType}
{r.firstReleaseDate ? ` · ${r.firstReleaseDate.slice(0, 4)}` : ""}
</small>{" "}
{r.have ? <em>In library</em> : r.monitored ? <em>Monitored</em> : null}{" "}
<button onClick={() => want(r)} disabled={r.have || r.monitored}>
Want
</button>{" "}
<button onClick={() => toggleTracks(r.rgMbid)}>{tracks[r.rgMbid] ? "Hide tracks" : "Tracks"}</button>
{tracks[r.rgMbid] === "loading" && <p>Loading tracks</p>}
{tracks[r.rgMbid] === "error" && <p>Couldnt load tracks.</p>}
{Array.isArray(tracks[r.rgMbid]) && (
<ol>
{(tracks[r.rgMbid] as Track[]).map((t) => (
<li key={t.position}>
{t.title} {t.lengthMs != null && <small>({fmt(t.lengthMs)})</small>}
</li>
))}
</ol>
)}
</li>
))}
</ul>
</div>
);
}