feat(discovery): POST /api/discover/[id] follow/want/dismiss actions
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
|
||||
vi.mock("@/lib/musicbrainz", () => ({ browseReleaseGroups: vi.fn() }));
|
||||
import { browseReleaseGroups } from "@/lib/musicbrainz";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { POST } from "./route";
|
||||
|
||||
const mockBrowse = vi.mocked(browseReleaseGroups);
|
||||
afterEach(() => mockBrowse.mockReset());
|
||||
|
||||
function post(id: string, body: unknown) {
|
||||
return POST(
|
||||
new Request(`http://localhost/api/discover/${id}`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
{ params: Promise.resolve({ id }) },
|
||||
);
|
||||
}
|
||||
|
||||
async function artist(mbid = "a1") {
|
||||
return prisma.discoverySuggestion.create({
|
||||
data: { kind: "artist", artistMbid: mbid, artistName: "Cand", score: 1, seedCount: 1,
|
||||
sources: ["listenbrainz"], secondaryTypes: [], dedupeKey: `artist:${mbid}:` },
|
||||
});
|
||||
}
|
||||
async function album(rg = "rg1") {
|
||||
return prisma.discoverySuggestion.create({
|
||||
data: { kind: "album", artistMbid: "a9", artistName: "Band", rgMbid: rg, album: "Rec",
|
||||
primaryType: "Album", score: 1, seedCount: 1, sources: ["listenbrainz"],
|
||||
secondaryTypes: [], firstReleaseDate: "2023", dedupeKey: `album:a9:${rg}` },
|
||||
});
|
||||
}
|
||||
|
||||
describe("discover action API", () => {
|
||||
it("dismiss marks the suggestion dismissed", async () => {
|
||||
const s = await artist("d1");
|
||||
const res = await post(s.id, { action: "dismiss" });
|
||||
expect(res.status).toBe(200);
|
||||
expect((await prisma.discoverySuggestion.findUnique({ where: { id: s.id } }))!.status).toBe("dismissed");
|
||||
});
|
||||
|
||||
it("follow creates a WatchedArtist + unmonitored discography and marks followed", async () => {
|
||||
mockBrowse.mockResolvedValue([
|
||||
{ rgMbid: "rg1", title: "Debut", primaryType: "Album", secondaryTypes: [], firstReleaseDate: "2019" },
|
||||
]);
|
||||
const s = await artist("f1");
|
||||
const res = await post(s.id, { action: "follow" });
|
||||
expect(res.status).toBe(200);
|
||||
expect(await prisma.watchedArtist.findUnique({ where: { mbid: "f1" } })).not.toBeNull();
|
||||
const rel = await prisma.monitoredRelease.findUnique({ where: { rgMbid: "rg1" } });
|
||||
expect(rel).toMatchObject({ monitored: false, artistName: "Cand" });
|
||||
expect((await prisma.discoverySuggestion.findUnique({ where: { id: s.id } }))!.status).toBe("followed");
|
||||
});
|
||||
|
||||
it("follow is idempotent when already following (no MB call, still marks followed)", async () => {
|
||||
await prisma.watchedArtist.create({ data: { mbid: "f2", name: "Cand" } });
|
||||
const s = await artist("f2");
|
||||
const res = await post(s.id, { action: "follow" });
|
||||
expect(res.status).toBe(200);
|
||||
expect(mockBrowse).not.toHaveBeenCalled();
|
||||
expect((await prisma.discoverySuggestion.findUnique({ where: { id: s.id } }))!.status).toBe("followed");
|
||||
});
|
||||
|
||||
it("want upserts a monitored release from the suggestion and marks wanted", async () => {
|
||||
const s = await album("rgW");
|
||||
const res = await post(s.id, { action: "want" });
|
||||
expect(res.status).toBe(200);
|
||||
const rel = await prisma.monitoredRelease.findUnique({ where: { rgMbid: "rgW" } });
|
||||
expect(rel).toMatchObject({ album: "Rec", monitored: true, artistName: "Band" });
|
||||
expect((await prisma.discoverySuggestion.findUnique({ where: { id: s.id } }))!.status).toBe("wanted");
|
||||
});
|
||||
|
||||
it("rejects bad action, mismatched kind, and unknown id", async () => {
|
||||
const a = await artist("m1");
|
||||
expect((await post(a.id, { action: "nope" })).status).toBe(400);
|
||||
expect((await post(a.id, { action: "want" })).status).toBe(400); // want on artist
|
||||
const al = await album("rgM");
|
||||
expect((await post(al.id, { action: "follow" })).status).toBe(400); // follow on album
|
||||
expect((await post("missing", { action: "dismiss" })).status).toBe(404);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { browseReleaseGroups } from "@/lib/musicbrainz";
|
||||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return Response.json({ error: "invalid JSON" }, { status: 400 });
|
||||
}
|
||||
const { action } = (body ?? {}) as { action?: unknown };
|
||||
if (action !== "follow" && action !== "want" && action !== "dismiss") {
|
||||
return Response.json({ error: "action must be follow, want, or dismiss" }, { status: 400 });
|
||||
}
|
||||
|
||||
const s = await prisma.discoverySuggestion.findUnique({ where: { id } });
|
||||
if (!s) return Response.json({ error: "not found" }, { status: 404 });
|
||||
|
||||
if (action === "dismiss") {
|
||||
await prisma.discoverySuggestion.update({ where: { id }, data: { status: "dismissed" } });
|
||||
return Response.json({ id, status: "dismissed" });
|
||||
}
|
||||
|
||||
if (action === "follow") {
|
||||
if (s.kind !== "artist") {
|
||||
return Response.json({ error: "follow applies to artist suggestions" }, { status: 400 });
|
||||
}
|
||||
const existing = await prisma.watchedArtist.findUnique({ where: { mbid: s.artistMbid } });
|
||||
if (!existing) {
|
||||
let releases;
|
||||
try {
|
||||
releases = await browseReleaseGroups(s.artistMbid);
|
||||
} catch {
|
||||
return Response.json({ error: "MusicBrainz unavailable" }, { status: 502 });
|
||||
}
|
||||
const created = await prisma.watchedArtist.create({
|
||||
data: { mbid: s.artistMbid, name: s.artistName },
|
||||
});
|
||||
if (releases.length > 0) {
|
||||
await prisma.monitoredRelease.createMany({
|
||||
data: releases.map((r) => ({
|
||||
watchedArtistId: created.id,
|
||||
artistMbid: s.artistMbid,
|
||||
artistName: s.artistName,
|
||||
rgMbid: r.rgMbid,
|
||||
album: r.title,
|
||||
primaryType: r.primaryType,
|
||||
secondaryTypes: r.secondaryTypes,
|
||||
firstReleaseDate: r.firstReleaseDate,
|
||||
monitored: false,
|
||||
})),
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
await prisma.discoverySuggestion.update({ where: { id }, data: { status: "followed" } });
|
||||
return Response.json({ id, status: "followed" });
|
||||
}
|
||||
|
||||
// action === "want"
|
||||
if (s.kind !== "album" || !s.rgMbid) {
|
||||
return Response.json({ error: "want applies to album suggestions" }, { status: 400 });
|
||||
}
|
||||
await prisma.monitoredRelease.upsert({
|
||||
where: { rgMbid: s.rgMbid },
|
||||
create: {
|
||||
artistMbid: s.artistMbid,
|
||||
artistName: s.artistName,
|
||||
rgMbid: s.rgMbid,
|
||||
album: s.album ?? "",
|
||||
primaryType: s.primaryType,
|
||||
secondaryTypes: s.secondaryTypes,
|
||||
firstReleaseDate: s.firstReleaseDate,
|
||||
monitored: true,
|
||||
},
|
||||
update: { monitored: true },
|
||||
});
|
||||
await prisma.discoverySuggestion.update({ where: { id }, data: { status: "wanted" } });
|
||||
return Response.json({ id, status: "wanted" });
|
||||
}
|
||||
Reference in New Issue
Block a user