From e4cc93d3628c21886d1107eb634b86f4c877a7c6 Mon Sep 17 00:00:00 2001 From: Honey Date: Wed, 20 May 2026 19:03:42 +0200 Subject: [PATCH] fix: restore LRCLIB fallback for synced lyrics; fix np-drag-handle null crash Co-Authored-By: Claude Sonnet 4.6 --- src/nowplaying.js | 50 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) 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';