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
+29
View File
@@ -21,4 +21,33 @@ describe("discover feed API", () => {
expect(body.albums.map((a: any) => a.album)).toEqual(["Rec"]);
expect(body.artists[0]).toMatchObject({ artistMbid: "a2", seedCount: 2, sources: ["listenbrainz"] });
});
it("annotates each suggestion with the followed-artist seeds that surfaced it", async () => {
await prisma.watchedArtist.createMany({
data: [
{ mbid: "seed-am", name: "Arctic Monkeys" },
{ mbid: "seed-cp", name: "Coldplay" },
{ mbid: "seed-gone", name: "Unfollowed" },
],
});
await prisma.discoverySuggestion.createMany({
data: [
{ kind: "artist", artistMbid: "cand1", artistName: "The Strokes", score: 0.9, seedCount: 2,
sources: ["listenbrainz"], secondaryTypes: [], dedupeKey: "artist:cand1:" },
{ kind: "album", artistMbid: "cand1", artistName: "The Strokes", rgMbid: "rgX", album: "Room on Fire",
score: 0.7, seedCount: 2, sources: ["listenbrainz"], secondaryTypes: [], dedupeKey: "album:cand1:rgX" },
],
});
await prisma.discoverySeedContribution.createMany({
data: [
{ candidateMbid: "cand1", candidateName: "The Strokes", seedMbid: "seed-am", score: 0.6, sources: ["listenbrainz"] },
{ candidateMbid: "cand1", candidateName: "The Strokes", seedMbid: "seed-cp", score: 0.3, sources: ["listenbrainz"] },
{ candidateMbid: "cand1", candidateName: "The Strokes", seedMbid: "no-such-seed", score: 0.9, sources: ["listenbrainz"] },
],
});
const body = await (await GET()).json();
// highest-scoring seed first; an unresolved/unfollowed seed is dropped
expect(body.artists[0].seeds).toEqual(["Arctic Monkeys", "Coldplay"]);
expect(body.albums[0].seeds).toEqual(["Arctic Monkeys", "Coldplay"]); // album inherits its artist's seeds
});
});