101 lines
3.2 KiB
JavaScript
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>`;
|
|
}
|
|
}
|