import { prisma } from "@/lib/db"; // One Discover row = a suggested artist plus that artist's suggested albums (newest / // most-played), since album suggestions are always derived from a surfaced artist. Pending // suggestions are grouped by artistMbid so the UI shows a single artist-centric row. type Row = { id: string | null; // the artist suggestion id (Follow/Dismiss); null if only albums exist artistMbid: string; artistName: string; score: number; seedCount: number; sources: string[]; seeds: string[]; albums: { id: string; rgMbid: string | null; album: string | null; primaryType: string | null; secondaryTypes: string[]; firstReleaseDate: string | null; albumReason: string | null; }[]; }; export async function GET() { const rows = await prisma.discoverySuggestion.findMany({ where: { status: "pending" }, orderBy: [{ score: "desc" }, { createdAt: "asc" }], }); // "Why suggested": the followed artists (seeds) that surfaced each candidate, from the // per-seed contributions joined to the current watched roster (an unfollowed seed drops out). const candidateMbids = [...new Set(rows.map((r) => r.artistMbid))]; const contribs = candidateMbids.length ? await prisma.discoverySeedContribution.findMany({ where: { candidateMbid: { in: candidateMbids } }, orderBy: { score: "desc" }, }) : []; const seedNames = new Map( (await prisma.watchedArtist.findMany({ select: { mbid: true, name: true } })).map((w) => [w.mbid, w.name]), ); const seedsByCandidate = new Map(); for (const c of contribs) { const name = seedNames.get(c.seedMbid); if (!name) continue; const list = seedsByCandidate.get(c.candidateMbid) ?? []; if (!list.includes(name)) list.push(name); seedsByCandidate.set(c.candidateMbid, list); } // Group by artist. An artist suggestion sets the row header; album suggestions fill albums[]. // An album whose artist has no pending artist suggestion still yields a header (never orphaned). const byArtist = new Map(); const ensure = (r: (typeof rows)[number]): Row => { let row = byArtist.get(r.artistMbid); if (!row) { row = { id: null, artistMbid: r.artistMbid, artistName: r.artistName, score: r.score, seedCount: r.seedCount, sources: r.sources, seeds: seedsByCandidate.get(r.artistMbid) ?? [], albums: [], }; byArtist.set(r.artistMbid, row); } return row; }; for (const r of rows) { const row = ensure(r); if (r.kind === "artist") { row.id = r.id; row.score = r.score; row.seedCount = r.seedCount; row.sources = r.sources; } else { row.albums.push({ id: r.id, rgMbid: r.rgMbid, album: r.album, primaryType: r.primaryType, secondaryTypes: r.secondaryTypes, firstReleaseDate: r.firstReleaseDate, albumReason: r.albumReason, }); row.score = Math.max(row.score, r.score); // album-only row scores by its best album } } for (const row of byArtist.values()) { row.albums.sort((a, b) => (b.firstReleaseDate ?? "").localeCompare(a.firstReleaseDate ?? "")); } const out = [...byArtist.values()].sort((a, b) => b.score - a.score); return Response.json({ rows: out }); }