feat(discover): show why each item is suggested

Each suggestion now carries the followed artists (seeds) that surfaced it,
from DiscoverySeedContribution joined to the current watched roster (an
unfollowed seed drops out), highest-scoring seed first. /discover renders
"Similar to Arctic Monkeys, Coldplay +2" under each artist and album row —
both kinds key off the candidate's artistMbid, so an album inherits its
artist's seeds. web 187 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-14 22:07:05 +02:00
parent 18a22db7fb
commit a47d37a787
5 changed files with 67 additions and 0 deletions
+25
View File
@@ -5,6 +5,30 @@ export async function GET() {
where: { status: "pending" },
orderBy: [{ score: "desc" }, { createdAt: "asc" }],
});
// "Why suggested": the followed artists (seeds) that surfaced each candidate. A suggestion's
// candidate is its artistMbid (for both artist and album kinds — the album's artist is what
// was found similar). Join the per-seed contributions to the current watched roster so an
// unfollowed seed drops out. Highest-scoring seed first.
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<string, string[]>();
for (const c of contribs) {
const name = seedNames.get(c.seedMbid);
if (!name) continue; // seed no longer followed
const list = seedsByCandidate.get(c.candidateMbid) ?? [];
if (!list.includes(name)) list.push(name);
seedsByCandidate.set(c.candidateMbid, list);
}
const map = (r: (typeof rows)[number]) => ({
id: r.id,
artistMbid: r.artistMbid,
@@ -17,6 +41,7 @@ export async function GET() {
score: r.score,
seedCount: r.seedCount,
sources: r.sources,
seeds: seedsByCandidate.get(r.artistMbid) ?? [],
});
return Response.json({
artists: rows.filter((r) => r.kind === "artist").map(map),