Files
Psychotoxical-psysonic/src/store/queueUndoAudioRestore.ts
T
cucadmuh 45e0e1206f fix(playback): pin queue playback to source server when browsing another library (#717)
* fix(playback): pin queue streams, cover art, and library links to queue server

When the active server changes while a queue from another server is playing,
keep streams and UI on queueServerId; switch back for artist/album links and
queue or player-bar context menus.

* fix(playback): switch to queue server when opening Now Playing

Ensure active server matches queueServerId before Subsonic fetches on the
Now Playing page, mobile player route, and queue info panel; scope caches
by server id.

* docs(credits): mention Now Playing in PR #717 contribution line

* fix(playback): route scrobble and queue sync to queue server

Address PR review: apiForServer for scrobble/now-playing/savePlayQueue,
clear queueServerId on server removal, mini-player queueServerId sync,
block cross-server enqueue with toast, and regression tests.
2026-05-15 16:08:41 +03:00

98 lines
4.0 KiB
TypeScript

import type { Track } from './playerStoreTypes';
import { invoke } from '@tauri-apps/api/core';
import { setDeferHotCachePrefetch } from '../utils/cache/hotCacheGate';
import { getPlaybackServerId } from '../utils/playback/playbackServer';
import { resolvePlaybackUrl } from '../utils/playback/resolvePlaybackUrl';
import { resolveReplayGainDb } from '../utils/audio/resolveReplayGainDb';
import { useAuthStore } from './authStore';
import { getPlayGeneration, setIsAudioPaused } from './engineState';
import { touchHotCacheOnPlayback } from './hotCacheTouch';
import { isReplayGainActive, loudnessGainDbForEngineBind } from './loudnessGainCache';
import { playbackSourceHintForResolvedUrl, recordEnginePlayUrl } from './playbackUrlRouting';
import { usePlayerStore } from './playerStore';
/**
* Reload the Rust audio engine to match a queue-undo snapshot. Zustand
* alone can rewrite the queue + currentTrack, but the engine is still
* playing whatever cold-started before the undo — so we need a full
* `audio_play` (+ optional `audio_seek` to the snapshot position) to
* line the audible playback back up with the restored UI state.
*
* Captures the play-generation at start so a later concurrent `playTrack`
* (e.g. user clicks another track) invalidates the seek/pause follow-up
* without clobbering the new engine state.
*/
export function queueUndoRestoreAudioEngine(opts: {
generation: number;
track: Track;
queue: Track[];
queueIndex: number;
atSeconds: number;
wantPlaying: boolean;
}): void {
const { generation, track, queue, queueIndex, atSeconds, wantPlaying } = opts;
const authState = useAuthStore.getState();
const vol = usePlayerStore.getState().volume;
const coldPrev = queueIndex > 0 ? queue[queueIndex - 1] : null;
const coldNext = queueIndex + 1 < queue.length ? queue[queueIndex + 1] : null;
const replayGainDb = resolveReplayGainDb(
track, coldPrev, coldNext,
isReplayGainActive(), authState.replayGainMode,
);
const replayGainPeak = isReplayGainActive() ? (track.replayGainPeak ?? null) : null;
const playbackSid = getPlaybackServerId();
const url = resolvePlaybackUrl(track.id, playbackSid);
recordEnginePlayUrl(track.id, url);
usePlayerStore.setState({
currentPlaybackSource: playbackSourceHintForResolvedUrl(track.id, playbackSid, url),
});
const keepPreloadHint = usePlayerStore.getState().enginePreloadedTrackId === track.id;
setDeferHotCachePrefetch(true);
invoke('audio_play', {
url,
volume: vol,
durationHint: track.duration,
replayGainDb,
replayGainPeak,
loudnessGainDb: loudnessGainDbForEngineBind(track.id),
preGainDb: authState.replayGainPreGainDb,
fallbackDb: authState.replayGainFallbackDb,
manual: false,
hiResEnabled: authState.enableHiRes,
analysisTrackId: track.id,
streamFormatSuffix: track.suffix ?? null,
})
.then(() => {
if (getPlayGeneration() !== generation) return;
if (keepPreloadHint) {
usePlayerStore.setState({ enginePreloadedTrackId: null });
}
const dur = track.duration && track.duration > 0 ? track.duration : null;
const seekTo = Math.max(0, atSeconds);
const canSeek = seekTo > 0.05 && (dur == null || seekTo < dur - 0.05);
const afterSeek = () => {
if (getPlayGeneration() !== generation) return;
if (!wantPlaying) {
invoke('audio_pause').catch(console.error);
setIsAudioPaused(true);
usePlayerStore.setState({ isPlaying: false });
} else {
setIsAudioPaused(false);
}
};
if (canSeek) {
void invoke('audio_seek', { seconds: seekTo }).then(afterSeek).catch(afterSeek);
} else {
afterSeek();
}
})
.catch((err: unknown) => {
if (getPlayGeneration() !== generation) return;
console.error('[psysonic] queue-undo audio_play failed:', err);
usePlayerStore.setState({ isPlaying: false });
})
.finally(() => {
setDeferHotCachePrefetch(false);
});
touchHotCacheOnPlayback(track.id, getPlaybackServerId());
}