feat: filter artist discography + counts to core releases unless showAllTypes
This commit is contained in:
@@ -12,6 +12,7 @@ async function seedArtist() {
|
|||||||
releases: {
|
releases: {
|
||||||
create: [
|
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: "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", () => {
|
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 a = await seedArtist();
|
||||||
const res = await GET(new Request("http://localhost/x"), ctx(a.id));
|
const res = await GET(new Request("http://localhost/x"), ctx(a.id));
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
expect(body.name).toBe("John Mayer");
|
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 });
|
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 () => {
|
it("404s an unknown artist on GET", async () => {
|
||||||
expect((await GET(new Request("http://localhost/x"), ctx("nope"))).status).toBe(404);
|
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);
|
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();
|
const a = await seedArtist();
|
||||||
expect((await PATCH(patchReq({ autoMonitorFuture: "yes" }), ctx(a.id))).status).toBe(400);
|
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);
|
expect((await PATCH(patchReq({ autoMonitorFuture: true }), ctx("nope"))).status).toBe(404);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { prisma } from "@/lib/db";
|
import { prisma } from "@/lib/db";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { isCoreRelease } from "@/lib/release-filter";
|
||||||
|
|
||||||
export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) {
|
export async function GET(_request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||||
const { id } = await params;
|
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" }] } },
|
include: { releases: { orderBy: [{ firstReleaseDate: "desc" }, { album: "asc" }] } },
|
||||||
});
|
});
|
||||||
if (!artist) return Response.json({ error: "not found" }, { status: 404 });
|
if (!artist) return Response.json({ error: "not found" }, { status: 404 });
|
||||||
|
const visibleReleases = artist.showAllTypes ? artist.releases : artist.releases.filter(isCoreRelease);
|
||||||
return Response.json({
|
return Response.json({
|
||||||
id: artist.id,
|
id: artist.id,
|
||||||
mbid: artist.mbid,
|
mbid: artist.mbid,
|
||||||
name: artist.name,
|
name: artist.name,
|
||||||
autoMonitorFuture: artist.autoMonitorFuture,
|
autoMonitorFuture: artist.autoMonitorFuture,
|
||||||
releases: artist.releases.map((r) => ({
|
showAllTypes: artist.showAllTypes,
|
||||||
|
releases: visibleReleases.map((r) => ({
|
||||||
id: r.id,
|
id: r.id,
|
||||||
rgMbid: r.rgMbid,
|
rgMbid: r.rgMbid,
|
||||||
album: r.album,
|
album: r.album,
|
||||||
@@ -35,13 +38,26 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
|
|||||||
} catch {
|
} catch {
|
||||||
return Response.json({ error: "invalid JSON" }, { status: 400 });
|
return Response.json({ error: "invalid JSON" }, { status: 400 });
|
||||||
}
|
}
|
||||||
const { autoMonitorFuture } = (body ?? {}) as { autoMonitorFuture?: unknown };
|
const { autoMonitorFuture, showAllTypes } = (body ?? {}) as {
|
||||||
if (typeof autoMonitorFuture !== "boolean") {
|
autoMonitorFuture?: unknown;
|
||||||
return Response.json({ error: "autoMonitorFuture (boolean) is required" }, { status: 400 });
|
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 {
|
try {
|
||||||
const updated = await prisma.watchedArtist.update({ where: { id }, data: { autoMonitorFuture } });
|
const updated = await prisma.watchedArtist.update({ where: { id }, data });
|
||||||
return Response.json({ id: updated.id, autoMonitorFuture: updated.autoMonitorFuture });
|
return Response.json({
|
||||||
|
id: updated.id,
|
||||||
|
autoMonitorFuture: updated.autoMonitorFuture,
|
||||||
|
showAllTypes: updated.showAllTypes,
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") {
|
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2025") {
|
||||||
return Response.json({ error: "not found" }, { status: 404 });
|
return Response.json({ error: "not found" }, { status: 404 });
|
||||||
|
|||||||
@@ -57,4 +57,26 @@ describe("artists collection API", () => {
|
|||||||
const a = (await res.json()).artists[0];
|
const a = (await res.json()).artists[0];
|
||||||
expect(a).toMatchObject({ mbid: "a1", name: "John Mayer", releaseCount: 1, monitoredCount: 1, haveCount: 1 });
|
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 { prisma } from "@/lib/db";
|
||||||
import { browseReleaseGroups } from "@/lib/musicbrainz";
|
import { browseReleaseGroups } from "@/lib/musicbrainz";
|
||||||
|
import { isCoreRelease } from "@/lib/release-filter";
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const artists = await prisma.watchedArtist.findMany({
|
const artists = await prisma.watchedArtist.findMany({
|
||||||
orderBy: { createdAt: "desc" },
|
orderBy: { createdAt: "desc" },
|
||||||
include: { releases: { select: { monitored: true, currentQualityClass: true } } },
|
include: {
|
||||||
|
releases: {
|
||||||
|
select: { monitored: true, currentQualityClass: true, primaryType: true, secondaryTypes: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return Response.json({
|
return Response.json({
|
||||||
artists: artists.map((a) => ({
|
artists: artists.map((a) => {
|
||||||
id: a.id,
|
const visible = a.showAllTypes ? a.releases : a.releases.filter(isCoreRelease);
|
||||||
mbid: a.mbid,
|
return {
|
||||||
name: a.name,
|
id: a.id,
|
||||||
autoMonitorFuture: a.autoMonitorFuture,
|
mbid: a.mbid,
|
||||||
releaseCount: a.releases.length,
|
name: a.name,
|
||||||
monitoredCount: a.releases.filter((r) => r.monitored).length,
|
autoMonitorFuture: a.autoMonitorFuture,
|
||||||
haveCount: a.releases.filter((r) => r.currentQualityClass !== null).length,
|
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