Files
client/greyscale/src/lyrics.js
T
2026-04-17 16:52:51 -04:00

101 lines
3.2 KiB
JavaScript

import { escapeHtml } from './utils.js';
function parseLRC(lrcText) {
const lines = String(lrcText || '')
.split('\n')
.map((line) => line.trim())
.filter(Boolean);
const parsed = [];
for (const line of lines) {
const matches = [...line.matchAll(/\[(\d{1,2}):(\d{2})(?:\.(\d{1,3}))?\]/g)];
if (matches.length === 0) continue;
const text = line.replace(/\[[^\]]+\]/g, '').trim();
for (const m of matches) {
const mm = Number(m[1] || 0);
const ss = Number(m[2] || 0);
const ms = Number((m[3] || '0').padEnd(3, '0'));
const time = mm * 60 + ss + ms / 1000;
parsed.push({ time, text });
}
}
parsed.sort((a, b) => a.time - b.time);
return parsed;
}
export class LyricsManager {
constructor() {
this.cache = new Map();
}
async fetchLyrics(track) {
const cacheKey = String(track?.id || `${track?.artistName || ''}:${track?.title || ''}`);
if (this.cache.has(cacheKey)) return this.cache.get(cacheKey);
const artist = track?.artistName || track?.artist?.name || '';
const title = track?.title || '';
const primaryUrl = `https://lrclib.net/api/get?artist_name=${encodeURIComponent(artist)}&track_name=${encodeURIComponent(title)}`;
try {
const res = await fetch(primaryUrl);
if (res.ok) {
const data = await res.json();
const syncedText = data?.syncedLyrics || '';
const plainText = data?.plainLyrics || '';
const lines = syncedText ? parseLRC(syncedText) : [];
const payload = {
synced: lines.length > 0,
lines,
plain: plainText || (lines.map((l) => l.text).join('\n') || ''),
};
this.cache.set(cacheKey, payload);
return payload;
}
} catch {
// fall through
}
const fallback = { synced: false, lines: [], plain: 'No lyrics found.' };
this.cache.set(cacheKey, fallback);
return fallback;
}
getCurrentLineIndex(currentTime, lines) {
if (!Array.isArray(lines) || lines.length === 0) return -1;
let lo = 0;
let hi = lines.length - 1;
let ans = -1;
while (lo <= hi) {
const mid = Math.floor((lo + hi) / 2);
if (lines[mid].time <= currentTime) {
ans = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return ans;
}
renderLyrics(payload, activeIndex = -1) {
if (!payload) return '<div class="lyrics-empty">No lyrics loaded.</div>';
if (payload.synced && payload.lines.length > 0) {
return payload.lines
.map((line, idx) => {
const cls = idx === activeIndex ? 'lyric-line active' : 'lyric-line';
return `<button class="${cls}" data-time="${line.time}">${escapeHtml(line.text || '...')}</button>`;
})
.join('');
}
return `<pre class="lyrics-plain">${escapeHtml(payload.plain || 'No lyrics found.')}</pre>`;
}
}