Slim song cards with an expand arrow, and lyrics
Feed and profile cards now show one slim line (small artwork, title, artist, Open button) with the progress bar and reactions. The arrow opens the rest: album, your other services, Share, and lyrics. Lyrics come from LRCLIB (open source; LRCLIB_URL can point at a self-hosted copy, or be empty to turn lyrics off). The server looks each song up once and caches it, including misses (retried after a week). Timed lyrics follow along with the song; otherwise the first lines show with "All lyrics". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
/** One line of time-synced lyrics; empty text is an instrumental gap. */
|
||||
export interface LyricLine {
|
||||
timeMs: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface LyricsDTO {
|
||||
/** Lines with timestamps, when the song has synced lyrics. */
|
||||
synced: LyricLine[] | null;
|
||||
/** Plain text, when there are no synced lyrics (or as a fallback). */
|
||||
plain: string | null;
|
||||
instrumental: boolean;
|
||||
/** Where they came from, for attribution. */
|
||||
source: { name: string; url: string };
|
||||
}
|
||||
|
||||
const TIME_TAG = /\[(\d{1,3}):(\d{1,2}(?:\.\d{1,3})?)\]/g;
|
||||
|
||||
/**
|
||||
* Parse LRC ("[01:23.45] words"). A line may carry several time tags; tags
|
||||
* that aren't times (like [ar:Artist]) are skipped. Sorted by time.
|
||||
*/
|
||||
export function parseLrc(lrc: string): LyricLine[] {
|
||||
const lines: LyricLine[] = [];
|
||||
for (const raw of lrc.split(/\r?\n/)) {
|
||||
const times = [...raw.matchAll(TIME_TAG)];
|
||||
if (times.length === 0) continue;
|
||||
const text = raw.replace(TIME_TAG, '').trim();
|
||||
for (const [, min, sec] of times) lines.push({ timeMs: Math.round((Number(min) * 60 + Number(sec)) * 1000), text });
|
||||
}
|
||||
return lines.sort((a, b) => a.timeMs - b.timeMs);
|
||||
}
|
||||
|
||||
/** Index of the line being sung at `positionMs`, or -1 before the first one. */
|
||||
export function currentLyricIndex(lines: LyricLine[], positionMs: number): number {
|
||||
let lo = 0;
|
||||
let hi = lines.length - 1;
|
||||
let found = -1;
|
||||
while (lo <= hi) {
|
||||
const mid = (lo + hi) >> 1;
|
||||
if (lines[mid]!.timeMs <= positionMs) {
|
||||
found = mid;
|
||||
lo = mid + 1;
|
||||
} else hi = mid - 1;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
Reference in New Issue
Block a user