feat: Android Auto, radio mode, lyrics overhaul + web lyrics cleanup

Android:
- fix: Android Auto credentials never set on MusicService → artwork/stream null
- feat: Android Auto tabs reduced to Zuletzt gespielt, Favoriten, Playlists
- feat: radio mode (getSimilarSongs2 → getRandomSongs fallback, <5 songs refill)
- fix: lyrics now use Navidrome getLyricsBySongId plugin exclusively
- fix: lyrics scroll rewritten with Column+ScrollState absolute animateScrollTo
- fix: lyrics recomposition lag — extracted LyricsLineItem + key(idx) per line
- chore: remove dead LrcLibClient (direct lrclib bypassed Navidrome plugin)

Web:
- fix: lyrics now use Navidrome getLyricsBySongId only, removed direct lrclib fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-06-15 20:30:20 +02:00
1 parent 8c8f03503b
commit 7e20062995
18 files changed
+700 -243

No files matched your search

+14 -42
View File
@@ -147,61 +147,33 @@ async function _loadNpLyrics(songId) {
content.innerHTML = '<div style="text-align:center;padding:60px 0;color:rgba(255,255,255,.3);font-size:14px">Lade Lyrics…</div>';
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)
// Navidrome plugin (getLyricsBySongId) — plugin handles source lookup + caching
const lyrics = await getLyricsBySongId(songId);
if(token !== _lyricsLoadToken) return;
const track = lyrics[0];
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;
}
// 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;
if(lrcText || lrclibPlain) break;
}
}
} catch {}
if(token !== _lyricsLoadToken) return;
content.innerHTML = '';
if(lrcText) {
const parsed = _parseLrc(lrcText);
if(parsed.length) { _renderSyncedLines(content, parsed); return; }
}
const synced = lyrics.find(t => t.synced && t.line?.length);
const plain = lyrics.find(t => t.line?.length);
const plain = lrclibPlain || (track?.line?.length ? track.line.map(l => l.value).join('\n') : null);
if(synced) {
_renderSyncedLines(content, synced.line.map(l => ({ start: l.start, value: l.value || '' })).filter(l => l.value));
return;
}
if(plain) {
const el = document.createElement('div');
el.className = 'lyrics-plain';
el.textContent = plain;
content.appendChild(el);
} else {
const el = document.createElement('div');
el.className = 'lyrics-none';
el.style.cssText = 'text-align:center;padding:60px 0;color:rgba(255,255,255,.3);font-size:14px';
el.textContent = 'Keine Lyrics verfügbar';
el.textContent = plain.line.map(l => l.value).join('\n');
content.appendChild(el);
return;
}
const el = document.createElement('div');
el.className = 'lyrics-none';
el.style.cssText = 'text-align:center;padding:60px 0;color:rgba(255,255,255,.3);font-size:14px';
el.textContent = 'Keine Lyrics verfügbar';
content.appendChild(el);
}
export function updateLyricsHighlight(currentMs) {