// Synced lyrics from LRCLIB, same lookup as the iOS app: exact match first, then a search. const cache = new Map(); function parse(lrc) { const out = []; for (const line of (lrc || "").split("\n")) { const stamps = [...line.matchAll(/\[(\d+):(\d+(?:\.\d+)?)\]/g)]; if (!stamps.length) continue; const text = line.replace(/\[[^\]]*\]/g, "").trim(); for (const m of stamps) out.push({ t: +m[1] * 60 + +m[2], text }); } return out.sort((a, b) => a.t - b.t); } // -> {lines: [{t, text}] | null, plain: string | null, instrumental: bool} or null if not found. export async function lyricsFor(t) { const id = t.id; if (cache.has(id)) return cache.get(id); const p = (async () => { const q = { track_name: t.title, artist_name: t.artist, album_name: t.album }; if (t.duration) q.duration = Math.round(t.duration / 1000); const get = async (path, params) => { const r = await fetch(`https://lrclib.net${path}?${new URLSearchParams(params)}`, { signal: AbortSignal.timeout(8000) }); return r.ok ? r.json() : null; }; let hit = await get("/api/get", q).catch(() => null); if (!hit) { const list = await get("/api/search", { track_name: t.title, artist_name: t.artist }).catch(() => null) || []; hit = list.find(x => x.syncedLyrics) || list[0] || null; } if (!hit) return null; return { lines: hit.syncedLyrics ? parse(hit.syncedLyrics) : null, plain: hit.plainLyrics || null, instrumental: !!hit.instrumental }; })(); cache.set(id, p); const res = await p; if (!res) cache.delete(id); return res; } // Index of the line playing at `sec`, or -1 before the first. export function lineAt(lines, sec) { let lo = 0, hi = lines.length - 1, ans = -1; while (lo <= hi) { const mid = (lo + hi) >> 1; if (lines[mid].t <= sec + 0.15) { ans = mid; lo = mid + 1; } else hi = mid - 1; } return ans; }