mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
refactor(player): E.25 — extract audio-orchestration cluster (#589)
Two thematic cuts in one PR:
- `src/store/queueUndoAudioRestore.ts` — `queueUndoRestoreAudioEngine`
(~70 LOC). Reload the Rust audio engine to match a queue-undo
snapshot: audio_play with snapshot track params, optional
audio_seek to the snapshot position, audio_pause if the snapshot
captured a paused state. Generation-guard bails on concurrent
playTrack. Drives recordEnginePlayUrl, setDeferHotCachePrefetch,
and touchHotCacheOnPlayback.
- `src/store/loudnessPrefetch.ts` — `prefetchLoudnessForEnqueuedTracks`.
Warms the loudness cache for the current track + next-N window
after a bulk enqueue, no-op when normalization isn't loudness.
Both file-private; no caller-side changes outside playerStore's own
imports. Removes the unused `collectLoudnessBackfillWindowTrackIds`
import that was only feeding the moved prefetch helper.
11 tests across the two modules pin the orchestration: audio_play
payload + seek-when-near-zero + wantPlaying=false → audio_pause +
generation-mismatch bail + .finally clears the hot-cache gate even
on errors; engine guard + window forwarding + sync-flag for prefetch.
playerStore 2865 → 2779 LOC.
This commit is contained in:
committed by
GitHub
parent
14bdcde33f
commit
a5aadeea67
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* `prefetchLoudnessForEnqueuedTracks` warms the loudness cache for the
|
||||
* current + next-N tracks after a bulk enqueue. Tests pin the engine
|
||||
* guard, the window collection, and the no-sync-engine flag on each
|
||||
* refresh call.
|
||||
*/
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { Track } from './playerStore';
|
||||
|
||||
const hoisted = vi.hoisted(() => {
|
||||
const auth = { normalizationEngine: 'loudness' as 'off' | 'replaygain' | 'loudness' };
|
||||
const player = { currentTrack: null as Track | null };
|
||||
return {
|
||||
auth,
|
||||
player,
|
||||
refreshMock: vi.fn(async () => undefined),
|
||||
collectMock: vi.fn((_q: Track[], _i: number, _c: Track | null): string[] => []),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('./authStore', () => ({ useAuthStore: { getState: () => hoisted.auth } }));
|
||||
vi.mock('./playerStore', () => ({
|
||||
usePlayerStore: { getState: () => hoisted.player },
|
||||
}));
|
||||
vi.mock('./loudnessRefresh', () => ({
|
||||
refreshLoudnessForTrack: hoisted.refreshMock,
|
||||
}));
|
||||
vi.mock('./loudnessBackfillWindow', () => ({
|
||||
collectLoudnessBackfillWindowTrackIds: hoisted.collectMock,
|
||||
}));
|
||||
|
||||
import { prefetchLoudnessForEnqueuedTracks } from './loudnessPrefetch';
|
||||
|
||||
function track(id: string): Track {
|
||||
return { id, title: id, artist: 'A', album: 'X', albumId: 'X', duration: 100 };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
hoisted.auth.normalizationEngine = 'loudness';
|
||||
hoisted.player.currentTrack = null;
|
||||
hoisted.refreshMock.mockClear();
|
||||
hoisted.collectMock.mockReset();
|
||||
hoisted.collectMock.mockReturnValue([]);
|
||||
});
|
||||
|
||||
describe('prefetchLoudnessForEnqueuedTracks', () => {
|
||||
it("is a no-op when engine isn't loudness", () => {
|
||||
hoisted.auth.normalizationEngine = 'off';
|
||||
hoisted.collectMock.mockReturnValueOnce(['t1']);
|
||||
prefetchLoudnessForEnqueuedTracks([track('t1')], 0);
|
||||
expect(hoisted.refreshMock).not.toHaveBeenCalled();
|
||||
expect(hoisted.collectMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('forwards each window id to refreshLoudnessForTrack with syncPlayingEngine=false', () => {
|
||||
hoisted.collectMock.mockReturnValueOnce(['t1', 't2', 't3']);
|
||||
prefetchLoudnessForEnqueuedTracks([track('t1'), track('t2'), track('t3')], 0);
|
||||
expect(hoisted.refreshMock).toHaveBeenCalledTimes(3);
|
||||
expect(hoisted.refreshMock).toHaveBeenCalledWith('t1', { syncPlayingEngine: false });
|
||||
expect(hoisted.refreshMock).toHaveBeenCalledWith('t2', { syncPlayingEngine: false });
|
||||
expect(hoisted.refreshMock).toHaveBeenCalledWith('t3', { syncPlayingEngine: false });
|
||||
});
|
||||
|
||||
it('passes the queue + currentTrack through to the window collector', () => {
|
||||
hoisted.player.currentTrack = track('cur');
|
||||
const q = [track('cur'), track('next')];
|
||||
prefetchLoudnessForEnqueuedTracks(q, 0);
|
||||
expect(hoisted.collectMock).toHaveBeenCalledWith(q, 0, hoisted.player.currentTrack);
|
||||
});
|
||||
|
||||
it('handles empty window list gracefully', () => {
|
||||
hoisted.collectMock.mockReturnValueOnce([]);
|
||||
prefetchLoudnessForEnqueuedTracks([], 0);
|
||||
expect(hoisted.refreshMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user