mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
fix(queue): push edited queue when playback starts on paused client (#1133)
This commit is contained in:
@@ -62,7 +62,7 @@ import { toQueueItemRefs } from '../utils/library/queueItemRef';
|
||||
import { getQueueTracksView, resolveQueueTrack } from '../utils/library/queueTrackView';
|
||||
import { seedQueueResolver } from '../utils/library/queueTrackResolver';
|
||||
import { promoteCompletedStreamToHotCache } from './promoteStreamCache';
|
||||
import { syncQueueToServer } from './queueSync';
|
||||
import { pushQueueOnPlaybackStart } from './queueSync';
|
||||
import { playListenSessionFinalize } from './playListenSession';
|
||||
import { pushQueueUndoFromGetter } from './queueUndo';
|
||||
import { stopRadio } from './radioPlayer';
|
||||
@@ -543,7 +543,7 @@ export function runPlayTrack(
|
||||
}));
|
||||
});
|
||||
}
|
||||
syncQueueToServer(get().queueItems, scopedTrack, initialTime);
|
||||
pushQueueOnPlaybackStart(get().queueItems, scopedTrack, initialTime);
|
||||
touchHotCacheOnPlayback(scopedTrack.id, playbackCacheSid);
|
||||
};
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
flushQueueSyncToServer,
|
||||
getLastQueueHeartbeatAt,
|
||||
hasPendingQueueSync,
|
||||
pushQueueOnPlaybackStart,
|
||||
syncQueueToServer,
|
||||
} from './queueSync';
|
||||
import {
|
||||
@@ -115,6 +116,26 @@ describe('syncQueueToServer (debounced)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('pushQueueOnPlaybackStart', () => {
|
||||
const queue = [ref('a'), ref('b')];
|
||||
|
||||
it('flushes immediately and clears idle pull suspension when locally edited', async () => {
|
||||
syncQueueToServer(queue, track('a'), 30);
|
||||
expect(hasPendingQueueSync()).toBe(true);
|
||||
pushQueueOnPlaybackStart(queue, track('a'), 42);
|
||||
expect(hasPendingQueueSync()).toBe(false);
|
||||
await vi.runAllTimersAsync();
|
||||
expect(savePlayQueueMock).toHaveBeenCalledWith(['a', 'b'], 'a', 42000, 'srv-a');
|
||||
expect(isIdleQueuePullSuspended()).toBe(false);
|
||||
});
|
||||
|
||||
it('debounces when idle pull is not suspended', () => {
|
||||
pushQueueOnPlaybackStart(queue, track('a'), 12);
|
||||
expect(hasPendingQueueSync()).toBe(true);
|
||||
expect(savePlayQueueMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('flushPlayQueueForServer', () => {
|
||||
it('flushes only the target server slice', async () => {
|
||||
playerState.queueItems = [ref('a', 'srv-a'), ref('b', 'b.test')];
|
||||
|
||||
+36
-1
@@ -8,7 +8,7 @@ import {
|
||||
} from '../utils/playback/playbackServer';
|
||||
import { filterQueueRefsForServerProfile } from '../utils/playback/trackServerScope';
|
||||
import { getPlaybackProgressSnapshot } from './playbackProgress';
|
||||
import { touchQueueMutationClock } from './queuePlaybackIdle';
|
||||
import { touchQueueMutationClock, isIdleQueuePullSuspended, resumeIdleQueuePull } from './queuePlaybackIdle';
|
||||
import { usePlayerStore } from './playerStore';
|
||||
|
||||
/**
|
||||
@@ -121,6 +121,41 @@ export function flushPlayQueuePosition(): Promise<void> {
|
||||
return flushQueueSyncToServer(s.queueItems, s.currentTrack, getPlaybackProgressSnapshot().currentTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* When the user edited the queue while paused, idle pull is suspended (yellow LED).
|
||||
* Starting playback makes this client authoritative — push the local queue immediately
|
||||
* and re-enable idle auto-pull (blocked anyway while `isPlaying`).
|
||||
*/
|
||||
export function pushQueueOnPlaybackStart(
|
||||
queue: QueueItemRef[],
|
||||
currentTrack: Track | null,
|
||||
currentTime: number,
|
||||
): void {
|
||||
if (!currentTrack || queue.length === 0) return;
|
||||
if (isIdleQueuePullSuspended()) {
|
||||
void flushQueueSyncToServer(queue, currentTrack, currentTime).then(() => {
|
||||
resumeIdleQueuePull();
|
||||
});
|
||||
return;
|
||||
}
|
||||
syncQueueToServer(queue, currentTrack, currentTime);
|
||||
}
|
||||
|
||||
export function flushLocalQueueWhenTakingPlayback(): Promise<void> {
|
||||
if (!isIdleQueuePullSuspended()) return Promise.resolve();
|
||||
const s = usePlayerStore.getState();
|
||||
if (s.currentRadio || !s.currentTrack || s.queueItems.length === 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return flushQueueSyncToServer(
|
||||
s.queueItems,
|
||||
s.currentTrack,
|
||||
getPlaybackProgressSnapshot().currentTime,
|
||||
).then(() => {
|
||||
resumeIdleQueuePull();
|
||||
});
|
||||
}
|
||||
|
||||
/** Test-only: drop the debounce + reset the heartbeat. */
|
||||
export function _resetQueueSyncForTest(): void {
|
||||
if (syncTimeout) {
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
import type { PlayerState } from './playerStoreTypes';
|
||||
import { resolveQueueTrack } from '../utils/library/queueTrackView';
|
||||
import { promoteCompletedStreamToHotCache } from './promoteStreamCache';
|
||||
import { syncQueueToServer } from './queueSync';
|
||||
import { pushQueueOnPlaybackStart, flushLocalQueueWhenTakingPlayback } from './queueSync';
|
||||
import { markPlaybackActive } from './queuePlaybackIdle';
|
||||
import { playbackReportPlaying } from './playbackReportSession';
|
||||
import { resumeRadio } from './radioPlayer';
|
||||
@@ -134,6 +134,7 @@ export function runResume(set: SetState, get: GetState): void {
|
||||
set({ isPlaying: true });
|
||||
// Mirror pause(): tell the server immediately, don't wait for `audio:playing`.
|
||||
playbackReportPlaying(currentTime);
|
||||
void flushLocalQueueWhenTakingPlayback();
|
||||
touchHotCacheOnPlayback(currentTrack.id, getPlaybackCacheServerKey());
|
||||
} else {
|
||||
// Engine has no loaded paused stream (app relaunch, or track ended and user
|
||||
@@ -199,7 +200,7 @@ export function runResume(set: SetState, get: GetState): void {
|
||||
console.error('[psysonic] audio_play (cold resume) failed:', err);
|
||||
set({ isPlaying: false });
|
||||
});
|
||||
syncQueueToServer(queueItems, trackToPlay, currentTime);
|
||||
pushQueueOnPlaybackStart(queueItems, trackToPlay, currentTime);
|
||||
}).catch(() => {
|
||||
if (getPlayGeneration() !== gen) return;
|
||||
// Fallback to currentTrack if fetch fails
|
||||
@@ -236,7 +237,7 @@ export function runResume(set: SetState, get: GetState): void {
|
||||
console.error('[psysonic] audio_play (cold resume) failed:', err);
|
||||
set({ isPlaying: false });
|
||||
});
|
||||
syncQueueToServer(queueItems, currentTrack, currentTime);
|
||||
pushQueueOnPlaybackStart(queueItems, currentTrack, currentTime);
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user