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:
@@ -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 });
|
||||
}
|
||||
Reference in New Issue
Block a user