feat: filter artist discography + counts to core releases unless showAllTypes
This commit is contained in:
@@ -12,6 +12,7 @@ async function seedArtist() {
|
||||
releases: {
|
||||
create: [
|
||||
{ artistMbid: "a1", artistName: "John Mayer", rgMbid: "rg1", album: "Continuum", primaryType: "Album", secondaryTypes: [], firstReleaseDate: "2006-09-12", monitored: true, state: "grabbed", currentQualityClass: 1 },
|
||||
{ artistMbid: "a1", artistName: "John Mayer", rgMbid: "rg2", album: "Any Given Thursday", primaryType: "Album", secondaryTypes: ["Live"], firstReleaseDate: "2003", monitored: false, state: "wanted", currentQualityClass: null },
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -23,15 +24,27 @@ function patchReq(body: unknown) {
|
||||
}
|
||||
|
||||
describe("artist item API", () => {
|
||||
it("returns detail with releases", async () => {
|
||||
it("returns detail with only core releases by default", async () => {
|
||||
const a = await seedArtist();
|
||||
const res = await GET(new Request("http://localhost/x"), ctx(a.id));
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.name).toBe("John Mayer");
|
||||
expect(body.showAllTypes).toBe(false);
|
||||
expect(body.releases).toHaveLength(1);
|
||||
expect(body.releases[0]).toMatchObject({ rgMbid: "rg1", album: "Continuum", monitored: true, state: "grabbed", currentQualityClass: 1 });
|
||||
});
|
||||
|
||||
it("returns all releases when showAllTypes is set", async () => {
|
||||
const a = await seedArtist();
|
||||
await prisma.watchedArtist.update({ where: { id: a.id }, data: { showAllTypes: true } });
|
||||
const res = await GET(new Request("http://localhost/x"), ctx(a.id));
|
||||
const body = await res.json();
|
||||
expect(body.showAllTypes).toBe(true);
|
||||
expect(body.releases).toHaveLength(2);
|
||||
expect(body.releases.map((r: { rgMbid: string }) => r.rgMbid).sort()).toEqual(["rg1", "rg2"]);
|
||||
});
|
||||
|
||||
it("404s an unknown artist on GET", async () => {
|
||||
expect((await GET(new Request("http://localhost/x"), ctx("nope"))).status).toBe(404);
|
||||
});
|
||||
@@ -44,9 +57,19 @@ describe("artist item API", () => {
|
||||
expect((await prisma.watchedArtist.findUnique({ where: { id: a.id } }))!.autoMonitorFuture).toBe(true);
|
||||
});
|
||||
|
||||
it("400s a non-boolean toggle and 404s an unknown artist", async () => {
|
||||
it("toggles showAllTypes", async () => {
|
||||
const a = await seedArtist();
|
||||
const res = await PATCH(patchReq({ showAllTypes: true }), ctx(a.id));
|
||||
expect(res.status).toBe(200);
|
||||
const body = await res.json();
|
||||
expect(body.showAllTypes).toBe(true);
|
||||
expect((await prisma.watchedArtist.findUnique({ where: { id: a.id } }))!.showAllTypes).toBe(true);
|
||||
});
|
||||
|
||||
it("400s a body with neither valid boolean field and 404s an unknown artist", async () => {
|
||||
const a = await seedArtist();
|
||||
expect((await PATCH(patchReq({ autoMonitorFuture: "yes" }), ctx(a.id))).status).toBe(400);
|
||||
expect((await PATCH(patchReq({}), ctx(a.id))).status).toBe(400);
|
||||
expect((await PATCH(patchReq({ autoMonitorFuture: true }), ctx("nope"))).status).toBe(404);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { isCoreRelease } from "@/lib/release-filter";
|
||||
|
||||
export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
@@ -8,12 +9,14 @@ export async function GET(_request: Request, { params }: { params: Promise<{ id:
|
||||
include: { releases: { orderBy: [{ firstReleaseDate: "desc" }, { album: "asc" }] } },
|
||||
});
|
||||
if (!artist) return Response.json({ error: "not found" }, { status: 404 });
|
||||
const visibleReleases = artist.showAllTypes ? artist.releases : artist.releases.filter(isCoreRelease);
|
||||
return Response.json({
|
||||
id: artist.id,
|
||||
mbid: artist.mbid,
|
||||
name: artist.name,
|
||||
autoMonitorFuture: artist.autoMonitorFuture,
|
||||
releases: artist.releases.map((r) => ({
|
||||
showAllTypes: artist.showAllTypes,
|
||||
releases: visibleReleases.map((r) => ({
|
||||
id: r.id,
|
||||
rgMbid: r.rgMbid,
|
||||
album: r.album,
|
||||
@@ -35,13 +38,26 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
|
||||
} catch {
|
||||
return Response.json({ error: "invalid JSON" }, { status: 400 });
|
||||
}
|
||||
const { autoMonitorFuture } = (body ?? {}) as { autoMonitorFuture?: unknown };
|
||||
if (typeof autoMonitorFuture !== "boolean") {
|
||||
return Response.json({ error: "autoMonitorFuture (boolean) is required" }, { status: 400 });
|
||||
const { autoMonitorFuture, showAllTypes } = (body ?? {}) as {
|
||||
autoMonitorFuture?: unknown;
|
||||
showAllTypes?: unknown;
|
||||
};
|
||||
const data: { autoMonitorFuture?: boolean; showAllTypes?: boolean } = {};
|
||||
if (typeof autoMonitorFuture === "boolean") data.autoMonitorFuture = autoMonitorFuture;
|
||||
if (typeof showAllTypes === "boolean") data.showAllTypes = showAllTypes;
|
||||
if (Object.keys(data).length === 0) {
|
||||
return Response.json(
|
||||
{ error: "autoMonitorFuture or showAllTypes (boolean) is required" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
try {
|
||||
const updated = await prisma.watchedArtist.update({ where: { id }, data: { autoMonitorFuture } });
|
||||
return Response.json({ id: updated.id, autoMonitorFuture: updated.autoMonitorFuture });
|
||||
const updated = await prisma.watchedArtist.update({ where: { id }, data });
|
||||
return Response.json({
|
||||
id: updated.id,
|
||||
autoMonitorFuture: updated.autoMonitorFuture,
|
||||
showAllTypes: updated.showAllTypes,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") {
|
||||
return Response.json({ error: "not found" }, { status: 404 });
|
||||
|
||||
@@ -57,4 +57,26 @@ describe("artists collection API", () => {
|
||||
const a = (await res.json()).artists[0];
|
||||
expect(a).toMatchObject({ mbid: "a1", name: "John Mayer", releaseCount: 1, monitoredCount: 1, haveCount: 1 });
|
||||
});
|
||||
|
||||
it("counts only core releases unless showAllTypes is set", async () => {
|
||||
await prisma.watchedArtist.create({
|
||||
data: {
|
||||
mbid: "a2",
|
||||
name: "Live Band",
|
||||
releases: {
|
||||
create: [
|
||||
{ artistMbid: "a2", artistName: "Live Band", rgMbid: "rg-core", album: "Studio Album", primaryType: "Album", secondaryTypes: [], firstReleaseDate: "2010", monitored: true, state: "grabbed", currentQualityClass: 1 },
|
||||
{ artistMbid: "a2", artistName: "Live Band", rgMbid: "rg-live", album: "Live at the Roxy", primaryType: "Album", secondaryTypes: ["Live"], firstReleaseDate: "2011", monitored: true, state: "wanted", currentQualityClass: 2 },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const before = (await (await GET()).json()).artists.find((a: { mbid: string }) => a.mbid === "a2");
|
||||
expect(before).toMatchObject({ showAllTypes: false, releaseCount: 1, monitoredCount: 1, haveCount: 1 });
|
||||
|
||||
await prisma.watchedArtist.update({ where: { mbid: "a2" }, data: { showAllTypes: true } });
|
||||
const after = (await (await GET()).json()).artists.find((a: { mbid: string }) => a.mbid === "a2");
|
||||
expect(after).toMatchObject({ showAllTypes: true, releaseCount: 2, monitoredCount: 2, haveCount: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import { browseReleaseGroups } from "@/lib/musicbrainz";
|
||||
import { isCoreRelease } from "@/lib/release-filter";
|
||||
|
||||
export async function GET() {
|
||||
const artists = await prisma.watchedArtist.findMany({
|
||||
orderBy: { createdAt: "desc" },
|
||||
include: { releases: { select: { monitored: true, currentQualityClass: true } } },
|
||||
include: {
|
||||
releases: {
|
||||
select: { monitored: true, currentQualityClass: true, primaryType: true, secondaryTypes: 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,
|
||||
})),
|
||||
artists: artists.map((a) => {
|
||||
const visible = a.showAllTypes ? a.releases : a.releases.filter(isCoreRelease);
|
||||
return {
|
||||
id: a.id,
|
||||
mbid: a.mbid,
|
||||
name: a.name,
|
||||
autoMonitorFuture: a.autoMonitorFuture,
|
||||
showAllTypes: a.showAllTypes,
|
||||
releaseCount: visible.length,
|
||||
monitoredCount: visible.filter((r) => r.monitored).length,
|
||||
haveCount: visible.filter((r) => r.currentQualityClass !== null).length,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { isCoreRelease } from "./release-filter";
|
||||
|
||||
describe("isCoreRelease", () => {
|
||||
it("accepts Album/Single/EP with no secondary types", () => {
|
||||
expect(isCoreRelease({ primaryType: "Album", secondaryTypes: [] })).toBe(true);
|
||||
expect(isCoreRelease({ primaryType: "Single", secondaryTypes: [] })).toBe(true);
|
||||
expect(isCoreRelease({ primaryType: "EP", secondaryTypes: [] })).toBe(true);
|
||||
});
|
||||
it("rejects secondary types and non-core primaries and null", () => {
|
||||
expect(isCoreRelease({ primaryType: "Album", secondaryTypes: ["Live"] })).toBe(false);
|
||||
expect(isCoreRelease({ primaryType: "Album", secondaryTypes: ["Compilation"] })).toBe(false);
|
||||
expect(isCoreRelease({ primaryType: "Broadcast", secondaryTypes: [] })).toBe(false);
|
||||
expect(isCoreRelease({ primaryType: null, secondaryTypes: [] })).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
const CORE_PRIMARY = new Set(["Album", "Single", "EP"]);
|
||||
|
||||
/** A "core" studio release: Album/Single/EP with no secondary types (live, compilation, etc.). */
|
||||
export function isCoreRelease(r: { primaryType: string | null; secondaryTypes: string[] }): boolean {
|
||||
return CORE_PRIMARY.has(r.primaryType ?? "") && r.secondaryTypes.length === 0;
|
||||
}
|
||||
Reference in New Issue
Block a user