mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
chore(playback): remove Preload Next Track setting (#1007)
* chore(playback): remove Preload Next Track setting The configurable next-track RAM preload duplicated hot cache and is obsolete now that ranged streaming starts playback sooner. Gapless and crossfade still use the internal audio_preload backup when hot cache is off. * docs: add CHANGELOG entry for Preload Next Track removal (PR #1007)
This commit is contained in:
@@ -207,46 +207,22 @@ export function handleAudioProgress(
|
||||
markStoreProgressCommit(nowCommit);
|
||||
}
|
||||
|
||||
// Pre-buffer / pre-chain next track based on preload mode and crossfade.
|
||||
// Pre-buffer / pre-chain next track for gapless and crossfade.
|
||||
const {
|
||||
gaplessEnabled,
|
||||
preloadMode,
|
||||
preloadCustomSeconds,
|
||||
hotCacheEnabled,
|
||||
crossfadeEnabled,
|
||||
crossfadeSecs,
|
||||
} = useAuthStore.getState();
|
||||
const remaining = dur - current_time;
|
||||
|
||||
// Gapless chain: always triggers at 30s regardless of preloadMode.
|
||||
const shouldChainGapless = gaplessEnabled && remaining < 30 && remaining > 0;
|
||||
// Byte pre-download: skip when Hot Cache is active (it already handles buffering).
|
||||
// Even with preload mode OFF, crossfade needs the next track bytes ready before
|
||||
// we enter the fade window to avoid a hard gap after track boundary.
|
||||
const shouldBytePreloadFromMode = preloadMode !== 'off' && (
|
||||
preloadMode === 'early'
|
||||
? current_time >= 5
|
||||
: preloadMode === 'custom'
|
||||
? remaining < preloadCustomSeconds && remaining > 0
|
||||
: remaining < 30 && remaining > 0 // balanced (default)
|
||||
);
|
||||
// Crossfade needs the next track bytes ready before the fade window.
|
||||
const crossfadeWindowSecs = Math.max(8, Math.min(30, crossfadeSecs + 6));
|
||||
const shouldBytePreloadForCrossfade =
|
||||
!gaplessEnabled && crossfadeEnabled && remaining < crossfadeWindowSecs && remaining > 0;
|
||||
const shouldBytePreload = !hotCacheEnabled && (
|
||||
shouldBytePreloadFromMode ||
|
||||
shouldBytePreloadForCrossfade
|
||||
);
|
||||
// Hot/offline cache: seed enrichment from disk (playback also uses psysonic-local://).
|
||||
const shouldPreloadLocalFileAnalysis = preloadMode !== 'off' && (
|
||||
preloadMode === 'early'
|
||||
? current_time >= 5
|
||||
: preloadMode === 'custom'
|
||||
? remaining < preloadCustomSeconds && remaining > 0
|
||||
: remaining < 30 && remaining > 0
|
||||
);
|
||||
!hotCacheEnabled && !gaplessEnabled && crossfadeEnabled && remaining < crossfadeWindowSecs && remaining > 0;
|
||||
|
||||
if (shouldChainGapless || shouldBytePreload || shouldPreloadLocalFileAnalysis || gaplessEnabled) {
|
||||
if (shouldChainGapless || shouldBytePreloadForCrossfade || gaplessEnabled) {
|
||||
const { queueItems, queueIndex, repeatMode } = store;
|
||||
const nextIdx = queueIndex + 1;
|
||||
// Next track for preload/chain. The resolver bridge keeps the window around
|
||||
@@ -281,11 +257,10 @@ export function handleAudioProgress(
|
||||
const serverId = getPlaybackCacheServerKey();
|
||||
const analysisServerId = getPlaybackIndexKey();
|
||||
const nextUrl = resolvePlaybackUrl(nextTrack.id, serverId);
|
||||
const nextIsLocalFile = nextUrl.startsWith('psysonic-local://');
|
||||
|
||||
// Byte pre-download — runs early so bytes are cached by chain time.
|
||||
// Byte pre-download — gapless backup or crossfade; runs early so bytes are ready by chain time.
|
||||
if (
|
||||
(shouldBytePreload || shouldBytePreloadForGaplessBackup || (shouldPreloadLocalFileAnalysis && nextIsLocalFile))
|
||||
(shouldBytePreloadForCrossfade || shouldBytePreloadForGaplessBackup)
|
||||
&& nextTrack.id !== getBytePreloadingId()
|
||||
) {
|
||||
setBytePreloadingId(nextTrack.id);
|
||||
@@ -296,10 +271,8 @@ export function handleAudioProgress(
|
||||
console.info('[psysonic][preload-request]', {
|
||||
nextTrackId: nextTrack.id,
|
||||
nextUrl,
|
||||
shouldBytePreload,
|
||||
shouldBytePreloadForCrossfade,
|
||||
shouldBytePreloadForGaplessBackup,
|
||||
shouldPreloadLocalFileAnalysis,
|
||||
nextIsLocalFile,
|
||||
remaining,
|
||||
gaplessEnabled,
|
||||
});
|
||||
|
||||
@@ -6,23 +6,19 @@ type SetState = (
|
||||
|
||||
/**
|
||||
* Persistent plumbing settings that don't fit a more specific domain:
|
||||
* runtime logging level, Navidrome `getNowPlaying` toggle, preload
|
||||
* mode + custom seconds, audiobook exclusion, genre blacklist.
|
||||
* runtime logging level, Navidrome `getNowPlaying` toggle, audiobook
|
||||
* exclusion, genre blacklist.
|
||||
*/
|
||||
export function createPlumbingSettingsActions(set: SetState): Pick<
|
||||
AuthState,
|
||||
| 'setLoggingMode'
|
||||
| 'setNowPlayingEnabled'
|
||||
| 'setPreloadMode'
|
||||
| 'setPreloadCustomSeconds'
|
||||
| 'setExcludeAudiobooks'
|
||||
| 'setCustomGenreBlacklist'
|
||||
> {
|
||||
return {
|
||||
setLoggingMode: (v) => set({ loggingMode: v }),
|
||||
setNowPlayingEnabled: (v) => set({ nowPlayingEnabled: v }),
|
||||
setPreloadMode: (v) => set({ preloadMode: v }),
|
||||
setPreloadCustomSeconds: (v) => set({ preloadCustomSeconds: v }),
|
||||
setExcludeAudiobooks: (v) => set({ excludeAudiobooks: v }),
|
||||
setCustomGenreBlacklist: (v) => set({ customGenreBlacklist: v }),
|
||||
};
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
* Covers Zustand persist's hydration path (via `useAuthStore.persist.rehydrate()`):
|
||||
* existing localStorage shapes load, missing fields default to the store's
|
||||
* initial values, corrupt JSON does not crash bootstrap, the
|
||||
* hotCacheEnabled / preloadMode≠'off' conflicting-legacy migration clears
|
||||
* both, and a few smaller field-level migrations.
|
||||
* legacy field stripping, and a few smaller field-level migrations.
|
||||
*
|
||||
* Also pins the **synchronous storage** invariant called out in `CLAUDE.md`
|
||||
* ("never switch to async storage") — regression §2 of the pre-refactor
|
||||
@@ -53,7 +52,6 @@ describe('hydration — loads existing localStorage shape', () => {
|
||||
expect(s.gaplessEnabled).toBe(false);
|
||||
expect(s.replayGainEnabled).toBe(false);
|
||||
expect(s.normalizationEngine).toBe('off');
|
||||
expect(s.preloadMode).toBe('balanced');
|
||||
});
|
||||
|
||||
it('preserves saved fields verbatim when present', async () => {
|
||||
@@ -94,34 +92,21 @@ describe('hydration — corrupt / unexpected input', () => {
|
||||
});
|
||||
|
||||
describe('onRehydrate migrations', () => {
|
||||
it('clears the conflicting hotCacheEnabled + preloadMode≠"off" legacy combo', async () => {
|
||||
it('keeps hotCacheEnabled when legacy preloadMode fields are present', async () => {
|
||||
writePersistedState({
|
||||
servers: [],
|
||||
activeServerId: null,
|
||||
hotCacheEnabled: true,
|
||||
preloadMode: 'balanced',
|
||||
});
|
||||
|
||||
await useAuthStore.persist.rehydrate();
|
||||
|
||||
const s = useAuthStore.getState();
|
||||
expect(s.hotCacheEnabled).toBe(false);
|
||||
expect(s.preloadMode).toBe('off');
|
||||
});
|
||||
|
||||
it('keeps hotCacheEnabled when preloadMode was already "off"', async () => {
|
||||
writePersistedState({
|
||||
servers: [],
|
||||
activeServerId: null,
|
||||
hotCacheEnabled: true,
|
||||
preloadMode: 'off',
|
||||
preloadCustomSeconds: 45,
|
||||
});
|
||||
|
||||
await useAuthStore.persist.rehydrate();
|
||||
|
||||
const s = useAuthStore.getState();
|
||||
expect(s.hotCacheEnabled).toBe(true);
|
||||
expect(s.preloadMode).toBe('off');
|
||||
expect((s as { preloadMode?: unknown }).preloadMode).toBeUndefined();
|
||||
expect((s as { preloadCustomSeconds?: unknown }).preloadCustomSeconds).toBeUndefined();
|
||||
});
|
||||
|
||||
it('migrates a legacy `waveform` seekbarStyle to `truewave`', async () => {
|
||||
|
||||
@@ -82,7 +82,6 @@ describe('trivial pass-through setters', () => {
|
||||
['setMaxCacheMb', 'maxCacheMb', 2048],
|
||||
['setHotCacheMaxMb', 'hotCacheMaxMb', 1024],
|
||||
['setHotCacheDebounceSec', 'hotCacheDebounceSec', 60],
|
||||
['setPreloadCustomSeconds', 'preloadCustomSeconds', 45],
|
||||
])('%s stores a numeric value', (setter, key, value) => {
|
||||
(useAuthStore.getState() as unknown as Record<string, (v: unknown) => void>)[setter](value);
|
||||
expect((useAuthStore.getState() as unknown as Record<string, unknown>)[key]).toBe(value);
|
||||
@@ -207,14 +206,7 @@ describe('replay-gain related setters (write through to player store)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('preload mode + discord cover source setters', () => {
|
||||
it('setPreloadMode accepts off / balanced / early / custom', () => {
|
||||
for (const mode of ['off', 'balanced', 'early', 'custom'] as const) {
|
||||
useAuthStore.getState().setPreloadMode(mode);
|
||||
expect(useAuthStore.getState().preloadMode).toBe(mode);
|
||||
}
|
||||
});
|
||||
|
||||
describe('discord cover source setters', () => {
|
||||
it('setDiscordCoverSource accepts none / apple / server', () => {
|
||||
for (const src of ['none', 'apple', 'server'] as const) {
|
||||
useAuthStore.getState().setDiscordCoverSource(src);
|
||||
|
||||
@@ -59,8 +59,6 @@ export const useAuthStore = create<AuthState>()(
|
||||
trackPreviewLocations: { ...DEFAULT_TRACK_PREVIEW_LOCATIONS },
|
||||
trackPreviewStartRatio: 0.33,
|
||||
trackPreviewDurationSec: 30,
|
||||
preloadMode: 'balanced',
|
||||
preloadCustomSeconds: 30,
|
||||
infiniteQueueEnabled: false,
|
||||
preservePlayNextOrder: false,
|
||||
showArtistImages: false,
|
||||
|
||||
@@ -34,11 +34,9 @@ import type {
|
||||
* and writes the one-shot Linux smooth-scroll migration sentinel.
|
||||
*/
|
||||
export function computeAuthStoreRehydration(state: AuthState): Partial<AuthState> {
|
||||
// If both hot cache and preload were enabled before mutual exclusion was enforced, reset both.
|
||||
const conflictingLegacyState =
|
||||
state.hotCacheEnabled && state.preloadMode !== 'off'
|
||||
? { hotCacheEnabled: false, preloadMode: 'off' as const }
|
||||
: {};
|
||||
// Drop removed preload-next-track settings from legacy persist blobs.
|
||||
delete (state as { preloadMode?: unknown }).preloadMode;
|
||||
delete (state as { preloadCustomSeconds?: unknown }).preloadCustomSeconds;
|
||||
|
||||
// Migrate lyricsServerFirst + enableNeteaselyrics → lyricsSources (one-time).
|
||||
// Only for an *existing* persisted state (upgrade from a build without
|
||||
@@ -166,7 +164,6 @@ export function computeAuthStoreRehydration(state: AuthState): Partial<AuthState
|
||||
loudnessTargetLufs: targetSan,
|
||||
loudnessPreAnalysisAttenuationDb: preSan,
|
||||
loudnessPreIsRefV1: true,
|
||||
...conflictingLegacyState,
|
||||
...lyricsSourcesMigrated,
|
||||
...youLyPlusMigrated,
|
||||
...wheelSmoothOneTime,
|
||||
|
||||
@@ -115,8 +115,6 @@ export interface AuthState {
|
||||
trackPreviewStartRatio: number;
|
||||
/** Preview window length in seconds. Default 30 s. */
|
||||
trackPreviewDurationSec: number;
|
||||
preloadMode: 'off' | 'balanced' | 'early' | 'custom';
|
||||
preloadCustomSeconds: number;
|
||||
infiniteQueueEnabled: boolean;
|
||||
preservePlayNextOrder: boolean;
|
||||
showArtistImages: boolean;
|
||||
@@ -307,8 +305,6 @@ export interface AuthState {
|
||||
setTrackPreviewLocation: (location: TrackPreviewLocation, enabled: boolean) => void;
|
||||
setTrackPreviewStartRatio: (v: number) => void;
|
||||
setTrackPreviewDurationSec: (v: number) => void;
|
||||
setPreloadMode: (v: 'off' | 'balanced' | 'early' | 'custom') => void;
|
||||
setPreloadCustomSeconds: (v: number) => void;
|
||||
setInfiniteQueueEnabled: (v: boolean) => void;
|
||||
setPreservePlayNextOrder: (v: boolean) => void;
|
||||
setShowArtistImages: (v: boolean) => void;
|
||||
|
||||
@@ -46,12 +46,11 @@ export function collectLoudnessBackfillWindowTrackIds(
|
||||
return Array.from(ids);
|
||||
}
|
||||
|
||||
/** Next ~5 queue neighbours (+ immediate next when preload is on) for middle-tier analysis. */
|
||||
/** Next ~5 queue neighbours for middle-tier analysis priority hints. */
|
||||
export function collectPlaybackMiddlePriorityTrackIds(
|
||||
queue: QueueItemRef[],
|
||||
queueIndex: number,
|
||||
currentTrack: Track | null,
|
||||
preloadMode: 'off' | 'balanced' | 'early' | 'custom',
|
||||
): string[] {
|
||||
const ids = new Set<string>();
|
||||
const start = Math.max(0, queueIndex + 1);
|
||||
@@ -60,13 +59,6 @@ export function collectPlaybackMiddlePriorityTrackIds(
|
||||
const tid = queue[i]?.trackId;
|
||||
if (tid && tid !== currentTrack?.id) ids.add(tid);
|
||||
}
|
||||
if (preloadMode !== 'off') {
|
||||
const nextIdx = queueIndex + 1;
|
||||
if (nextIdx >= 0 && nextIdx < queue.length) {
|
||||
const tid = queue[nextIdx]?.trackId;
|
||||
if (tid && tid !== currentTrack?.id) ids.add(tid);
|
||||
}
|
||||
}
|
||||
return Array.from(ids);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user