feat(discovery): web ListenBrainz similar-artists client

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-12 00:26:29 +02:00
parent a15f4ae8bb
commit 2dfc9ab277
2 changed files with 40 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
import { describe, it, expect, vi, afterEach } from "vitest";
import { similarArtists } from "./listenbrainz";
afterEach(() => vi.restoreAllMocks());
describe("similarArtists", () => {
it("parses rows and drops the seed itself", async () => {
vi.spyOn(global, "fetch").mockResolvedValue(
new Response(JSON.stringify([
{ artist_mbid: "seed", name: "Seed", score: 100 },
{ artist_mbid: "c1", name: "Cand", score: 42 },
]), { status: 200 }),
);
const out = await similarArtists("seed");
expect(out).toEqual([{ mbid: "c1", name: "Cand", score: 42 }]);
});
it("throws on a non-2xx response", async () => {
vi.spyOn(global, "fetch").mockResolvedValue(new Response("nope", { status: 500 }));
await expect(similarArtists("seed")).rejects.toThrow();
});
});
+18
View File
@@ -0,0 +1,18 @@
const LB_BASE = process.env.LISTENBRAINZ_URL ?? "https://labs.api.listenbrainz.org";
// Keep in sync with the worker's _listenbrainz.py default algorithm.
const ALGORITHM =
"session_based_days_7500_session_300_contribution_5_threshold_15_limit_50_filter_True_skip_30";
export type SimilarArtist = { mbid: string; name: string; score: number };
export async function similarArtists(artistMbid: string): Promise<SimilarArtist[]> {
const url =
`${LB_BASE}/similar-artists/json?artist_mbids=${encodeURIComponent(artistMbid)}` +
`&algorithm=${encodeURIComponent(ALGORITHM)}`;
const res = await fetch(url, { headers: { Accept: "application/json" } });
if (!res.ok) throw new Error(`ListenBrainz request failed: ${res.status}`);
const rows: any[] = (await res.json()) ?? [];
return rows
.filter((r) => r.artist_mbid && r.artist_mbid !== artistMbid)
.map((r) => ({ mbid: r.artist_mbid, name: r.name ?? r.comment ?? "", score: Number(r.score ?? 0) }));
}