feat(discovery): GET /api/discover/search interactive seed search

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-12 00:34:28 +02:00
parent be59862dfc
commit bf868829d8
2 changed files with 67 additions and 0 deletions
@@ -0,0 +1,43 @@
import { describe, it, expect, vi, afterEach } from "vitest";
vi.mock("@/lib/musicbrainz", () => ({ searchArtists: vi.fn() }));
vi.mock("@/lib/listenbrainz", () => ({ similarArtists: vi.fn() }));
import { searchArtists } from "@/lib/musicbrainz";
import { similarArtists } from "@/lib/listenbrainz";
import { GET } from "./route";
const mockSearch = vi.mocked(searchArtists);
const mockSimilar = vi.mocked(similarArtists);
afterEach(() => { mockSearch.mockReset(); mockSimilar.mockReset(); });
function req(artist?: string) {
const u = new URL("http://localhost/api/discover/search");
if (artist !== undefined) u.searchParams.set("artist", artist);
return new Request(u);
}
describe("discover search API", () => {
it("400s when artist is missing/blank", async () => {
expect((await GET(req())).status).toBe(400);
expect((await GET(req(" "))).status).toBe(400);
});
it("resolves the top MB hit then returns its similar artists", async () => {
mockSearch.mockResolvedValue([{ mbid: "seed", name: "Boygenius", disambiguation: "" }]);
mockSimilar.mockResolvedValue([{ mbid: "c1", name: "Cand", score: 9 }]);
const body = await (await GET(req("boygenius"))).json();
expect(body.seed).toMatchObject({ mbid: "seed", name: "Boygenius" });
expect(body.similar).toEqual([{ mbid: "c1", name: "Cand", score: 9 }]);
expect(mockSimilar).toHaveBeenCalledWith("seed");
});
it("returns empty when MB has no hit, 502 on failures", async () => {
mockSearch.mockResolvedValue([]);
expect(await (await GET(req("nobody"))).json()).toEqual({ seed: null, similar: [] });
mockSearch.mockRejectedValue(new Error("mb down"));
expect((await GET(req("x"))).status).toBe(502);
mockSearch.mockResolvedValue([{ mbid: "seed", name: "X", disambiguation: "" }]);
mockSimilar.mockRejectedValue(new Error("lb down"));
expect((await GET(req("x"))).status).toBe(502);
});
});
+24
View File
@@ -0,0 +1,24 @@
import { searchArtists } from "@/lib/musicbrainz";
import { similarArtists } from "@/lib/listenbrainz";
export async function GET(request: Request) {
const artist = new URL(request.url).searchParams.get("artist");
if (!artist || !artist.trim()) {
return Response.json({ error: "artist is required" }, { status: 400 });
}
let hits;
try {
hits = await searchArtists(artist.trim());
} catch {
return Response.json({ error: "MusicBrainz unavailable" }, { status: 502 });
}
if (hits.length === 0) return Response.json({ seed: null, similar: [] });
const seed = hits[0];
let similar;
try {
similar = await similarArtists(seed.mbid);
} catch {
return Response.json({ error: "ListenBrainz unavailable" }, { status: 502 });
}
return Response.json({ seed, similar });
}