diff --git a/src/player.js b/src/player.js index 00c066a..27607ce 100644 --- a/src/player.js +++ b/src/player.js @@ -134,6 +134,20 @@ window.NSPlayer = { document.getElementById('player-art')?.classList.toggle('spinning', state.isPlaying); if (navigator.mediaSession) navigator.mediaSession.playbackState = state.isPlaying ? 'playing' : 'paused'; + // Safety net: when playback starts, verify JS is showing the correct track. + // Catches any edge cases where onTrackChanged was missed or queue drifted. + if (state.isPlaying) { + try { + const live = JSON.parse(window.NSBridge.getState() || '{}'); + const liveIdx = live.currentIndex; + if (typeof liveIdx === 'number' && liveIdx !== S.qIdx && + liveIdx >= 0 && liveIdx < S.queue.length) { + S.qIdx = liveIdx; + const s = S.queue[liveIdx]; + if (s) { updatePlayerUI(s, true); onNpSongChange(s.id); } + } + } catch {} + } } } catch (e) {} }, @@ -158,15 +172,9 @@ window.NSPlayer = { updatePlayerUI(s, true); onNpSongChange(s.id); if (indexChanged) renderQueue(); - // Radio mode: top up the queue when running low; push new songs to native. - if (S.radioMode && S.queue.length - S.qIdx < 5) { - const prevLen = S.queue.length; - fillRadioQueue().then(() => { - S.queue.slice(prevLen).forEach(t => - _nsBridge('addToQueue', JSON.stringify(_trackToNative(t)), -1) - ); - }); - } + // Radio mode: top up the queue when running low. + // fillRadioQueue sends addToQueue to native internally — no re-send here. + if (S.radioMode && S.queue.length - S.qIdx < 5) fillRadioQueue(); } catch (e) {} }, @@ -200,10 +208,9 @@ window.NSPlayer = { setPlayIcon(false); if (S.radioMode) { const prevLen = S.queue.length; + // fillRadioQueue sends addToQueue to native internally — only trigger play here. fillRadioQueue().then(() => { - const newTracks = S.queue.slice(prevLen); - newTracks.forEach(t => _nsBridge('addToQueue', JSON.stringify(_trackToNative(t)), -1)); - if (newTracks.length > 0) _nsBridge('play'); + if (S.queue.length > prevLen) _nsBridge('play'); }); } }, @@ -272,8 +279,12 @@ function _clearPreload() { // ─── PLAYBACK ───────────────────────────────────────────────────────────── let _playGen = 0; +// Incremented on every playQueue() call so in-flight fillRadioQueue callbacks +// from the previous queue can detect they're stale and bail out. +let _queueGen = 0; export function playQueue(songs, startIdx) { + _queueGen++; S.queue = songs.map(s => ({...s})); S.qIdx = startIdx; _clearPreload(); @@ -291,8 +302,10 @@ export function playQueue(songs, startIdx) { requestAnimationFrame(() => { try { window.NSBridge.setQueue(trackListJson, startIdx, true); } catch (e) { console.warn('NSBridge.setQueue failed', e); } + // fillRadioQueue AFTER setQueue so addToQueue lands in the new native queue, + // not the old one that setQueue is about to wipe. + if (S.radioMode) fillRadioQueue(); }); - if (S.radioMode) fillRadioQueue(); return; } @@ -624,6 +637,7 @@ export async function toggleRadioMode() { export async function fillRadioQueue() { if(!S.radioMode) return; + const gen = _queueGen; try { const seed = S.queue[S.qIdx]; let newSongs = []; @@ -636,6 +650,9 @@ export async function fillRadioQueue() { const r = await api('getRandomSongs', '&size=20'); newSongs = r.randomSongs?.song || []; } + // Bail if playQueue() was called while we were awaiting — appending to a + // replaced queue would desync native (setQueue already wiped the old one). + if (gen !== _queueGen) return; // Deduplicate against songs already queued const existingIds = new Set(S.queue.map(s => s.id)); newSongs = newSongs.filter(s => !existingIds.has(s.id));