fix(queue): prevent ghost next_track after context switch
Auto-advance from a previous track could arrive after the user switched to a new album/playlist, advancing the new queue's index and skipping a track (observed: Sorceress track skipped). Added queue epoch counter that increments on setQueue/clearQueue. nextTrackGuarded() captures epoch before the invoke and discards the result if the queue changed while in-flight. Applied to all auto-advance paths (track-end, gapless transition) but not to manual skip (user-initiated). cc
This commit is contained in:
2 files changed
+32
-3
No files matched your search
@@ -368,11 +368,24 @@ export async function addTracksToQueueNext(tracks: BackendQueueTrack[]): Promise
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue epoch counter — incremented on every setQueue/clearQueue call.
|
||||
* Used by nextTrackGuarded() to discard stale auto-advance results
|
||||
* that arrive after a context switch (album/playlist change).
|
||||
*/
|
||||
let queueEpoch = 0;
|
||||
|
||||
/** Current queue epoch (read-only for callers). */
|
||||
export function getQueueEpoch(): number {
|
||||
return queueEpoch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set queue with new tracks (V2)
|
||||
*/
|
||||
export async function setQueue(tracks: BackendQueueTrack[], startIndex: number, clearLocal = true): Promise<boolean> {
|
||||
try {
|
||||
queueEpoch++;
|
||||
await invoke('v2_set_queue', { tracks, startIndex });
|
||||
if (clearLocal) {
|
||||
localTrackIds = new Set();
|
||||
@@ -390,6 +403,7 @@ export async function setQueue(tracks: BackendQueueTrack[], startIndex: number,
|
||||
*/
|
||||
export async function clearQueue(): Promise<boolean> {
|
||||
try {
|
||||
queueEpoch++;
|
||||
await invoke('v2_clear_queue');
|
||||
return true;
|
||||
} catch (err) {
|
||||
@@ -422,6 +436,20 @@ export async function nextTrack(): Promise<BackendQueueTrack | null> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next track, but discard the result if the queue changed while
|
||||
* the invoke was in-flight (prevents ghost auto-advance after context switch).
|
||||
*/
|
||||
export async function nextTrackGuarded(): Promise<BackendQueueTrack | null> {
|
||||
const epochBefore = queueEpoch;
|
||||
const result = await nextTrack();
|
||||
if (queueEpoch !== epochBefore) {
|
||||
console.warn('[Queue] Discarding stale next_track result (queue changed during invoke)');
|
||||
return null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get previous track from queue (V2)
|
||||
*/
|
||||
|
||||
@@ -221,6 +221,7 @@
|
||||
clearQueue,
|
||||
playQueueIndex,
|
||||
nextTrack,
|
||||
nextTrackGuarded,
|
||||
previousTrack,
|
||||
moveQueueTrack,
|
||||
setLocalTrackIds,
|
||||
@@ -4369,7 +4370,7 @@
|
||||
return;
|
||||
}
|
||||
const previousTrackId = currentTrack?.id ?? null;
|
||||
const nextTrackResult = await nextTrack();
|
||||
const nextTrackResult = await nextTrackGuarded();
|
||||
if (nextTrackResult) {
|
||||
// Defensive fallback for issue #80:
|
||||
// if backend returns same track on auto-advance while repeat-one is off,
|
||||
@@ -4383,7 +4384,7 @@
|
||||
'[Player] Auto-advance returned same track id, forcing one extra nextTrack()',
|
||||
previousTrackId
|
||||
);
|
||||
const forcedNext = await nextTrack();
|
||||
const forcedNext = await nextTrackGuarded();
|
||||
if (forcedNext && forcedNext.id !== previousTrackId) {
|
||||
await playQueueTrack(forcedNext);
|
||||
return;
|
||||
@@ -4496,7 +4497,7 @@
|
||||
if (qconnectSuppressLocalPlaybackAutomation) return;
|
||||
console.log('[Gapless] Handling transition to track', trackId);
|
||||
// Advance the queue to match backend state
|
||||
const advanced = await nextTrack();
|
||||
const advanced = await nextTrackGuarded();
|
||||
if (advanced && advanced.id === trackId) {
|
||||
// Queue advanced successfully — update UI metadata
|
||||
await playQueueTrack(advanced, undefined, true);
|
||||
|
||||
Reference in new issue
Block a user