feat(lyrics): prefetch on play + smooth measured-position autoscroll

Warm the lrclib cache the moment a track starts (Android PlayerController
onMediaItemTransition; web onNpSongChange) so opening the Lyrics tab is
instant. Extract a DOM-free fetch helper on web for reuse.

Rewrite Android lyrics autoscroll to center on the active line's measured
position in a LazyColumn instead of a fixed line-height estimate — removes
stutter and the downward drift on wrapped lines; active line sits at a
~40%-from-top reading anchor.

CHANGELOG: fold into 1.0; drop the now-fixed lyrics known-issue note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-06-26 00:59:51 +02:00
1 parent 7a5197b776
commit 211b8ec6c2
4 files changed
+87 -46

No files matched your search

+33 -16
View File
@@ -145,7 +145,6 @@ async function _loadNpLyrics(songId) {
}
_lyricsSongId = songId;
_lyricsLines = [];
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];
@@ -155,6 +154,28 @@ async function _loadNpLyrics(songId) {
const dur = s?.duration || 0;
const cacheKey = `${artist}|${title}`;
// Cache hit (warmed on play, or from a previous view) — render instantly, no placeholder flash
if(_lyricsCache.has(cacheKey)) {
content.innerHTML = '';
_showLyricsResult(content, _lyricsCache.get(cacheKey));
return;
}
content.innerHTML = '<div style="text-align:center;padding:60px 0;color:rgba(255,255,255,.3);font-size:14px">Lade Lyrics…</div>';
const result = await _fetchLyricsData(artist, title, album, dur);
if(token !== _lyricsLoadToken) return;
content.innerHTML = '';
_showLyricsResult(content, result);
}
/**
* Pure lrclib fetch cascade — no DOM. Populates _lyricsCache and returns the result.
* Shared by the Lyrics tab and by prefetchLyrics() so opening Lyrics feels instant.
*/
async function _fetchLyricsData(artist, title, album, dur) {
const cacheKey = `${artist}|${title}`;
if(_lyricsCache.has(cacheKey)) return _lyricsCache.get(cacheKey);
const SIG = () => AbortSignal.timeout(6000);
const safeJson = async (promise) => { try { const r = await promise; return r.ok ? r.json() : null; } catch { return null; } };
const bestMatch = (candidates, durSecs) => {
@@ -166,19 +187,12 @@ async function _loadNpLyrics(songId) {
return Math.abs(pick.duration - durSecs) <= 5 ? pick : (synced[0] || candidates[0]);
};
// Cache hit — instant
if(_lyricsCache.has(cacheKey)) {
const cached = _lyricsCache.get(cacheKey);
content.innerHTML = '';
_showLyricsResult(content, cached);
return;
}
let result = null;
// 1a. lrclib exact match with duration
const exactP = new URLSearchParams({ artist_name: artist, track_name: title });
if(album) exactP.set('album_name', album);
if(dur) exactP.set('duration', dur);
let result = null;
try {
const r = await fetch(`https://lrclib.net/api/get?${exactP}`, { signal: SIG() });
if(r.status === 200) {
@@ -199,8 +213,6 @@ async function _loadNpLyrics(songId) {
} catch {}
}
if(token !== _lyricsLoadToken) return;
// 2+3. lrclib search — artist+title and title-only in parallel (2 requests, not 3)
if(!result) {
const artistP = new URLSearchParams({ track_name: title });
@@ -214,12 +226,15 @@ async function _loadNpLyrics(songId) {
result = bestMatch(artistData, dur) ?? bestMatch(titleData, dur);
}
if(token !== _lyricsLoadToken) return;
if(token !== _lyricsLoadToken) return;
_lyricsCache.set(cacheKey, result ?? null);
content.innerHTML = '';
_showLyricsResult(content, result);
return result ?? null;
}
/** Warm the lyrics cache when a track starts, so opening the Lyrics tab is instant. */
function prefetchLyrics(song) {
if(!song || !song.title) return;
const artist = (song.artist || '').split(/[/,]/)[0].trim();
_fetchLyricsData(artist, song.title, song.album || '', song.duration || 0).catch(() => {});
}
function _showLyricsResult(content, result) {
@@ -268,6 +283,8 @@ export function onNpSongChange(songId) {
_lyricsLines = [];
const content = document.getElementById('np-lyrics-content');
if(content) content.innerHTML = '';
// Warm the cache the moment the song changes, so the Lyrics tab opens instantly.
prefetchLyrics(S.queue.find(x => x.id === songId) || S.queue[S.qIdx]);
if(S.npLyricsMode) _loadNpLyrics(songId);
}