feat(preview): MB getArtistName + browseReleaseGroupTracks
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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([]);
|
||||
});
|
||||
});
|
||||
@@ -63,3 +63,36 @@ export async function searchReleaseGroup(artist: string, album: string): Promise
|
||||
const credit = g["artist-credit"]?.[0]?.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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user