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