diff --git a/src/nowplaying.js b/src/nowplaying.js
index b8cb5be..3c59190 100644
--- a/src/nowplaying.js
+++ b/src/nowplaying.js
@@ -41,7 +41,7 @@ export function openNowPlaying() {
}, {passive:true});
overlay.addEventListener('touchmove', e => onMove(e.touches[0].clientY), {passive:true});
overlay.addEventListener('touchend', e => onEnd(e.changedTouches[0].clientY), {passive:true});
- handle.addEventListener('mousedown', e => { onStart(e.clientY); e.preventDefault(); });
+ handle?.addEventListener('mousedown', e => { onStart(e.clientY); e.preventDefault(); });
document.addEventListener('mousemove', e => onMove(e.clientY));
document.addEventListener('mouseup', e => onEnd(e.clientY));
}
@@ -114,6 +114,19 @@ function _renderSyncedLines(content, lines) {
updateLyricsHighlight(S.audio.currentTime * 1000);
}
+function _parseLrc(text) {
+ let offsetMs = 0;
+ const lines = [];
+ for(const raw of text.split('\n')) {
+ const offM = raw.match(/^\[offset:\s*([+-]?\d+)\s*\]/i);
+ if(offM) { offsetMs = parseInt(offM[1]); continue; }
+ const m = raw.match(/^\[(\d+):(\d+(?:\.\d+)?)\](.*)/);
+ if(m) lines.push({ start: Math.round((parseInt(m[1])*60 + parseFloat(m[2]))*1000), value: m[3].trim() });
+ }
+ if(offsetMs) lines.forEach(l => l.start = Math.max(0, l.start + offsetMs));
+ return lines.sort((a,b) => a.start - b.start).filter(l => l.value);
+}
+
async function _loadNpLyrics(songId) {
const content = document.getElementById('np-lyrics-content');
if(!content) return;
@@ -126,20 +139,49 @@ async function _loadNpLyrics(songId) {
content.innerHTML = '
Lade Lyrics…
';
const token = ++_lyricsLoadToken;
+ const s = S.queue[S.qIdx];
const { getLyricsBySongId } = await import('./api.js');
if(token !== _lyricsLoadToken) return;
+ // 1. Navidrome (plugin handles fetching + caching server-side)
const lyrics = await getLyricsBySongId(songId);
if(token !== _lyricsLoadToken) return;
const track = lyrics[0];
-
- content.innerHTML = '';
if(track?.synced && track.line?.length) {
+ content.innerHTML = '';
_renderSyncedLines(content, track.line.map(l => ({ start: l.start, value: l.value || '' })).filter(l => l.value));
return;
}
- const plain = track?.line?.length ? track.line.map(l => l.value).join('\n') : null;
+ // 2. Fallback: LRCLIB directly (synced LRC format)
+ const firstArtist = (s?.artist||'').split(/[/,]/)[0].trim();
+ let lrcText = null;
+ let lrclibPlain = null;
+ try {
+ const attempts = [
+ new URLSearchParams({ artist_name: firstArtist, track_name: s?.title||'', ...(s?.album ? { album_name: s.album } : {}) }),
+ ...(s?.album ? [new URLSearchParams({ artist_name: firstArtist, track_name: s?.title||'' })] : []),
+ ];
+ for(const p of attempts) {
+ if(token !== _lyricsLoadToken) return;
+ const r = await fetch(`https://lrclib.net/api/get?${p}`, { signal: AbortSignal.timeout(6000) });
+ if(r.ok) {
+ const d = await r.json();
+ lrcText = d.syncedLyrics || null;
+ lrclibPlain = d.plainLyrics || null;
+ break;
+ }
+ }
+ } catch {}
+ if(token !== _lyricsLoadToken) return;
+
+ content.innerHTML = '';
+ if(lrcText) {
+ const parsed = _parseLrc(lrcText);
+ if(parsed.length) { _renderSyncedLines(content, parsed); return; }
+ }
+
+ const plain = lrclibPlain || (track?.line?.length ? track.line.map(l => l.value).join('\n') : null);
if(plain) {
const el = document.createElement('div');
el.className = 'lyrics-plain';