Files
mcma-webui/src/lib/lrc.ts
T
Цвылев Александр Вадимович 9d88d70be0 feat(player): Now Playing overlay + synced lyrics panel (A1/A11, §6.7)
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>
2026-07-28 21:04:39 +03:00

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;
}