feat(library): ignore a release (do-not-monitor)

Add MonitoredRelease.ignored (migration add_release_ignored). The monitor's
enqueue_due skips ignored releases, and they drop off the Wanted list —
independent of `monitored`, so auto-monitor-future can't silently re-grab
an album the user chose to ignore.

- Worker enqueue_due: AND NOT mr.ignored.
- /api/releases/[id] PATCH now accepts `ignored`; /api/wanted filters it out;
  artist detail exposes `ignored`.
- UI: Ignore button on Wanted rows; discography shows an "Ignored" badge with
  an Ignore/Un-ignore toggle (monitor + search disabled while ignored).

Worker + web tests added.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-14 21:03:54 +02:00
parent 6ca39859fa
commit 855a0f15fe
10 changed files with 70 additions and 10 deletions
+1
View File
@@ -27,6 +27,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ id:
secondaryTypes: r.secondaryTypes,
firstReleaseDate: r.firstReleaseDate,
monitored: r.monitored,
ignored: r.ignored,
state: r.state,
currentQualityClass: r.currentQualityClass,
})),
+11 -2
View File
@@ -22,9 +22,18 @@ describe("PATCH /api/releases/[id]", () => {
expect((await prisma.monitoredRelease.findUnique({ where: { id: r.id } }))!.monitored).toBe(true);
});
it("400s a non-boolean and 404s an unknown release", async () => {
it("toggles ignored independently of monitored", async () => {
const r = await seedRelease(true);
const res = await PATCH(patchReq({ ignored: true }), ctx(r.id));
expect(res.status).toBe(200);
const row = (await prisma.monitoredRelease.findUnique({ where: { id: r.id } }))!;
expect(row.ignored).toBe(true);
expect(row.monitored).toBe(true); // ignore doesn't clear monitored
});
it("400s when neither monitored nor ignored is a boolean, 404s an unknown release", async () => {
const r = await seedRelease();
expect((await PATCH(patchReq({ monitored: 1 }), ctx(r.id))).status).toBe(400);
expect((await PATCH(patchReq({ monitored: true }), ctx("nope"))).status).toBe(404);
expect((await PATCH(patchReq({ ignored: true }), ctx("nope"))).status).toBe(404);
});
});
+8 -5
View File
@@ -9,13 +9,16 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
} catch {
return Response.json({ error: "invalid JSON" }, { status: 400 });
}
const { monitored } = (body ?? {}) as { monitored?: unknown };
if (typeof monitored !== "boolean") {
return Response.json({ error: "monitored (boolean) is required" }, { status: 400 });
const { monitored, ignored } = (body ?? {}) as { monitored?: unknown; ignored?: unknown };
if (typeof monitored !== "boolean" && typeof ignored !== "boolean") {
return Response.json({ error: "monitored or ignored (boolean) is required" }, { status: 400 });
}
const data: { monitored?: boolean; ignored?: boolean } = {};
if (typeof monitored === "boolean") data.monitored = monitored;
if (typeof ignored === "boolean") data.ignored = ignored;
try {
const updated = await prisma.monitoredRelease.update({ where: { id }, data: { monitored } });
return Response.json({ id: updated.id, monitored: updated.monitored });
const updated = await prisma.monitoredRelease.update({ where: { id }, data });
return Response.json({ id: updated.id, monitored: updated.monitored, ignored: updated.ignored });
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") {
return Response.json({ error: "not found" }, { status: 404 });
+1 -1
View File
@@ -4,7 +4,7 @@ import { searchReleaseGroup } from "@/lib/musicbrainz";
export async function GET() {
const items = await prisma.monitoredRelease.findMany({
where: { monitored: true, state: { not: "fulfilled" } },
where: { monitored: true, ignored: false, state: { not: "fulfilled" } },
orderBy: [{ lastSearchedAt: { sort: "asc", nulls: "first" } }, { createdAt: "asc" }],
});
return Response.json({