mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
refactor(player): E.18 — extract three transport-coordination modules (#581)
Cluster of three small thematically-related cuts in one PR (per the new
'cluster, don't single-shot' convention):
- `src/store/seekTargetState.ts` — the seek-target guard (`seekTarget`,
`seekTargetSetAt`, `SEEK_TARGET_GUARD_TIMEOUT_MS`, set/clear/get
accessors) that suppresses stale Rust progress ticks until the
engine catches up to the requested position
- `src/store/togglePlayLock.ts` — the 300 ms double-click cooldown
behind a `tryAcquireTogglePlayLock()` helper that auto-releases on
a timer (collapses the three-line inline pattern in `togglePlay`)
- `src/store/loudnessReseed.ts` — the full `reseedLoudnessForTrackId`
pipeline (gen-bump → cache + backfill wipe → state reset → server
row delete → forced seed enqueue), pulled out of playerStore as a
single async helper
All three were file-private; no caller-side changes outside
playerStore's own imports. The progress handler's seek-guard branch is
now ~3 lines shorter and reads through accessors. `togglePlay` collapses
to one guard check.
24 tests across the three modules pin the API contracts.
playerStore 3189 → 3139 LOC.
This commit is contained in:
committed by
GitHub
parent
9029ab8ec5
commit
4c64844349
+14
-64
@@ -105,6 +105,15 @@ import {
|
||||
setGaplessPreloadingId,
|
||||
} from './gaplessPreloadState';
|
||||
import { promoteCompletedStreamToHotCache } from './promoteStreamCache';
|
||||
import {
|
||||
SEEK_TARGET_GUARD_TIMEOUT_MS,
|
||||
clearSeekTarget,
|
||||
getSeekTarget,
|
||||
getSeekTargetSetAt,
|
||||
setSeekTarget,
|
||||
} from './seekTargetState';
|
||||
import { tryAcquireTogglePlayLock } from './togglePlayLock';
|
||||
import { reseedLoudnessForTrackId } from './loudnessReseed';
|
||||
|
||||
// Re-export so TauriEventBridge + persistence test keep their existing
|
||||
// `from './playerStore'` imports.
|
||||
@@ -476,11 +485,6 @@ function queueUndoRestoreAudioEngine(opts: {
|
||||
|
||||
// Debounce timer for seek slider drags.
|
||||
let seekDebounce: ReturnType<typeof setTimeout> | null = null;
|
||||
// Target time of the last seek — blocks stale Rust progress ticks until the
|
||||
// engine has actually caught up to the new position.
|
||||
let seekTarget: number | null = null;
|
||||
let seekTargetSetAt = 0;
|
||||
const SEEK_TARGET_GUARD_TIMEOUT_MS = 5000;
|
||||
// Streaming fallback seek guard: coalesce repeated "not seekable" recoveries.
|
||||
let seekFallbackRetryTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let seekFallbackRetryStartedAt = 0;
|
||||
@@ -499,16 +503,6 @@ const STORE_PROGRESS_COMMIT_MIN_DELTA_SEC = 5.0;
|
||||
let lastStoreProgressCommitAt = 0;
|
||||
|
||||
|
||||
function setSeekTarget(seconds: number) {
|
||||
seekTarget = seconds;
|
||||
seekTargetSetAt = Date.now();
|
||||
}
|
||||
|
||||
function clearSeekTarget() {
|
||||
seekTarget = null;
|
||||
seekTargetSetAt = 0;
|
||||
}
|
||||
|
||||
function clearSeekFallbackRetry() {
|
||||
if (seekFallbackRetryTimer) {
|
||||
clearTimeout(seekFallbackRetryTimer);
|
||||
@@ -562,9 +556,6 @@ function scheduleSeekFallbackRetry(trackId: string, seconds: number) {
|
||||
}, SEEK_FALLBACK_RETRY_INTERVAL_MS);
|
||||
}
|
||||
|
||||
// Guard against rapid double-click play/pause sending two state transitions
|
||||
// to the Rust backend before it has finished the previous one.
|
||||
let togglePlayLock = false;
|
||||
// ── HTML5 Radio Player ────────────────────────────────────────────────────────
|
||||
// Internet radio streams are played via a native <audio> element instead of
|
||||
// the Rust/Symphonia engine. This gives us browser-native reconnect logic,
|
||||
@@ -640,46 +631,6 @@ radioAudio.addEventListener('suspend', () => {
|
||||
* preload + current-track completion can stack two SQLite reads + two state writes. */
|
||||
const loudnessRefreshInflight = new Map<string, Promise<void>>();
|
||||
|
||||
async function reseedLoudnessForTrackId(trackId: string) {
|
||||
if (!trackId) return;
|
||||
const auth = useAuthStore.getState();
|
||||
if (auth.normalizationEngine !== 'loudness') return;
|
||||
bumpWaveformRefreshGen(trackId);
|
||||
if (usePlayerStore.getState().currentTrack?.id === trackId) {
|
||||
usePlayerStore.setState({ waveformBins: null });
|
||||
}
|
||||
clearLoudnessCacheStateForTrackId(trackId);
|
||||
resetLoudnessBackfillStateForTrackId(trackId);
|
||||
if (auth.normalizationEngine === 'loudness') {
|
||||
usePlayerStore.setState({
|
||||
normalizationNowDb: null,
|
||||
normalizationTargetLufs: auth.loudnessTargetLufs,
|
||||
normalizationEngineLive: 'loudness',
|
||||
});
|
||||
}
|
||||
try {
|
||||
await invoke('analysis_delete_waveform_for_track', { trackId });
|
||||
} catch (e) {
|
||||
console.error('[psysonic] analysis_delete_waveform_for_track failed:', e);
|
||||
}
|
||||
try {
|
||||
await invoke('analysis_delete_loudness_for_track', { trackId });
|
||||
} catch (e) {
|
||||
console.error('[psysonic] analysis_delete_loudness_for_track failed:', e);
|
||||
}
|
||||
usePlayerStore.getState().updateReplayGainForCurrentTrack();
|
||||
const url = buildStreamUrl(trackId);
|
||||
try {
|
||||
await invoke('analysis_enqueue_seed_from_url', {
|
||||
trackId,
|
||||
url,
|
||||
force: true,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[psysonic] analysis_enqueue_seed_from_url (reseed) failed:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshWaveformForTrack(trackId: string) {
|
||||
if (!trackId) return;
|
||||
const gen = getWaveformRefreshGen(trackId);
|
||||
@@ -828,10 +779,11 @@ function handleAudioProgress(current_time: number, duration: number) {
|
||||
// After the debounce fires, Rust may still emit 1–2 ticks with the old
|
||||
// position before the seek takes effect. Block until current_time is
|
||||
// within 2 s of the requested target, then clear the guard.
|
||||
if (seekTarget !== null) {
|
||||
if (Math.abs(current_time - seekTarget) > 2.0) {
|
||||
const activeSeekTarget = getSeekTarget();
|
||||
if (activeSeekTarget !== null) {
|
||||
if (Math.abs(current_time - activeSeekTarget) > 2.0) {
|
||||
// If a seek command hangs while streaming is stalled, do not freeze UI.
|
||||
if (Date.now() - seekTargetSetAt <= SEEK_TARGET_GUARD_TIMEOUT_MS) return;
|
||||
if (Date.now() - getSeekTargetSetAt() <= SEEK_TARGET_GUARD_TIMEOUT_MS) return;
|
||||
clearSeekTarget();
|
||||
} else {
|
||||
clearSeekTarget();
|
||||
@@ -2515,9 +2467,7 @@ export const usePlayerStore = create<PlayerState>()(
|
||||
},
|
||||
|
||||
togglePlay: () => {
|
||||
if (togglePlayLock) return;
|
||||
togglePlayLock = true;
|
||||
setTimeout(() => { togglePlayLock = false; }, 300);
|
||||
if (!tryAcquireTogglePlayLock()) return;
|
||||
const { isPlaying } = get();
|
||||
isPlaying ? get().pause() : get().resume();
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user