feat(preview): MB getArtistName + browseReleaseGroupTracks

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-12 01:33:44 +02:00
parent 170180d471
commit e9759b8ef2
2 changed files with 67 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
import { describe, it, expect, vi, afterEach } from "vitest";
import { browseReleaseGroupTracks, getArtistName } from "./musicbrainz";
afterEach(() => vi.restoreAllMocks());
const ok = (obj: unknown) => new Response(JSON.stringify(obj), { status: 200 });
describe("getArtistName", () => {
it("returns the MB artist name", async () => {
vi.spyOn(global, "fetch").mockResolvedValue(ok({ name: "Radiohead" }));
expect(await getArtistName("mbid")).toBe("Radiohead");
});
});
describe("browseReleaseGroupTracks", () => {
it("picks the Official release and flattens its tracks in order", async () => {
vi.spyOn(global, "fetch").mockResolvedValue(ok({
releases: [
{ status: "Bootleg", date: "2001", media: [{ tracks: [{ position: 1, title: "Boot", length: 1000 }] }] },
{ status: "Official", date: "2000", media: [
{ tracks: [{ position: 1, title: "One", length: 200000 }, { position: 2, title: "Two", length: null }] },
] },
],
}));
expect(await browseReleaseGroupTracks("rg")).toEqual([
{ position: 1, title: "One", lengthMs: 200000 },
{ position: 2, title: "Two", lengthMs: null },
]);
});
it("returns [] when there are no releases", async () => {
vi.spyOn(global, "fetch").mockResolvedValue(ok({ releases: [] }));
expect(await browseReleaseGroupTracks("rg")).toEqual([]);
});
});
+33
View File
@@ -63,3 +63,36 @@ export async function searchReleaseGroup(artist: string, album: string): Promise
const credit = g["artist-credit"]?.[0]?.artist ?? {}; const credit = g["artist-credit"]?.[0]?.artist ?? {};
return { ...toReleaseGroup(g), artistMbid: credit.id ?? "", artistName: credit.name ?? artist }; return { ...toReleaseGroup(g), artistMbid: credit.id ?? "", artistName: credit.name ?? artist };
} }
export type Track = { position: number; title: string; lengthMs: number | null };
export async function getArtistName(mbid: string): Promise<string> {
const data = await mbGet(`/artist/${encodeURIComponent(mbid)}?fmt=json`);
return data.name ?? "";
}
export async function browseReleaseGroupTracks(rgMbid: string): Promise<Track[]> {
const data = await mbGet(
`/release?release-group=${encodeURIComponent(rgMbid)}&inc=recordings&fmt=json&limit=25`,
);
const releases: any[] = data.releases ?? [];
if (releases.length === 0) return [];
// Prefer an Official release, then the earliest date.
const pick = [...releases].sort((a, b) => {
const ao = a.status === "Official" ? 0 : 1;
const bo = b.status === "Official" ? 0 : 1;
if (ao !== bo) return ao - bo;
return (a.date ?? "9999").localeCompare(b.date ?? "9999");
})[0];
const tracks: Track[] = [];
for (const medium of pick.media ?? []) {
for (const t of medium.tracks ?? []) {
tracks.push({
position: Number(t.position ?? tracks.length + 1),
title: t.title ?? t.recording?.title ?? "",
lengthMs: t.length ?? t.recording?.length ?? null,
});
}
}
return tracks;
}