feat: read-only MusicBrainz module + monitoring test cleanup

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jonathan
2026-07-11 13:11:11 +02:00
parent eb2a2207a1
commit cdcda9e501
3 changed files with 118 additions and 1 deletions
+65
View File
@@ -0,0 +1,65 @@
const MB_BASE = "https://musicbrainz.org/ws/2";
const USER_AGENT = "Lyra/0.1 ( https://git.jger.nl/Jonathan/Lyra )";
export type ArtistHit = { mbid: string; name: string; disambiguation: string };
export type ReleaseGroupInfo = {
rgMbid: string;
title: string;
primaryType: string | null;
secondaryTypes: string[];
firstReleaseDate: string | null;
};
export type ReleaseGroupMatch = ReleaseGroupInfo & { artistMbid: string; artistName: string };
async function mbGet(path: string): Promise<any> {
const res = await fetch(`${MB_BASE}${path}`, {
headers: { "User-Agent": USER_AGENT, Accept: "application/json" },
});
if (!res.ok) throw new Error(`MusicBrainz request failed: ${res.status}`);
return res.json();
}
function toReleaseGroup(g: any): ReleaseGroupInfo {
return {
rgMbid: g.id,
title: g.title ?? "",
primaryType: g["primary-type"] ?? null,
secondaryTypes: g["secondary-types"] ?? [],
firstReleaseDate: g["first-release-date"] || null,
};
}
export async function searchArtists(query: string): Promise<ArtistHit[]> {
const data = await mbGet(`/artist?query=${encodeURIComponent(query)}&fmt=json&limit=8`);
return (data.artists ?? []).map((a: any) => ({
mbid: a.id,
name: a.name ?? "",
disambiguation: a.disambiguation ?? "",
}));
}
export async function browseReleaseGroups(artistMbid: string): Promise<ReleaseGroupInfo[]> {
const out: ReleaseGroupInfo[] = [];
let offset = 0;
for (;;) {
const data = await mbGet(
`/release-group?artist=${encodeURIComponent(artistMbid)}&fmt=json&limit=100&offset=${offset}`,
);
const groups: any[] = data["release-groups"] ?? [];
for (const g of groups) out.push(toReleaseGroup(g));
offset += groups.length;
const total = data["release-group-count"] ?? offset;
if (groups.length === 0 || offset >= total) break;
}
return out;
}
export async function searchReleaseGroup(artist: string, album: string): Promise<ReleaseGroupMatch | null> {
const query = `releasegroup:"${album}" AND artist:"${artist}"`;
const data = await mbGet(`/release-group?query=${encodeURIComponent(query)}&fmt=json&limit=5`);
const groups: any[] = data["release-groups"] ?? [];
if (groups.length === 0) return null;
const g = groups[0];
const credit = g["artist-credit"]?.[0]?.artist ?? {};
return { ...toReleaseGroup(g), artistMbid: credit.id ?? "", artistName: credit.name ?? artist };
}