refactor(player): E.14 — extract loudness-backfill-window helpers (#577)

The `LOUDNESS_BACKFILL_WINDOW_AHEAD` constant, the predicate
`isTrackInsideLoudnessBackfillWindow`, and the id-list collector
`collectLoudnessBackfillWindowTrackIds` move into
`src/store/loudnessBackfillWindow.ts`.

`isTrackInsideLoudnessBackfillWindow` was rewritten to be pure — it
now takes the queue / queueIndex / currentTrack as explicit
parameters instead of reading `usePlayerStore.getState()` internally.
The one call site in `refreshLoudnessForTrack` reads state once and
passes it through. Net: zero behaviour change, clearer test surface
(no store mocks), no top-level coupling back to playerStore.

`prefetchLoudnessForEnqueuedTracks` stays in playerStore — it calls
`refreshLoudnessForTrack` which lives there too.

13 focused tests pin the window-slides-with-queueIndex contract, the
clamp at the end of the queue, the empty-input fallbacks, and the
duplicate collapse when currentTrack is also in the ahead range.

playerStore 3261 → 3238 LOC.
This commit is contained in:
Frank Stellmacher
2026-05-12 14:36:55 +02:00
committed by GitHub
parent c88649836e
commit 08d098d5aa
3 changed files with 147 additions and 31 deletions
+8 -31
View File
@@ -84,6 +84,11 @@ import {
resetBackfillAttempts,
resetLoudnessBackfillStateForTrackId,
} from './loudnessBackfillState';
import {
LOUDNESS_BACKFILL_WINDOW_AHEAD,
collectLoudnessBackfillWindowTrackIds,
isTrackInsideLoudnessBackfillWindow,
} from './loudnessBackfillWindow';
// Re-export the playback-progress public surface so existing call sites
// (PlayerBar, FullscreenPlayer, WaveformSeek, LyricsPane, MobilePlayerView,
@@ -721,10 +726,11 @@ async function runRefreshLoudnessForTrack(trackId: string, syncEngine: boolean):
if (auth.normalizationEngine === 'loudness'
&& !isBackfillInFlight(trackId)
&& attempts < MAX_BACKFILL_ATTEMPTS_PER_TRACK) {
if (!isTrackInsideLoudnessBackfillWindow(trackId)) {
const live = usePlayerStore.getState();
if (!isTrackInsideLoudnessBackfillWindow(trackId, live.queue, live.queueIndex, live.currentTrack)) {
emitNormalizationDebug('backfill:skipped-outside-window', {
trackId,
queueIndex: usePlayerStore.getState().queueIndex,
queueIndex: live.queueIndex,
aheadWindow: LOUDNESS_BACKFILL_WINDOW_AHEAD,
});
return;
@@ -774,35 +780,6 @@ async function runRefreshLoudnessForTrack(trackId: string, syncEngine: boolean):
}
}
/** After bulk enqueue, warm loudness cache so gapless `audio_chain_preload` sees real gain, not only startup trim. */
const LOUDNESS_BACKFILL_WINDOW_AHEAD = 5;
function isTrackInsideLoudnessBackfillWindow(trackId: string): boolean {
if (!trackId) return false;
const state = usePlayerStore.getState();
const currentId = state.currentTrack?.id;
if (currentId === trackId) return true;
if (state.queue.length === 0) return false;
const start = Math.max(0, state.queueIndex + 1);
const end = Math.min(state.queue.length, start + LOUDNESS_BACKFILL_WINDOW_AHEAD);
for (let i = start; i < end; i++) {
if (state.queue[i]?.id === trackId) return true;
}
return false;
}
function collectLoudnessBackfillWindowTrackIds(queue: Track[], queueIndex: number, currentTrack: Track | null): string[] {
const ids = new Set<string>();
if (currentTrack?.id) ids.add(currentTrack.id);
const start = Math.max(0, queueIndex + 1);
const end = Math.min(queue.length, start + LOUDNESS_BACKFILL_WINDOW_AHEAD);
for (let i = start; i < end; i++) {
const tid = queue[i]?.id;
if (tid) ids.add(tid);
}
return Array.from(ids);
}
function prefetchLoudnessForEnqueuedTracks(
mergedQueue: Track[],
queueIndex: number,