/** BeatSaver public API — map key / hash resolution for campaign `songid` + `hash`. */ const BEATSAVER_BASE = "https://api.beatsaver.com"; export type BeatSaverMapMeta = { id: string; name?: string; /** Lowercase SHA-1 for latest / selected version */ hash: string; songName?: string; songSubName?: string; levelAuthorName?: string; uploaderName?: string; coverURL?: string; }; function asRecord(v: unknown): Record | null { return v != null && typeof v === "object" && !Array.isArray(v) ? (v as Record) : null; } async function fetchJson(url: string): Promise { const res = await fetch(url); if (!res.ok) { throw new Error(`BeatSaver request failed ${res.status} ${url}`); } return res.json(); } function pickLatestVersionHashes( versions: unknown, ): { hash: string; coverURL?: string } | null { if (!Array.isArray(versions) || versions.length === 0) return null; const last = versions[versions.length - 1]; const vr = asRecord(last); if (!vr) return null; const h = vr["hash"]; const coverURL = vr["coverURL"]; return { hash: typeof h === "string" ? h : "", coverURL: typeof coverURL === "string" ? coverURL : undefined, }; } export function mapBeatSaverResponseToMeta( data: unknown, ): BeatSaverMapMeta | null { const r = asRecord(data); if (!r) return null; const meta = asRecord(r["metadata"]); const uid = asRecord(r["uploader"]); const vhash = pickLatestVersionHashes(r["versions"]); const id = r["id"]; if (!vhash?.hash || typeof id !== "string") return null; return { id, name: typeof r["name"] === "string" ? r["name"] : undefined, hash: vhash.hash, songName: meta?.["songName"] as string | undefined, songSubName: meta?.["songSubName"] as string | undefined, levelAuthorName: meta?.["levelAuthorName"] as string | undefined, uploaderName: uid?.["name"] as string | undefined, coverURL: vhash.coverURL, }; } export async function fetchBeatSaverByKey( key: string, ): Promise { const trimmed = key.trim().toLowerCase(); if (!trimmed) return null; const data = await fetchJson( `${BEATSAVER_BASE}/maps/id/${encodeURIComponent(trimmed)}`, ); return mapBeatSaverResponseToMeta(data); } export async function fetchBeatSaverByHash( hash: string, ): Promise { const h = hash.trim().toLowerCase(); if (!h) return null; const data = await fetchJson( `${BEATSAVER_BASE}/maps/hash/${encodeURIComponent(h)}`, ); return mapBeatSaverResponseToMeta(data); } /** Strip Twitch-style `!bsr` prefix when pasting a map key (e.g. `!bsr 43a47` → `43a47`). */ export function normalizeBeatSaverLookupInput(input: string): string { return input.trim().replace(/^!bsr\s*/i, "").trim(); } export async function resolveBeatSaverMeta( input: string, ): Promise { const s = normalizeBeatSaverLookupInput(input); if (!s) return null; if (/^[a-f0-9]{40}$/i.test(s)) { return await fetchBeatSaverByHash(s); } return await fetchBeatSaverByKey(s); }