feat(preview): GET /api/discover/preview/[mbid] annotated discography

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-12 01:39:35 +02:00
parent f3cb306a19
commit 46164c9eab
2 changed files with 105 additions and 0 deletions
@@ -0,0 +1,54 @@
import { describe, it, expect, vi, afterEach } from "vitest";
vi.mock("@/lib/musicbrainz", () => ({ browseReleaseGroups: vi.fn(), getArtistName: vi.fn() }));
import { browseReleaseGroups, getArtistName } from "@/lib/musicbrainz";
import { prisma } from "@/lib/db";
import { GET } from "./route";
const mockBrowse = vi.mocked(browseReleaseGroups);
const mockName = vi.mocked(getArtistName);
afterEach(() => { mockBrowse.mockReset(); mockName.mockReset(); });
function get(mbid: string, qs = "") {
return GET(new Request(`http://localhost/api/discover/preview/${mbid}${qs}`), {
params: Promise.resolve({ mbid }),
});
}
describe("discover preview API", () => {
it("annotates own-state, defaults to core releases, reports followed", async () => {
mockBrowse.mockResolvedValue([
{ rgMbid: "rg1", title: "Studio", primaryType: "Album", secondaryTypes: [], firstReleaseDate: "2000" },
{ rgMbid: "rg2", title: "Live One", primaryType: "Album", secondaryTypes: ["Live"], firstReleaseDate: "2001" },
{ rgMbid: "rg3", title: "Owned", primaryType: "Album", secondaryTypes: [], firstReleaseDate: "2002" },
{ rgMbid: "rg4", title: "Scanned", primaryType: "Album", secondaryTypes: [], firstReleaseDate: "2003" },
]);
await prisma.watchedArtist.create({ data: { mbid: "a1", name: "Band" } });
await prisma.monitoredRelease.create({ data: { artistMbid: "a1", artistName: "Band", rgMbid: "rg1", album: "Studio", secondaryTypes: [], monitored: true } });
await prisma.monitoredRelease.create({ data: { artistMbid: "a1", artistName: "Band", rgMbid: "rg3", album: "Owned", secondaryTypes: [], monitored: false, currentQualityClass: 3 } });
await prisma.libraryItem.create({ data: { artist: "Band", album: "Scanned", path: "/x", source: "scan", format: "FLAC", qualityClass: 3 } });
const body = await (await get("a1", "?name=Band")).json();
expect(body.followed).toBe(true);
expect(body.artistName).toBe("Band");
expect(body.releases.map((r: any) => r.album)).toEqual(["Studio", "Owned", "Scanned"]); // Live excluded
expect(body.releases.find((r: any) => r.rgMbid === "rg1")).toMatchObject({ monitored: true, have: false });
expect(body.releases.find((r: any) => r.rgMbid === "rg3")).toMatchObject({ monitored: false, have: true });
expect(body.releases.find((r: any) => r.rgMbid === "rg4")).toMatchObject({ have: true }); // via LibraryItem
expect(mockName).not.toHaveBeenCalled(); // ?name= shortcut
});
it("?all=true includes non-core; missing ?name falls back to getArtistName; 502 on MB throw", async () => {
mockBrowse.mockResolvedValue([
{ rgMbid: "rg2", title: "Live One", primaryType: "Album", secondaryTypes: ["Live"], firstReleaseDate: "2001" },
]);
mockName.mockResolvedValue("Resolved");
const body = await (await get("a1", "?all=true")).json();
expect(body.releases.map((r: any) => r.album)).toEqual(["Live One"]);
expect(body.artistName).toBe("Resolved");
expect(body.followed).toBe(false);
mockBrowse.mockRejectedValue(new Error("down"));
expect((await get("a1")).status).toBe(502);
});
});
@@ -0,0 +1,51 @@
import { prisma } from "@/lib/db";
import { browseReleaseGroups, getArtistName } from "@/lib/musicbrainz";
import { isCoreRelease } from "@/lib/release-filter";
export async function GET(request: Request, { params }: { params: Promise<{ mbid: string }> }) {
const { mbid } = await params;
const url = new URL(request.url);
const nameParam = url.searchParams.get("name");
const showAll = url.searchParams.get("all") === "true";
let groups;
try {
groups = await browseReleaseGroups(mbid);
} catch {
return Response.json({ error: "MusicBrainz unavailable" }, { status: 502 });
}
const visible = showAll ? groups : groups.filter(isCoreRelease);
let artistName = nameParam?.trim() || "";
if (!artistName) {
try {
artistName = await getArtistName(mbid);
} catch {
artistName = "";
}
}
const followed = (await prisma.watchedArtist.findUnique({ where: { mbid } })) != null;
const rgMbids = visible.map((g) => g.rgMbid);
const albums = visible.map((g) => g.title);
const monitored = await prisma.monitoredRelease.findMany({ where: { rgMbid: { in: rgMbids } } });
const monMap = new Map(monitored.map((m) => [m.rgMbid, m]));
const libItems = await prisma.libraryItem.findMany({ where: { artist: artistName, album: { in: albums } } });
const haveAlbums = new Set(libItems.map((l) => l.album));
const releases = visible.map((g) => {
const m = monMap.get(g.rgMbid);
return {
rgMbid: g.rgMbid,
album: g.title,
primaryType: g.primaryType,
secondaryTypes: g.secondaryTypes,
firstReleaseDate: g.firstReleaseDate,
monitored: m?.monitored ?? false,
have: m?.currentQualityClass != null || haveAlbums.has(g.title),
};
});
return Response.json({ artistName, followed, releases });
}