9d88d70be0
Full-screen overlay expanded from the persistent player: big artwork or a Lyrics tab. Synced (LRC) lyrics highlight the active line by playback position, auto-scroll, and seek on tap; plain text is the fallback; missing lyrics offers a re-fetch. Adds lyrics endpoint + LRC parser + isNowPlayingOpen player state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
51 lines
1.6 KiB
TypeScript
51 lines
1.6 KiB
TypeScript
/** One parsed LRC line: a timestamp (seconds) and its text. */
|
|
export interface LrcLine {
|
|
time: number;
|
|
text: string;
|
|
}
|
|
|
|
const TAG = /\[(\d{1,2}):(\d{2})(?:[.:](\d{1,3}))?\]/g;
|
|
|
|
/**
|
|
* Parse an LRC string into time-sorted lines. A single source line may carry
|
|
* several timestamps (repeated lyric) — each becomes its own entry. Metadata
|
|
* tags (`[ar:…]`, `[ti:…]`) and untimed lines are skipped; empty-text lines are
|
|
* kept so the active-line highlight advances through instrumental gaps.
|
|
*/
|
|
export function parseLrc(lrc: string): LrcLine[] {
|
|
const lines: LrcLine[] = [];
|
|
for (const raw of lrc.split(/\r?\n/)) {
|
|
const stamps = [...raw.matchAll(TAG)];
|
|
if (stamps.length === 0) continue;
|
|
const text = raw.replace(/\[[^\]]*\]/g, '').trim();
|
|
for (const m of stamps) {
|
|
const min = Number(m[1]);
|
|
const sec = Number(m[2]);
|
|
const frac = m[3] ? Number(m[3].padEnd(3, '0')) / 1000 : 0;
|
|
lines.push({ time: min * 60 + sec + frac, text });
|
|
}
|
|
}
|
|
return lines.sort((a, b) => a.time - b.time);
|
|
}
|
|
|
|
/**
|
|
* Index of the active line for a playback position (seconds) — the last line
|
|
* whose timestamp has passed. Returns -1 before the first line. Assumes `lines`
|
|
* is time-sorted (as `parseLrc` returns).
|
|
*/
|
|
export function activeLineIndex(lines: LrcLine[], position: number): number {
|
|
let lo = 0;
|
|
let hi = lines.length - 1;
|
|
let result = -1;
|
|
while (lo <= hi) {
|
|
const mid = (lo + hi) >> 1;
|
|
if (lines[mid].time <= position) {
|
|
result = mid;
|
|
lo = mid + 1;
|
|
} else {
|
|
hi = mid - 1;
|
|
}
|
|
}
|
|
return result;
|
|
}
|