diff --git a/web/src/app/api/artists/route.test.ts b/web/src/app/api/artists/route.test.ts new file mode 100644 index 0000000..4b964be --- /dev/null +++ b/web/src/app/api/artists/route.test.ts @@ -0,0 +1,60 @@ +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 { GET, POST } from "./route"; + +const mockBrowse = vi.mocked(browseReleaseGroups); +afterEach(() => mockBrowse.mockReset()); + +function postReq(body: unknown) { + return new Request("http://localhost/api/artists", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(body), + }); +} + +describe("artists collection API", () => { + it("follows an artist and persists the discography unmonitored", async () => { + mockBrowse.mockResolvedValue([ + { rgMbid: "rg1", title: "Continuum", primaryType: "Album", secondaryTypes: [], firstReleaseDate: "2006-09-12" }, + { rgMbid: "rg2", title: "Battle Studies", primaryType: "Album", secondaryTypes: [], firstReleaseDate: "2009" }, + ]); + const res = await POST(postReq({ mbid: "a1", name: "John Mayer" })); + expect(res.status).toBe(201); + expect((await res.json()).releaseCount).toBe(2); + + const rels = await prisma.monitoredRelease.findMany({ where: { artistMbid: "a1" } }); + expect(rels).toHaveLength(2); + expect(rels.every((r) => r.monitored === false && r.state === "wanted")).toBe(true); + }); + + it("rejects a bad body and a duplicate follow", async () => { + expect((await POST(postReq({ mbid: "a1" }))).status).toBe(400); + mockBrowse.mockResolvedValue([]); + expect((await POST(postReq({ mbid: "a1", name: "John Mayer" }))).status).toBe(201); + expect((await POST(postReq({ mbid: "a1", name: "John Mayer" }))).status).toBe(409); + }); + + it("502s when the discography fetch fails and creates nothing", async () => { + mockBrowse.mockRejectedValue(new Error("boom")); + expect((await POST(postReq({ mbid: "a9", name: "X" }))).status).toBe(502); + expect(await prisma.watchedArtist.findUnique({ where: { mbid: "a9" } })).toBeNull(); + }); + + it("lists followed artists with counts", async () => { + mockBrowse.mockResolvedValue([ + { rgMbid: "rg1", title: "A", primaryType: "Album", secondaryTypes: [], firstReleaseDate: "2001" }, + ]); + await POST(postReq({ mbid: "a1", name: "John Mayer" })); + // mark the one release as monitored + have, to exercise the counts + await prisma.monitoredRelease.updateMany({ where: { artistMbid: "a1" }, data: { monitored: true, currentQualityClass: 2 } }); + + const res = await GET(); + expect(res.status).toBe(200); + const a = (await res.json()).artists[0]; + expect(a).toMatchObject({ mbid: "a1", name: "John Mayer", releaseCount: 1, monitoredCount: 1, haveCount: 1 }); + }); +}); diff --git a/web/src/app/api/artists/route.ts b/web/src/app/api/artists/route.ts new file mode 100644 index 0000000..d04fd45 --- /dev/null +++ b/web/src/app/api/artists/route.ts @@ -0,0 +1,65 @@ +import { prisma } from "@/lib/db"; +import { browseReleaseGroups } from "@/lib/musicbrainz"; + +export async function GET() { + const artists = await prisma.watchedArtist.findMany({ + orderBy: { createdAt: "desc" }, + include: { releases: { select: { monitored: true, currentQualityClass: true } } }, + }); + return Response.json({ + artists: artists.map((a) => ({ + id: a.id, + mbid: a.mbid, + name: a.name, + autoMonitorFuture: a.autoMonitorFuture, + releaseCount: a.releases.length, + monitoredCount: a.releases.filter((r) => r.monitored).length, + haveCount: a.releases.filter((r) => r.currentQualityClass !== null).length, + })), + }); +} + +export async function POST(request: Request) { + let body: unknown; + try { + body = await request.json(); + } catch { + return Response.json({ error: "invalid JSON" }, { status: 400 }); + } + const { mbid, name } = (body ?? {}) as { mbid?: unknown; name?: unknown }; + if (typeof mbid !== "string" || !mbid.trim() || typeof name !== "string" || !name.trim()) { + return Response.json({ error: "mbid and name are required" }, { status: 400 }); + } + if (await prisma.watchedArtist.findUnique({ where: { mbid } })) { + return Response.json({ error: "already following this artist" }, { status: 409 }); + } + + let releases; + try { + releases = await browseReleaseGroups(mbid); + } catch { + return Response.json({ error: "MusicBrainz unavailable" }, { status: 502 }); + } + + const artist = await prisma.watchedArtist.create({ data: { mbid, name: name.trim() } }); + if (releases.length > 0) { + await prisma.monitoredRelease.createMany({ + data: releases.map((r) => ({ + watchedArtistId: artist.id, + artistMbid: mbid, + artistName: name.trim(), + rgMbid: r.rgMbid, + album: r.title, + primaryType: r.primaryType, + secondaryTypes: r.secondaryTypes, + firstReleaseDate: r.firstReleaseDate, + monitored: false, + })), + skipDuplicates: true, + }); + } + return Response.json( + { id: artist.id, mbid: artist.mbid, name: artist.name, releaseCount: releases.length }, + { status: 201 }, + ); +}