feat(discover): group suggestions into artist rows
/api/discover now returns rows[] grouped by artist, each carrying its album suggestions (with albumReason) + the seed names, ordered by score. Album suggestions whose artist has no pending artist suggestion still yield a row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,51 +3,61 @@ import { prisma } from "@/lib/db";
|
||||
import { GET } from "./route";
|
||||
|
||||
describe("discover feed API", () => {
|
||||
it("returns pending artists and albums split, highest score first, excluding non-pending", async () => {
|
||||
it("groups pending suggestions into artist rows with their albums, highest score first", async () => {
|
||||
await prisma.discoverySuggestion.createMany({
|
||||
data: [
|
||||
{ kind: "artist", artistMbid: "a1", artistName: "Low", score: 0.2, seedCount: 1,
|
||||
sources: ["listenbrainz"], secondaryTypes: [], dedupeKey: "artist:a1:" },
|
||||
{ kind: "artist", artistMbid: "a2", artistName: "High", score: 0.9, seedCount: 2,
|
||||
sources: ["listenbrainz"], secondaryTypes: [], dedupeKey: "artist:a2:" },
|
||||
{ kind: "album", artistMbid: "a3", artistName: "Band", rgMbid: "rg1", album: "Rec",
|
||||
score: 0.5, seedCount: 1, sources: ["listenbrainz"], secondaryTypes: [], dedupeKey: "album:a3:rg1" },
|
||||
{ kind: "artist", artistMbid: "a4", artistName: "Gone", score: 5, seedCount: 1,
|
||||
sources: ["listenbrainz"], secondaryTypes: [], status: "dismissed", dedupeKey: "artist:a4:" },
|
||||
{ kind: "album", artistMbid: "a2", artistName: "High", rgMbid: "rg-new", album: "Latest",
|
||||
firstReleaseDate: "2022-01-01", albumReason: "newest", score: 0.9, seedCount: 2,
|
||||
sources: ["listenbrainz"], secondaryTypes: [], dedupeKey: "album:a2:rg-new" },
|
||||
{ kind: "album", artistMbid: "a2", artistName: "High", rgMbid: "rg-hit", album: "Big Hit",
|
||||
firstReleaseDate: "2018-01-01", albumReason: "popular", score: 0.9, seedCount: 2,
|
||||
sources: ["listenbrainz"], secondaryTypes: [], dedupeKey: "album:a2:rg-hit" },
|
||||
{ kind: "artist", artistMbid: "a3", artistName: "Gone", score: 5, seedCount: 1,
|
||||
sources: ["listenbrainz"], secondaryTypes: [], status: "dismissed", dedupeKey: "artist:a3:" },
|
||||
],
|
||||
});
|
||||
const body = await (await GET()).json();
|
||||
expect(body.artists.map((a: any) => a.artistName)).toEqual(["High", "Low"]); // dismissed excluded
|
||||
expect(body.albums.map((a: any) => a.album)).toEqual(["Rec"]);
|
||||
expect(body.artists[0]).toMatchObject({ artistMbid: "a2", seedCount: 2, sources: ["listenbrainz"] });
|
||||
expect(body.rows.map((r: { artistName: string }) => r.artistName)).toEqual(["High", "Low"]); // dismissed excluded, score desc
|
||||
const high = body.rows[0];
|
||||
expect(high).toMatchObject({ id: expect.any(String), artistMbid: "a2", seedCount: 2 });
|
||||
// albums sorted newest-first, each carrying its reason
|
||||
expect(high.albums.map((a: { album: string; albumReason: string }) => [a.album, a.albumReason]))
|
||||
.toEqual([["Latest", "newest"], ["Big Hit", "popular"]]);
|
||||
expect(body.rows[1].albums).toEqual([]); // "Low" has no album suggestions
|
||||
});
|
||||
|
||||
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" },
|
||||
],
|
||||
it("yields a row for an album whose artist has no pending artist suggestion", async () => {
|
||||
await prisma.discoverySuggestion.create({
|
||||
data: { kind: "album", artistMbid: "orphan", artistName: "Orphan Band", rgMbid: "rgX",
|
||||
album: "Stray", albumReason: "newest", score: 0.4, seedCount: 1, sources: ["lastfm"],
|
||||
secondaryTypes: [], dedupeKey: "album:orphan:rgX" },
|
||||
});
|
||||
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" },
|
||||
],
|
||||
const body = await (await GET()).json();
|
||||
expect(body.rows).toHaveLength(1);
|
||||
expect(body.rows[0]).toMatchObject({ id: null, artistName: "Orphan Band" });
|
||||
expect(body.rows[0].albums[0].album).toBe("Stray");
|
||||
});
|
||||
|
||||
it("annotates rows with the followed-artist seeds that surfaced them", async () => {
|
||||
await prisma.watchedArtist.createMany({
|
||||
data: [{ mbid: "seed-am", name: "Arctic Monkeys" }, { mbid: "seed-cp", name: "Coldplay" }],
|
||||
});
|
||||
await prisma.discoverySuggestion.create({
|
||||
data: { kind: "artist", artistMbid: "cand1", artistName: "The Strokes", score: 0.9, seedCount: 2,
|
||||
sources: ["listenbrainz"], secondaryTypes: [], dedupeKey: "artist:cand1:" },
|
||||
});
|
||||
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"] },
|
||||
{ candidateMbid: "cand1", candidateName: "The Strokes", seedMbid: "gone", 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
|
||||
expect(body.rows[0].seeds).toEqual(["Arctic Monkeys", "Coldplay"]); // score desc, unfollowed dropped
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user