"use client"; import { useCallback, useEffect, useState } from "react"; import { PageHead } from "../_ui/page-head"; import { SectionHeader } from "../_ui/section-header"; import { ArtistModal } from "../_ui/artist-modal"; import { AlbumModal } from "../_ui/album-modal"; import { CoverArt } from "../_ui/cover-art"; import { toast } from "../_ui/toast"; type Suggestion = { id: string; artistMbid: string; artistName: string; rgMbid: string | null; album: string | null; score: number; seedCount: number; sources: string[]; }; type Feed = { artists: Suggestion[]; albums: Suggestion[] }; type SearchResult = { seed: { mbid: string; name: string } | null; similar: { mbid: string; name: string; score: number }[]; }; export function DiscoverClient() { const [feed, setFeed] = useState({ artists: [], albums: [] }); const [result, setResult] = useState(null); const [running, setRunning] = useState(false); const [query, setQuery] = useState(""); const [search, setSearch] = useState(null); const [busy, setBusy] = useState(false); const [followedSimilar, setFollowedSimilar] = useState>(new Set()); const [artistModal, setArtistModal] = useState<{ mbid: string; name: string } | null>(null); const [albumModal, setAlbumModal] = useState(null); const loadFeed = useCallback(async () => { setFeed(await (await fetch("/api/discover")).json()); }, []); useEffect(() => { loadFeed(); fetch("/api/discover/run") .then((r) => r.json()) .then((s: { result: string | null; requested: boolean }) => { setResult(s.result); setRunning(s.requested); }); }, [loadFeed]); useEffect(() => { if (!running) return; const id = setInterval(async () => { const s = await (await fetch("/api/discover/run")).json(); setResult(s.result); if (!s.requested) { setRunning(false); loadFeed(); } }, 2000); return () => clearInterval(id); }, [running, loadFeed]); async function discoverNow() { await fetch("/api/discover/run", { method: "POST" }); setRunning(true); } async function runSearch(e: React.FormEvent) { e.preventDefault(); if (!query.trim()) return; setBusy(true); try { setSearch(await (await fetch(`/api/discover/search?artist=${encodeURIComponent(query.trim())}`)).json()); } finally { setBusy(false); } } async function followSimilar(a: { mbid: string; name: string }) { const res = await fetch("/api/artists", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ mbid: a.mbid, name: a.name }), }); if (res.ok || res.status === 409) { setFollowedSimilar((s) => new Set(s).add(a.mbid)); toast(`Following ${a.name}`); } } async function act(id: string, action: "follow" | "want" | "dismiss") { await fetch(`/api/discover/${id}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ action }), }); toast(action === "follow" ? "Following" : action === "want" ? "Added to wanted" : "Dismissed"); loadFeed(); } return (
{result ? last run · {result} : null}
{search && !search.seed ?

No MusicBrainz match.

: null} {search?.seed ? (
    {search.similar.map((s) => (
  • similarity {s.score.toFixed(1)}
    {followedSimilar.has(s.mbid) ? ( Following ) : ( )}
  • ))}
) : null} {feed.artists.length === 0 ? (

No suggestions yet — run Discover, or follow a few artists to seed it.

) : (
    {feed.artists.map((s) => (
  • score {s.score.toFixed(2)} · {s.seedCount} seed{s.seedCount === 1 ? "" : "s"} · {s.sources.join(", ")}
  • ))}
)} {feed.albums.length === 0 ? (

No album suggestions yet.

) : (
    {feed.albums.map((s) => (
  • ))}
)} {artistModal ? ( setArtistModal(null)} mbid={artistModal.mbid} initialName={artistModal.name} /> ) : null} {albumModal ? ( setAlbumModal(null)} album={{ rgMbid: albumModal.rgMbid, album: albumModal.album ?? "", artist: albumModal.artistName, year: null, type: null }} actions={ } /> ) : null}
); }