mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
cb1a110afb
Relocate the playback/queue/transport/audio-output engine out of the type-first
store/ + utils/playback/ + utils/audio/ dirs into a cohesive src/features/playback/,
structure-preserving:
store/<x> -> features/playback/store/<x>
store/audioListenerSetup/<x> -> features/playback/store/audioListenerSetup/<x>
utils/playback/<x> -> features/playback/utils/playback/<x>
utils/audio/<x> -> features/playback/utils/audio/<x>
184 files moved (107 source + 77 tests), 365 consumers rewritten. Pure move — no
behavior change, no state-split (the playerStore state-split stays a separate M5
question). Enabled by this session's decouple seams (artist/offline/orbit/auth →
core registries), so the engine carries no inbound core->feature inversion: store/
now holds only the 50 cross-cutting global stores (auth family, the seams, library
index, UI/settings stores).
KEPT OUT of the move (would re-create global->engine edges): the 3 pure config
helpers utils/audio/{loudnessPreAnalysisSlider,hiResCrossfadeResample} +
utils/playback/autodjOverlapCap (authStore + settings UI read them — they stay in
utils/). Ambiguous view-state stores (eqStore, queueToolbarStore,
playerBarLayoutStore) stay global (no engine imports).
Consumers use DEEP paths (@/features/playback/...), no barrel — matches the lib/
approach and avoids barrel-mock-collapse across the 140 usePlayerStore consumers.
Two tolerated type-only core->feature edges remain (localPlaybackStore->QueueItemRef,
localPlaybackMigration->HotCacheEntry, both erased).
tsc 0, lint 0, full suite 319/2353 green, iron-rule clean (no runtime store->feature
import). Behavior-touching only via the prerequisite bridge seam (already QA-flagged);
the move itself is pure.
115 lines
3.7 KiB
TypeScript
115 lines
3.7 KiB
TypeScript
import type { QueueItemRef } from '@/features/playback/store/playerStoreTypes';
|
|
import { create } from 'zustand';
|
|
import type { HotCacheEntry } from '@/features/playback/store/hotCacheStoreTypes';
|
|
import { useLocalPlaybackStore, type LocalPlaybackEntry } from '@/store/localPlaybackStore';
|
|
import { entryBelongsToServer } from '@/store/localPlaybackResolve';
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
import { getMediaDir } from '@/utils/media/mediaDir';
|
|
|
|
export type { HotCacheEntry } from '@/features/playback/store/hotCacheStoreTypes';
|
|
/** @deprecated Use {@link LOCAL_PLAYBACK_PROTECT_AFTER_CURRENT}. */
|
|
export const HOT_CACHE_PROTECT_AFTER_CURRENT = 1;
|
|
|
|
interface HotCacheState {
|
|
getLocalUrl: (trackId: string, serverId: string) => string | null;
|
|
setEntry: (
|
|
trackId: string,
|
|
serverId: string,
|
|
localPath: string,
|
|
sizeBytes: number,
|
|
debugSource?: string,
|
|
layoutFingerprint?: string,
|
|
suffix?: string,
|
|
) => void;
|
|
touchPlayed: (trackId: string, serverId: string) => void;
|
|
removeEntry: (trackId: string, serverId: string) => Promise<void>;
|
|
totalBytes: () => number;
|
|
evictToFit: (
|
|
queue: QueueItemRef[],
|
|
queueIndex: number,
|
|
maxBytes: number,
|
|
activeServerId: string,
|
|
mediaDir: string | null,
|
|
) => Promise<void>;
|
|
clearAllDisk: (mediaDir: string | null) => Promise<void>;
|
|
}
|
|
|
|
/** Ephemeral-tier view for UI selectors (Settings track count, prefetch helpers). */
|
|
export function selectHotCacheEntries(
|
|
entries: Record<string, import('@/store/localPlaybackStore').LocalPlaybackEntry>,
|
|
): Record<string, HotCacheEntry> {
|
|
const out: Record<string, HotCacheEntry> = {};
|
|
for (const [key, e] of Object.entries(entries)) {
|
|
if (e.tier !== 'ephemeral') continue;
|
|
out[key] = {
|
|
localPath: e.localPath,
|
|
sizeBytes: e.sizeBytes,
|
|
cachedAt: e.cachedAt,
|
|
lastPlayedAt: e.lastPlayedAt,
|
|
};
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** Ephemeral-tier row count for Settings (optional active-server scope). */
|
|
export function countHotCacheTracks(
|
|
entries: Record<string, LocalPlaybackEntry>,
|
|
scopeServerId?: string,
|
|
): number {
|
|
let n = 0;
|
|
for (const e of Object.values(entries)) {
|
|
if (e.tier !== 'ephemeral') continue;
|
|
if (scopeServerId && !entryBelongsToServer(e, scopeServerId)) continue;
|
|
n++;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
export const useHotCacheStore = create<HotCacheState>()(() => ({
|
|
getLocalUrl: (trackId, serverId) =>
|
|
useLocalPlaybackStore.getState().getLocalUrl(trackId, serverId, 'ephemeral'),
|
|
|
|
setEntry: (trackId, serverId, localPath, sizeBytes, _debugSource, layoutFingerprint = '', suffix = 'mp3') => {
|
|
useLocalPlaybackStore.getState().upsertEntry({
|
|
serverIndexKey: serverId,
|
|
trackId,
|
|
localPath,
|
|
sizeBytes,
|
|
layoutFingerprint,
|
|
tier: 'ephemeral',
|
|
suffix,
|
|
});
|
|
},
|
|
|
|
touchPlayed: (trackId, serverId) => {
|
|
useLocalPlaybackStore.getState().touchPlayed(trackId, serverId);
|
|
},
|
|
|
|
removeEntry: async (trackId, serverId) => {
|
|
const lp = useLocalPlaybackStore.getState();
|
|
const e = lp.getEntry(trackId, serverId);
|
|
if (e?.tier === 'ephemeral' && e.localPath) {
|
|
await invoke('delete_media_file', { localPath: e.localPath, mediaDir: getMediaDir() }).catch(
|
|
() => {},
|
|
);
|
|
lp.removeEntry(trackId, serverId, 'hot-cache-shim');
|
|
}
|
|
},
|
|
|
|
totalBytes: () => useLocalPlaybackStore.getState().ephemeralTotalBytes(),
|
|
|
|
evictToFit: async (queue, queueIndex, maxBytes, activeServerId, mediaDir) => {
|
|
await useLocalPlaybackStore.getState().evictEphemeralToFit(
|
|
queue,
|
|
queueIndex,
|
|
maxBytes,
|
|
activeServerId,
|
|
mediaDir,
|
|
);
|
|
},
|
|
|
|
clearAllDisk: async (mediaDir) => {
|
|
await useLocalPlaybackStore.getState().purgeEphemeralDisk(mediaDir);
|
|
},
|
|
}));
|