fix: lyrics race condition, PC scroll, artwork matching, accidental artist/album taps
- nowplaying: add cancellation token to _loadNpLyrics to prevent stale async calls overwriting newer results (synced→plain race condition) - nowplaying: replace scrollIntoView with explicit container.scrollTo to prevent page-level scroll on desktop (topbar shifting / gap at edge) - backend: remove results[0] fallback in lookupItunesTrackMeta — only use exact title+artist match to avoid embedding wrong artwork - components/views: makeSongItem accepts noArtistLink/noAlbumLink opts; album-detail disables both, playlist-detail disables artist link Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
14e8226d1d
commit
9da48b88d8
4 files changed
+27
-9
No files matched your search
@@ -145,7 +145,7 @@ async function lookupItunesTrackMeta(artist, title) {
|
|||||||
const match = results.find(t =>
|
const match = results.find(t =>
|
||||||
norm(t.trackName) === norm(title) &&
|
norm(t.trackName) === norm(title) &&
|
||||||
norm(t.artistName).split(' ')[0] === norm(artist).split(' ')[0]
|
norm(t.artistName).split(' ')[0] === norm(artist).split(' ')[0]
|
||||||
) || results[0];
|
);
|
||||||
if (!match) return {};
|
if (!match) return {};
|
||||||
return {
|
return {
|
||||||
album: match.collectionName || '',
|
album: match.collectionName || '',
|
||||||
|
|||||||
+10
-5
@@ -27,19 +27,24 @@ export function makeAlbumCard(a) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ─── SONG LIST ITEM (library) ───
|
// ─── SONG LIST ITEM (library) ───
|
||||||
export function makeSongItem(s, idx, list) {
|
export function makeSongItem(s, idx, list, { noArtistLink = false, noAlbumLink = false } = {}) {
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
if(s.starred && !s._starred) s._starred = true;
|
if(s.starred && !s._starred) s._starred = true;
|
||||||
el.className = 'song-item' + (S.queue[S.qIdx]?.id === s.id ? ' playing' : '');
|
el.className = 'song-item' + (S.queue[S.qIdx]?.id === s.id ? ' playing' : '');
|
||||||
const cv = coverUrl(s.coverArt, 60);
|
const cv = coverUrl(s.coverArt, 60);
|
||||||
|
const artistHtml = s.artistId||s.artist
|
||||||
|
? (noArtistLink
|
||||||
|
? `<span style="color:var(--text2)">${esc(s.artist||'')}</span>`
|
||||||
|
: `<span class="song-link" onclick="event.stopPropagation();s_goArtist(event,'${esc(s.artist||'')}','${s.artistId||''}')">${esc(s.artist||'')}</span>`)
|
||||||
|
: '';
|
||||||
|
const albumHtml = !noAlbumLink && (s.albumId||s.album)
|
||||||
|
? `<span style="color:var(--text3)"> · </span><span class="song-link" onclick="event.stopPropagation();s_goAlbum(event,'${s.albumId||''}','${esc(s.album||'')}','${esc(s.artist||'')}','${s.artistId||''}')">${esc(s.album||'')}</span>`
|
||||||
|
: '';
|
||||||
el.innerHTML = `<div class="song-num">${idx+1}</div>
|
el.innerHTML = `<div class="song-num">${idx+1}</div>
|
||||||
<div class="song-thumb">${cv ? `<img src="${cv}" loading="lazy" onerror="this.parentNode.innerHTML='<div class=\\'no-art\\'>♪</div>'">` : '<div class="no-art">♪</div>'}</div>
|
<div class="song-thumb">${cv ? `<img src="${cv}" loading="lazy" onerror="this.parentNode.innerHTML='<div class=\\'no-art\\'>♪</div>'">` : '<div class="no-art">♪</div>'}</div>
|
||||||
<div class="song-info">
|
<div class="song-info">
|
||||||
<div class="song-title">${esc(s.title)}</div>
|
<div class="song-title">${esc(s.title)}</div>
|
||||||
<div class="song-artist">
|
<div class="song-artist">${artistHtml}${albumHtml}</div>
|
||||||
${s.artistId||s.artist ? `<span class="song-link" onclick="event.stopPropagation();s_goArtist(event,'${esc(s.artist||'')}','${s.artistId||''}')">${esc(s.artist||'')}</span>` : ''}
|
|
||||||
${s.albumId||s.album ? `<span style="color:var(--text3)"> · </span><span class="song-link" onclick="event.stopPropagation();s_goAlbum(event,'${s.albumId||''}','${esc(s.album||'')}','${esc(s.artist||'')}','${s.artistId||''}')">${esc(s.album||'')}</span>` : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="song-meta">
|
<div class="song-meta">
|
||||||
<span class="song-dur">${formatDur(s.duration)}</span>
|
<span class="song-dur">${formatDur(s.duration)}</span>
|
||||||
|
|||||||
+14
-1
@@ -80,6 +80,7 @@ export function switchNpTab(tab) {
|
|||||||
// ─── LYRICS ───
|
// ─── LYRICS ───
|
||||||
let _lyricsLines = [];
|
let _lyricsLines = [];
|
||||||
let _lyricsSongId = null;
|
let _lyricsSongId = null;
|
||||||
|
let _lyricsLoadToken = 0;
|
||||||
|
|
||||||
const LYRICS_LOOKAHEAD_MS = 200;
|
const LYRICS_LOOKAHEAD_MS = 200;
|
||||||
|
|
||||||
@@ -125,11 +126,14 @@ async function _loadNpLyrics(songId) {
|
|||||||
_lyricsLines = [];
|
_lyricsLines = [];
|
||||||
content.innerHTML = '<div style="text-align:center;padding:60px 0;color:rgba(255,255,255,.3);font-size:14px">Lade Lyrics…</div>';
|
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 s = S.queue[S.qIdx];
|
||||||
const { getLyricsBySongId } = await import('./api.js');
|
const { getLyricsBySongId } = await import('./api.js');
|
||||||
|
if(token !== _lyricsLoadToken) return;
|
||||||
|
|
||||||
// 1. Try Navidrome structured lyrics
|
// 1. Try Navidrome structured lyrics
|
||||||
const lyrics = await getLyricsBySongId(songId);
|
const lyrics = await getLyricsBySongId(songId);
|
||||||
|
if(token !== _lyricsLoadToken) return;
|
||||||
const track = lyrics[0];
|
const track = lyrics[0];
|
||||||
if(track?.synced && track.line?.length) {
|
if(track?.synced && track.line?.length) {
|
||||||
content.innerHTML = '';
|
content.innerHTML = '';
|
||||||
@@ -149,6 +153,7 @@ async function _loadNpLyrics(songId) {
|
|||||||
...(s?.album ? [new URLSearchParams({ artist_name: firstArtist, track_name: s?.title||'' })] : []),
|
...(s?.album ? [new URLSearchParams({ artist_name: firstArtist, track_name: s?.title||'' })] : []),
|
||||||
];
|
];
|
||||||
for(const p of attempts) {
|
for(const p of attempts) {
|
||||||
|
if(token !== _lyricsLoadToken) return;
|
||||||
const r = await fetch(`https://lrclib.net/api/get?${p}`, { signal: AbortSignal.timeout(6000) });
|
const r = await fetch(`https://lrclib.net/api/get?${p}`, { signal: AbortSignal.timeout(6000) });
|
||||||
if(r.ok) {
|
if(r.ok) {
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
@@ -158,6 +163,7 @@ async function _loadNpLyrics(songId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
if(token !== _lyricsLoadToken) return;
|
||||||
|
|
||||||
content.innerHTML = '';
|
content.innerHTML = '';
|
||||||
if(lrcText) {
|
if(lrcText) {
|
||||||
@@ -194,7 +200,14 @@ export function updateLyricsHighlight(currentMs) {
|
|||||||
line.el.classList.toggle('active', i === activeIdx);
|
line.el.classList.toggle('active', i === activeIdx);
|
||||||
line.el.classList.toggle('past', i < activeIdx);
|
line.el.classList.toggle('past', i < activeIdx);
|
||||||
});
|
});
|
||||||
if(activeIdx >= 0) _lyricsLines[activeIdx].el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
if(activeIdx >= 0) {
|
||||||
|
const activeEl = _lyricsLines[activeIdx].el;
|
||||||
|
const container = document.getElementById('np-lyrics-view');
|
||||||
|
if(container) {
|
||||||
|
const target = activeEl.offsetTop - container.clientHeight / 2 + activeEl.clientHeight / 2;
|
||||||
|
container.scrollTo({ top: Math.max(0, target), behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onNpSongChange(songId) {
|
export function onNpSongChange(songId) {
|
||||||
|
|||||||
+2
-2
@@ -440,7 +440,7 @@ export async function loadAlbumDetail(album) {
|
|||||||
toast(`⬇ ${songs.length} Titel Download gestartet`);
|
toast(`⬇ ${songs.length} Titel Download gestartet`);
|
||||||
};
|
};
|
||||||
sl.innerHTML = '';
|
sl.innerHTML = '';
|
||||||
songs.forEach((s,i) => sl.appendChild(makeSongItem(s, i, songs)));
|
songs.forEach((s,i) => sl.appendChild(makeSongItem(s, i, songs, { noArtistLink: true, noAlbumLink: true })));
|
||||||
} catch(e) { sl.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; }
|
} catch(e) { sl.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,7 +492,7 @@ export async function loadPlaylistDetail(pl) {
|
|||||||
playQueue(shuffled, 0);
|
playQueue(shuffled, 0);
|
||||||
};
|
};
|
||||||
sl.innerHTML = '';
|
sl.innerHTML = '';
|
||||||
songs.forEach((s,i) => sl.appendChild(makeSongItem(s, i, songs)));
|
songs.forEach((s,i) => sl.appendChild(makeSongItem(s, i, songs, { noArtistLink: true })));
|
||||||
} catch(e) { sl.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; }
|
} catch(e) { sl.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user