From 9fe8656a21abb87331071ad01fbb853e1f549482 Mon Sep 17 00:00:00 2001 From: Honey Date: Thu, 21 May 2026 00:02:42 +0200 Subject: [PATCH] fix: resolve queue desync causing player bar / NP to show wrong song MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: addToQueue was sent to native twice for every radio top-up. fillRadioQueue() already sends addToQueue internally, but onTrackChanged and onQueueEnded also re-sent the same songs in their .then() callbacks, causing native to accumulate duplicates. With native having double the songs JS has, native indices no longer map to the correct JS queue entries. Also fixed: playQueue called fillRadioQueue before setQueue was dispatched, so addToQueue could land in the old native queue before setQueue wiped it. Changes: - fillRadioQueue: add _queueGen guard — bail if playQueue() replaced the queue while the network request was in flight - playQueue: increment _queueGen; move fillRadioQueue() inside the rAF after setQueue so addToQueue always lands in the correct native queue - onTrackChanged: remove .then() that re-sent addToQueue (double-send fix) - onQueueEnded: remove addToQueue re-send, keep only _nsBridge('play') - onPlaybackStateChanged: add safety-net sync when isPlaying transitions to true — catches any remaining edge cases where onTrackChanged missed Co-Authored-By: Claude Sonnet 4.6 --- src/player.js | 43 ++++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 13 deletions(-) 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));