diff --git a/web/src/app/api/wanted/route.test.ts b/web/src/app/api/wanted/route.test.ts new file mode 100644 index 0000000..a93ae2e --- /dev/null +++ b/web/src/app/api/wanted/route.test.ts @@ -0,0 +1,54 @@ +import { describe, it, expect, vi, afterEach } from "vitest"; + +vi.mock("@/lib/musicbrainz", () => ({ searchReleaseGroup: vi.fn() })); +import { searchReleaseGroup } from "@/lib/musicbrainz"; +import { prisma } from "@/lib/db"; +import { GET, POST } from "./route"; + +const mockSearch = vi.mocked(searchReleaseGroup); +afterEach(() => mockSearch.mockReset()); + +function postReq(body: unknown) { + return new Request("http://localhost/api/wanted", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }); +} + +describe("wanted API", () => { + it("lists monitored, non-fulfilled releases only", async () => { + await prisma.monitoredRelease.createMany({ + data: [ + { artistMbid: "a1", artistName: "A", rgMbid: "rg1", album: "Wanted", secondaryTypes: [], monitored: true, state: "wanted" }, + { artistMbid: "a1", artistName: "A", rgMbid: "rg2", album: "Done", secondaryTypes: [], monitored: true, state: "fulfilled" }, + { artistMbid: "a1", artistName: "A", rgMbid: "rg3", album: "Dormant", secondaryTypes: [], monitored: false, state: "wanted" }, + ], + }); + const res = await GET(); + expect(res.status).toBe(200); + const wanted = (await res.json()).wanted; + expect(wanted.map((w: any) => w.album)).toEqual(["Wanted"]); + }); + + it("adds a standalone wanted release from artist+album", async () => { + mockSearch.mockResolvedValue({ rgMbid: "rg9", title: "Continuum", primaryType: "Album", secondaryTypes: [], firstReleaseDate: "2006-09-12", artistMbid: "a1", artistName: "John Mayer" }); + const res = await POST(postReq({ artist: "John Mayer", album: "Continuum" })); + expect(res.status).toBe(201); + const row = await prisma.monitoredRelease.findUnique({ where: { rgMbid: "rg9" } }); + expect(row).toMatchObject({ artistName: "John Mayer", album: "Continuum", monitored: true, watchedArtistId: null }); + }); + + it("re-monitors an existing release-group (200) instead of duplicating", async () => { + await prisma.monitoredRelease.create({ data: { artistMbid: "a1", artistName: "John Mayer", rgMbid: "rg9", album: "Continuum", secondaryTypes: [], monitored: false } }); + mockSearch.mockResolvedValue({ rgMbid: "rg9", title: "Continuum", primaryType: "Album", secondaryTypes: [], firstReleaseDate: "2006", artistMbid: "a1", artistName: "John Mayer" }); + const res = await POST(postReq({ artist: "John Mayer", album: "Continuum" })); + expect(res.status).toBe(200); + expect((await prisma.monitoredRelease.findUnique({ where: { rgMbid: "rg9" } }))!.monitored).toBe(true); + expect(await prisma.monitoredRelease.count({ where: { rgMbid: "rg9" } })).toBe(1); + }); + + it("400s a bad body, 404s no MB match, 502s on MB failure", async () => { + expect((await POST(postReq({ artist: "X" }))).status).toBe(400); + mockSearch.mockResolvedValue(null); + expect((await POST(postReq({ artist: "X", album: "Y" }))).status).toBe(404); + mockSearch.mockRejectedValue(new Error("boom")); + expect((await POST(postReq({ artist: "X", album: "Y" }))).status).toBe(502); + }); +}); diff --git a/web/src/app/api/wanted/route.ts b/web/src/app/api/wanted/route.ts new file mode 100644 index 0000000..8fdd1e8 --- /dev/null +++ b/web/src/app/api/wanted/route.ts @@ -0,0 +1,62 @@ +import { prisma } from "@/lib/db"; +import { searchReleaseGroup } from "@/lib/musicbrainz"; + +export async function GET() { + const items = await prisma.monitoredRelease.findMany({ + where: { monitored: true, state: { not: "fulfilled" } }, + orderBy: [{ lastSearchedAt: { sort: "asc", nulls: "first" } }, { createdAt: "asc" }], + }); + return Response.json({ + wanted: items.map((r) => ({ + id: r.id, + artistName: r.artistName, + album: r.album, + state: r.state, + currentQualityClass: r.currentQualityClass, + lastSearchedAt: r.lastSearchedAt, + watchedArtistId: r.watchedArtistId, + })), + }); +} + +export async function POST(request: Request) { + let body: unknown; + try { + body = await request.json(); + } catch { + return Response.json({ error: "invalid JSON" }, { status: 400 }); + } + const { artist, album } = (body ?? {}) as { artist?: unknown; album?: unknown }; + if (typeof artist !== "string" || !artist.trim() || typeof album !== "string" || !album.trim()) { + return Response.json({ error: "artist and album are required" }, { status: 400 }); + } + + let match; + try { + match = await searchReleaseGroup(artist.trim(), album.trim()); + } catch { + return Response.json({ error: "MusicBrainz unavailable" }, { status: 502 }); + } + if (!match) { + return Response.json({ error: "no matching release-group found" }, { status: 404 }); + } + + const existing = await prisma.monitoredRelease.findUnique({ where: { rgMbid: match.rgMbid } }); + if (existing) { + const updated = await prisma.monitoredRelease.update({ where: { rgMbid: match.rgMbid }, data: { monitored: true } }); + return Response.json({ id: updated.id, album: updated.album, monitored: updated.monitored }, { status: 200 }); + } + const created = await prisma.monitoredRelease.create({ + data: { + artistMbid: match.artistMbid, + artistName: match.artistName, + rgMbid: match.rgMbid, + album: match.title, + primaryType: match.primaryType, + secondaryTypes: match.secondaryTypes, + firstReleaseDate: match.firstReleaseDate, + monitored: true, + }, + }); + return Response.json({ id: created.id, album: created.album, monitored: created.monitored }, { status: 201 }); +}