mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 14:55:43 +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:
@@ -60,6 +60,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Playback — Preload Next Track setting removed
|
||||||
|
|
||||||
|
**By [@cucadmuh](https://github.com/cucadmuh), PR [#1007](https://github.com/Psychotoxical/psysonic/pull/1007)**
|
||||||
|
|
||||||
|
* The **Preload Next Track** toggle and timing modes under **Settings → Storage → Buffering** are gone — ranged streaming now starts playback without that extra RAM prefetch.
|
||||||
|
* Gapless and crossfade still prefetch the next track internally when Hot Cache is off; Hot Cache is unchanged.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
### Servers — complete border on the active server card
|
### Servers — complete border on the active server card
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ impl AnalysisBackfillQueueState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Frontend-maintained set of queue-neighbour track ids (next ~5 + preload next).
|
/// Frontend-maintained set of queue-neighbour track ids (next ~5 in queue).
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct PlaybackPriorityHints {
|
pub struct PlaybackPriorityHints {
|
||||||
middle_track_ids: Mutex<HashSet<String>>,
|
middle_track_ids: Mutex<HashSet<String>>,
|
||||||
|
|||||||
@@ -203,65 +203,6 @@ export function StorageTab() {
|
|||||||
icon={<Download size={16} />}
|
icon={<Download size={16} />}
|
||||||
>
|
>
|
||||||
<div className="settings-card">
|
<div className="settings-card">
|
||||||
<div style={{ fontSize: 12, color: 'var(--text-muted)', lineHeight: 1.5, marginBottom: '0.75rem' }}>
|
|
||||||
{t('settings.preloadHotCacheMutualExclusive')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Preload mode */}
|
|
||||||
<div className="settings-toggle-row">
|
|
||||||
<div>
|
|
||||||
<div style={{ fontWeight: 500 }}>{t('settings.preloadMode')}</div>
|
|
||||||
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{t('settings.preloadModeDesc')}</div>
|
|
||||||
</div>
|
|
||||||
<label className="toggle-switch" aria-label={t('settings.preloadMode')}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={auth.preloadMode !== 'off'}
|
|
||||||
onChange={e => {
|
|
||||||
if (e.target.checked) {
|
|
||||||
auth.setPreloadMode('balanced');
|
|
||||||
if (auth.hotCacheEnabled) auth.setHotCacheEnabled(false);
|
|
||||||
} else {
|
|
||||||
auth.setPreloadMode('off');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="toggle-track" />
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{auth.preloadMode !== 'off' && (
|
|
||||||
<>
|
|
||||||
<div style={{ paddingLeft: '1rem', marginTop: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
|
|
||||||
{(['balanced', 'early', 'custom'] as const).map(mode => (
|
|
||||||
<button
|
|
||||||
key={mode}
|
|
||||||
className={`btn ${auth.preloadMode === mode ? 'btn-primary' : 'btn-surface'}`}
|
|
||||||
style={{ fontSize: 12, padding: '3px 12px' }}
|
|
||||||
onClick={() => auth.setPreloadMode(mode)}
|
|
||||||
>
|
|
||||||
{t(`settings.preload${mode.charAt(0).toUpperCase() + mode.slice(1)}` as any)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{auth.preloadMode === 'custom' && (
|
|
||||||
<div style={{ paddingLeft: '1rem', marginTop: '0.5rem', display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min={5} max={120} step={5}
|
|
||||||
value={auth.preloadCustomSeconds}
|
|
||||||
onChange={e => auth.setPreloadCustomSeconds(parseInt(e.target.value))}
|
|
||||||
style={{ flex: 1, minWidth: 80, maxWidth: 200 }}
|
|
||||||
/>
|
|
||||||
<span style={{ fontSize: 13, color: 'var(--text-secondary)', minWidth: 36 }}>
|
|
||||||
{t('settings.preloadCustomSeconds', { n: auth.preloadCustomSeconds })}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="divider" />
|
|
||||||
|
|
||||||
{/* Hot Cache */}
|
{/* Hot Cache */}
|
||||||
<div className="settings-toggle-row">
|
<div className="settings-toggle-row">
|
||||||
<div>
|
<div>
|
||||||
@@ -280,7 +221,6 @@ export function StorageTab() {
|
|||||||
auth.setHotCacheEnabled(false);
|
auth.setHotCacheEnabled(false);
|
||||||
} else {
|
} else {
|
||||||
auth.setHotCacheEnabled(true);
|
auth.setHotCacheEnabled(true);
|
||||||
if (auth.preloadMode !== 'off') auth.setPreloadMode('off');
|
|
||||||
invoke<number>('get_hot_cache_size', { customDir: auth.hotCacheDownloadDir || null })
|
invoke<number>('get_hot_cache_size', { customDir: auth.hotCacheDownloadDir || null })
|
||||||
.then(setHotCacheBytes)
|
.then(setHotCacheBytes)
|
||||||
.catch(() => setHotCacheBytes(0));
|
.catch(() => setHotCacheBytes(0));
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export const SETTINGS_INDEX: SearchIndexEntry[] = [
|
|||||||
{ tab: 'library', titleKey: 'settings.ratingsSectionTitle', keywords: 'ratings stars skip threshold manual' },
|
{ tab: 'library', titleKey: 'settings.ratingsSectionTitle', keywords: 'ratings stars skip threshold manual' },
|
||||||
{ tab: 'storage', titleKey: 'settings.coverCacheStrategyTitle', keywords: 'cover art cache webp aggressive lazy disk per server' },
|
{ tab: 'storage', titleKey: 'settings.coverCacheStrategyTitle', keywords: 'cover art cache webp aggressive lazy disk per server' },
|
||||||
{ tab: 'storage', titleKey: 'settings.offlineDirTitle', keywords: 'offline library download directory folder cache' },
|
{ tab: 'storage', titleKey: 'settings.offlineDirTitle', keywords: 'offline library download directory folder cache' },
|
||||||
{ tab: 'storage', titleKey: 'settings.nextTrackBufferingTitle', keywords: 'next track buffering preload hot cache streaming' },
|
{ tab: 'storage', titleKey: 'settings.nextTrackBufferingTitle', keywords: 'next track buffering hot cache streaming' },
|
||||||
{ tab: 'storage', titleKey: 'settings.downloadsTitle', keywords: 'downloads zip export archive folder' },
|
{ tab: 'storage', titleKey: 'settings.downloadsTitle', keywords: 'downloads zip export archive folder' },
|
||||||
{ tab: 'appearance', titleKey: 'settings.theme', keywords: 'theme color palette dark light' },
|
{ tab: 'appearance', titleKey: 'settings.theme', keywords: 'theme color palette dark light' },
|
||||||
{ tab: 'appearance', titleKey: 'settings.themeSchedulerTitle', keywords: 'theme scheduler auto time dark mode sunset' },
|
{ tab: 'appearance', titleKey: 'settings.themeSchedulerTitle', keywords: 'theme scheduler auto time dark mode sunset' },
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ type AnalysisPrunePendingResult = {
|
|||||||
|
|
||||||
export function scheduleAnalysisQueuePruneFromPlaybackQueue(): void {
|
export function scheduleAnalysisQueuePruneFromPlaybackQueue(): void {
|
||||||
const { queueItems, currentTrack, queueIndex } = usePlayerStore.getState();
|
const { queueItems, currentTrack, queueIndex } = usePlayerStore.getState();
|
||||||
const { preloadMode } = useAuthStore.getState();
|
|
||||||
const rawServerId = getPlaybackServerId() ?? '';
|
const rawServerId = getPlaybackServerId() ?? '';
|
||||||
const server = useAuthStore.getState().servers.find(s => s.id === rawServerId);
|
const server = useAuthStore.getState().servers.find(s => s.id === rawServerId);
|
||||||
const serverId = server ? serverIndexKeyFromUrl(server.url) : rawServerId;
|
const serverId = server ? serverIndexKeyFromUrl(server.url) : rawServerId;
|
||||||
@@ -42,7 +41,6 @@ export function scheduleAnalysisQueuePruneFromPlaybackQueue(): void {
|
|||||||
queueItems,
|
queueItems,
|
||||||
queueIndex,
|
queueIndex,
|
||||||
currentTrack,
|
currentTrack,
|
||||||
preloadMode,
|
|
||||||
);
|
);
|
||||||
const sig = JSON.stringify({ keepTrackIds, middleTrackIds, serverId });
|
const sig = JSON.stringify({ keepTrackIds, middleTrackIds, serverId });
|
||||||
if (sig === lastAnalysisPruneSig) return;
|
if (sig === lastAnalysisPruneSig) return;
|
||||||
|
|||||||
@@ -490,15 +490,7 @@ export const settings = {
|
|||||||
trackPreviewLocation_favorites: 'In Favoriten',
|
trackPreviewLocation_favorites: 'In Favoriten',
|
||||||
trackPreviewLocation_artist: 'In Künstler-Top-Tracks',
|
trackPreviewLocation_artist: 'In Künstler-Top-Tracks',
|
||||||
trackPreviewLocation_randomMix: 'In Random Mix',
|
trackPreviewLocation_randomMix: 'In Random Mix',
|
||||||
preloadMode: 'Nächsten Track vorpuffern',
|
|
||||||
preloadModeDesc: 'Wann mit dem Puffern des nächsten Tracks begonnen werden soll',
|
|
||||||
nextTrackBufferingTitle: 'Pufferung',
|
nextTrackBufferingTitle: 'Pufferung',
|
||||||
preloadHotCacheMutualExclusive: 'Preload und Hot Cache erfüllen denselben Zweck – nur eine Methode kann gleichzeitig aktiv sein. Das Aktivieren einer deaktiviert die andere automatisch.',
|
|
||||||
preloadOff: 'Aus',
|
|
||||||
preloadBalanced: 'Ausgewogen (30 s vor Ende)',
|
|
||||||
preloadEarly: 'Früh (nach 5 s Wiedergabe)',
|
|
||||||
preloadCustom: 'Benutzerdefiniert',
|
|
||||||
preloadCustomSeconds: 'Sekunden vor Ende: {{n}}',
|
|
||||||
infiniteQueue: 'Endlose Warteschlange',
|
infiniteQueue: 'Endlose Warteschlange',
|
||||||
infiniteQueueDesc: 'Automatisch Zufallstitel anhängen wenn die Warteschlange leer wird',
|
infiniteQueueDesc: 'Automatisch Zufallstitel anhängen wenn die Warteschlange leer wird',
|
||||||
experimental: 'Experimentell',
|
experimental: 'Experimentell',
|
||||||
@@ -551,7 +543,7 @@ export const settings = {
|
|||||||
analyticsStrategyAdvancedDesc: 'Aggressiver Hintergrundscan. Nach Abschluss erhältst du BPM für alle Tracks sowie sofort verfügbare Loudness- und Waveform-Daten.',
|
analyticsStrategyAdvancedDesc: 'Aggressiver Hintergrundscan. Nach Abschluss erhältst du BPM für alle Tracks sowie sofort verfügbare Loudness- und Waveform-Daten.',
|
||||||
analyticsStrategyPriorityTitle: 'Analyse-Priorität (immer aktiv)',
|
analyticsStrategyPriorityTitle: 'Analyse-Priorität (immer aktiv)',
|
||||||
analyticsStrategyPriorityHigh: '1 — Aktuell laufender Track: zuerst herunterladen und analysieren (Playback-Bytes, Loudness-Backfill oder HTTP falls nötig).',
|
analyticsStrategyPriorityHigh: '1 — Aktuell laufender Track: zuerst herunterladen und analysieren (Playback-Bytes, Loudness-Backfill oder HTTP falls nötig).',
|
||||||
analyticsStrategyPriorityMiddle: '2 — Als Nächstes: die nächsten ~5 Queue-Tracks, Preload-next und Hot-Cache-Nachbarn — Analyse bevorzugt aus Cache/Downloads; HTTP-Backfill nur bei Miss.',
|
analyticsStrategyPriorityMiddle: '2 — Als Nächstes: die nächsten ~5 Queue-Tracks und Hot-Cache-Nachbarn — Analyse bevorzugt aus Cache/Downloads; HTTP-Backfill nur bei Miss.',
|
||||||
analyticsStrategyPriorityLow: '3 — Bibliotheks-Batch (nur Aggressiv): automatische Hintergrund-Queue am Ende; wird per Watermark (~3× Worker) nachgefüllt, ohne auf eine leere Queue zu warten.',
|
analyticsStrategyPriorityLow: '3 — Bibliotheks-Batch (nur Aggressiv): automatische Hintergrund-Queue am Ende; wird per Watermark (~3× Worker) nachgefüllt, ohne auf eine leere Queue zu warten.',
|
||||||
analyticsStrategyServerLabel: 'Server',
|
analyticsStrategyServerLabel: 'Server',
|
||||||
analyticsStrategyProgressLabel: 'Analysefortschritt:',
|
analyticsStrategyProgressLabel: 'Analysefortschritt:',
|
||||||
|
|||||||
@@ -334,7 +334,7 @@ export const settings = {
|
|||||||
analyticsStrategyAdvancedDesc: 'More aggressive background scan. Once finished, you get BPM for every track plus instant loudness + waveform loads.',
|
analyticsStrategyAdvancedDesc: 'More aggressive background scan. Once finished, you get BPM for every track plus instant loudness + waveform loads.',
|
||||||
analyticsStrategyPriorityTitle: 'Analysis priority (always enforced)',
|
analyticsStrategyPriorityTitle: 'Analysis priority (always enforced)',
|
||||||
analyticsStrategyPriorityHigh: '1 — Currently playing: download + analyze first (playback bytes, loudness backfill, or HTTP when needed).',
|
analyticsStrategyPriorityHigh: '1 — Currently playing: download + analyze first (playback bytes, loudness backfill, or HTTP when needed).',
|
||||||
analyticsStrategyPriorityMiddle: '2 — Up next: the next ~5 queue tracks, preload next, and hot-cache neighbours — analyze from cached/downloaded bytes when possible; HTTP backfill only on miss.',
|
analyticsStrategyPriorityMiddle: '2 — Up next: the next ~5 queue tracks and hot-cache neighbours — analyze from cached/downloaded bytes when possible; HTTP backfill only on miss.',
|
||||||
analyticsStrategyPriorityLow: '3 — Library batch (Aggressive only): automatic background queue at the tail; refilled by watermark (~3× workers) without waiting for an empty queue.',
|
analyticsStrategyPriorityLow: '3 — Library batch (Aggressive only): automatic background queue at the tail; refilled by watermark (~3× workers) without waiting for an empty queue.',
|
||||||
analyticsStrategyServerLabel: 'Server',
|
analyticsStrategyServerLabel: 'Server',
|
||||||
analyticsStrategyProgressLabel: 'Analysis progress:',
|
analyticsStrategyProgressLabel: 'Analysis progress:',
|
||||||
@@ -577,15 +577,7 @@ export const settings = {
|
|||||||
trackPreviewLocation_favorites: 'In favorites',
|
trackPreviewLocation_favorites: 'In favorites',
|
||||||
trackPreviewLocation_artist: 'In artist top tracks',
|
trackPreviewLocation_artist: 'In artist top tracks',
|
||||||
trackPreviewLocation_randomMix: 'In Random Mix',
|
trackPreviewLocation_randomMix: 'In Random Mix',
|
||||||
preloadMode: 'Preload Next Track',
|
|
||||||
preloadModeDesc: 'When to start buffering the next track in the queue',
|
|
||||||
nextTrackBufferingTitle: 'Buffering',
|
nextTrackBufferingTitle: 'Buffering',
|
||||||
preloadHotCacheMutualExclusive: 'Preload and Hot Cache serve the same purpose — only one can be active at a time. Enabling one will automatically disable the other.',
|
|
||||||
preloadOff: 'Off',
|
|
||||||
preloadBalanced: 'Balanced (30 s before end)',
|
|
||||||
preloadEarly: 'Early (after 5 s of playback)',
|
|
||||||
preloadCustom: 'Custom',
|
|
||||||
preloadCustomSeconds: 'Seconds before end: {{n}}',
|
|
||||||
infiniteQueue: 'Infinite Queue',
|
infiniteQueue: 'Infinite Queue',
|
||||||
infiniteQueueDesc: 'Automatically append random tracks when the queue runs out',
|
infiniteQueueDesc: 'Automatically append random tracks when the queue runs out',
|
||||||
experimental: 'Experimental',
|
experimental: 'Experimental',
|
||||||
|
|||||||
@@ -489,15 +489,7 @@ export const settings = {
|
|||||||
trackPreviewLocation_favorites: 'En favoritos',
|
trackPreviewLocation_favorites: 'En favoritos',
|
||||||
trackPreviewLocation_artist: 'En las mejores pistas del artista',
|
trackPreviewLocation_artist: 'En las mejores pistas del artista',
|
||||||
trackPreviewLocation_randomMix: 'En Random Mix',
|
trackPreviewLocation_randomMix: 'En Random Mix',
|
||||||
preloadMode: 'Precargar Siguiente Pista',
|
|
||||||
preloadModeDesc: 'Cuándo comenzar a cargar la siguiente pista en cola',
|
|
||||||
nextTrackBufferingTitle: 'Buffer',
|
nextTrackBufferingTitle: 'Buffer',
|
||||||
preloadHotCacheMutualExclusive: 'Precarga y Caché Activa sirven para lo mismo — solo uno puede estar activo a la vez. Habilitar uno desactivará automáticamente el otro.',
|
|
||||||
preloadOff: 'Apagado',
|
|
||||||
preloadBalanced: 'Balanceado (30 s antes del final)',
|
|
||||||
preloadEarly: 'Temprano (después de 5 s de reproducción)',
|
|
||||||
preloadCustom: 'Personalizado',
|
|
||||||
preloadCustomSeconds: 'Segundos antes del final: {{n}}',
|
|
||||||
infiniteQueue: 'Cola Infinita',
|
infiniteQueue: 'Cola Infinita',
|
||||||
infiniteQueueDesc: 'Agregar automáticamente pistas aleatorias cuando la cola termina',
|
infiniteQueueDesc: 'Agregar automáticamente pistas aleatorias cuando la cola termina',
|
||||||
experimental: 'Experimental',
|
experimental: 'Experimental',
|
||||||
@@ -551,7 +543,7 @@ export const settings = {
|
|||||||
analyticsStrategyAdvancedDesc: 'Escaneo en segundo plano más agresivo. Al terminar, obtienes BPM para todas las pistas y carga inmediata de loudness y forma de onda.',
|
analyticsStrategyAdvancedDesc: 'Escaneo en segundo plano más agresivo. Al terminar, obtienes BPM para todas las pistas y carga inmediata de loudness y forma de onda.',
|
||||||
analyticsStrategyPriorityTitle: 'Prioridad de análisis (siempre activa)',
|
analyticsStrategyPriorityTitle: 'Prioridad de análisis (siempre activa)',
|
||||||
analyticsStrategyPriorityHigh: '1 — Reproducción actual: descargar y analizar primero (bytes de reproducción, backfill de loudness o HTTP cuando haga falta).',
|
analyticsStrategyPriorityHigh: '1 — Reproducción actual: descargar y analizar primero (bytes de reproducción, backfill de loudness o HTTP cuando haga falta).',
|
||||||
analyticsStrategyPriorityMiddle: '2 — Siguientes: ~5 pistas de cola, precarga de la siguiente y vecinas de caché caliente; analiza desde caché/descarga cuando sea posible, con backfill HTTP solo en fallo.',
|
analyticsStrategyPriorityMiddle: '2 — Siguientes: ~5 pistas de cola y vecinas de caché caliente; analiza desde caché/descarga cuando sea posible, con backfill HTTP solo en fallo.',
|
||||||
analyticsStrategyPriorityLow: '3 — Lote de biblioteca (solo Agresiva): cola automática en segundo plano al final; se rellena por watermark (~3× workers) sin esperar a que quede vacía.',
|
analyticsStrategyPriorityLow: '3 — Lote de biblioteca (solo Agresiva): cola automática en segundo plano al final; se rellena por watermark (~3× workers) sin esperar a que quede vacía.',
|
||||||
analyticsStrategyServerLabel: 'Servidor',
|
analyticsStrategyServerLabel: 'Servidor',
|
||||||
analyticsStrategyProgressLabel: 'Progreso del análisis:',
|
analyticsStrategyProgressLabel: 'Progreso del análisis:',
|
||||||
|
|||||||
@@ -487,15 +487,7 @@ export const settings = {
|
|||||||
trackPreviewLocation_favorites: 'Dans les favoris',
|
trackPreviewLocation_favorites: 'Dans les favoris',
|
||||||
trackPreviewLocation_artist: 'Dans les meilleurs morceaux de l\'artiste',
|
trackPreviewLocation_artist: 'Dans les meilleurs morceaux de l\'artiste',
|
||||||
trackPreviewLocation_randomMix: 'Dans Random Mix',
|
trackPreviewLocation_randomMix: 'Dans Random Mix',
|
||||||
preloadMode: 'Précharger la piste suivante',
|
|
||||||
preloadModeDesc: 'Quand commencer à mettre en mémoire tampon la piste suivante',
|
|
||||||
nextTrackBufferingTitle: 'Mise en mémoire tampon',
|
nextTrackBufferingTitle: 'Mise en mémoire tampon',
|
||||||
preloadHotCacheMutualExclusive: "Preload et Hot Cache remplissent le même rôle — un seul peut être actif à la fois. Activer l'un désactive automatiquement l'autre.",
|
|
||||||
preloadOff: 'Désactivé',
|
|
||||||
preloadBalanced: 'Équilibré (30 s avant la fin)',
|
|
||||||
preloadEarly: 'Tôt (après 5 s de lecture)',
|
|
||||||
preloadCustom: 'Personnalisé',
|
|
||||||
preloadCustomSeconds: 'Secondes avant la fin : {{n}}',
|
|
||||||
infiniteQueue: 'File infinie',
|
infiniteQueue: 'File infinie',
|
||||||
infiniteQueueDesc: 'Ajouter automatiquement des morceaux aléatoires quand la file est épuisée',
|
infiniteQueueDesc: 'Ajouter automatiquement des morceaux aléatoires quand la file est épuisée',
|
||||||
experimental: 'Expérimental',
|
experimental: 'Expérimental',
|
||||||
@@ -551,7 +543,7 @@ export const settings = {
|
|||||||
analyticsStrategyAdvancedDesc: 'Analyse de fond plus agressive. Une fois terminée, vous obtenez le BPM pour toutes les pistes et un chargement immédiat de loudness et de formes d’onde.',
|
analyticsStrategyAdvancedDesc: 'Analyse de fond plus agressive. Une fois terminée, vous obtenez le BPM pour toutes les pistes et un chargement immédiat de loudness et de formes d’onde.',
|
||||||
analyticsStrategyPriorityTitle: 'Priorité d’analyse (toujours appliquée)',
|
analyticsStrategyPriorityTitle: 'Priorité d’analyse (toujours appliquée)',
|
||||||
analyticsStrategyPriorityHigh: '1 — En cours de lecture : télécharger et analyser en premier (octets de lecture, backfill loudness ou HTTP si nécessaire).',
|
analyticsStrategyPriorityHigh: '1 — En cours de lecture : télécharger et analyser en premier (octets de lecture, backfill loudness ou HTTP si nécessaire).',
|
||||||
analyticsStrategyPriorityMiddle: '2 — À suivre : les ~5 pistes suivantes de la file, préchargement de la suivante et voisines du hot-cache — analyse depuis cache/téléchargements si possible, backfill HTTP seulement en cas d’absence.',
|
analyticsStrategyPriorityMiddle: '2 — À suivre : les ~5 pistes suivantes de la file et voisines du hot-cache — analyse depuis cache/téléchargements si possible, backfill HTTP seulement en cas d’absence.',
|
||||||
analyticsStrategyPriorityLow: '3 — Lot bibliothèque (Agressive uniquement) : file d’arrière-plan automatique en fin de file ; réalimentée par watermark (~3× workers) sans attendre une file vide.',
|
analyticsStrategyPriorityLow: '3 — Lot bibliothèque (Agressive uniquement) : file d’arrière-plan automatique en fin de file ; réalimentée par watermark (~3× workers) sans attendre une file vide.',
|
||||||
analyticsStrategyServerLabel: 'Server',
|
analyticsStrategyServerLabel: 'Server',
|
||||||
analyticsStrategyProgressLabel: 'Progression de l’analyse :',
|
analyticsStrategyProgressLabel: 'Progression de l’analyse :',
|
||||||
|
|||||||
@@ -489,15 +489,7 @@ export const settings = {
|
|||||||
infiniteQueue: 'Uendelig kø',
|
infiniteQueue: 'Uendelig kø',
|
||||||
infiniteQueueDesc: 'Legg automatisk til tilfeldige spor når køen går tom',
|
infiniteQueueDesc: 'Legg automatisk til tilfeldige spor når køen går tom',
|
||||||
experimental: 'Eksperimentell',
|
experimental: 'Eksperimentell',
|
||||||
preloadMode: 'Forhåndslast neste spor',
|
|
||||||
preloadModeDesc: 'Når buffering av neste spor i køen skal starte',
|
|
||||||
nextTrackBufferingTitle: 'Bufring',
|
nextTrackBufferingTitle: 'Bufring',
|
||||||
preloadHotCacheMutualExclusive: 'Forhåndslasting og Hot Cache tjener samme formål – bare én kan være aktiv om gangen. Å aktivere den ene deaktiverer automatisk den andre.',
|
|
||||||
preloadOff: 'Av',
|
|
||||||
preloadBalanced: 'Balansert (30 s før slutt)',
|
|
||||||
preloadEarly: 'Tidlig (etter 5 s avspilling)',
|
|
||||||
preloadCustom: 'Egendefinert',
|
|
||||||
preloadCustomSeconds: 'Sekunder før slutt: {{n}}',
|
|
||||||
sidebarLyricsStyle: 'Rullestil for tekst',
|
sidebarLyricsStyle: 'Rullestil for tekst',
|
||||||
sidebarLyricsStyleClassic: 'Klassisk',
|
sidebarLyricsStyleClassic: 'Klassisk',
|
||||||
sidebarLyricsStyleClassicDesc: 'Aktiv linje sentreres.',
|
sidebarLyricsStyleClassicDesc: 'Aktiv linje sentreres.',
|
||||||
@@ -552,7 +544,7 @@ export const settings = {
|
|||||||
analyticsStrategyAdvancedDesc: 'Mer aggressiv bakgrunnsskanning. Når den er ferdig får du BPM for alle spor samt umiddelbar lasting av loudness og waveform.',
|
analyticsStrategyAdvancedDesc: 'Mer aggressiv bakgrunnsskanning. Når den er ferdig får du BPM for alle spor samt umiddelbar lasting av loudness og waveform.',
|
||||||
analyticsStrategyPriorityTitle: 'Analyseprioritet (alltid aktiv)',
|
analyticsStrategyPriorityTitle: 'Analyseprioritet (alltid aktiv)',
|
||||||
analyticsStrategyPriorityHigh: '1 — Spiller nå: last ned og analyser først (avspillingsbytes, loudness-backfill eller HTTP ved behov).',
|
analyticsStrategyPriorityHigh: '1 — Spiller nå: last ned og analyser først (avspillingsbytes, loudness-backfill eller HTTP ved behov).',
|
||||||
analyticsStrategyPriorityMiddle: '2 — Neste opp: de neste ~5 køsporene, preload av neste og hot-cache-naboer — analyser fra cache/nedlastede bytes når mulig; HTTP-backfill kun ved bom.',
|
analyticsStrategyPriorityMiddle: '2 — Neste opp: de neste ~5 køsporene og hot-cache-naboer — analyser fra cache/nedlastede bytes når mulig; HTTP-backfill kun ved bom.',
|
||||||
analyticsStrategyPriorityLow: '3 — Bibliotekbatch (kun Aggressiv): automatisk bakgrunnskø bakerst; fylles på ved watermark (~3× workers) uten å vente på tom kø.',
|
analyticsStrategyPriorityLow: '3 — Bibliotekbatch (kun Aggressiv): automatisk bakgrunnskø bakerst; fylles på ved watermark (~3× workers) uten å vente på tom kø.',
|
||||||
analyticsStrategyServerLabel: 'Server',
|
analyticsStrategyServerLabel: 'Server',
|
||||||
analyticsStrategyProgressLabel: 'Analysefremdrift:',
|
analyticsStrategyProgressLabel: 'Analysefremdrift:',
|
||||||
|
|||||||
@@ -487,15 +487,7 @@ export const settings = {
|
|||||||
trackPreviewLocation_favorites: 'In favorieten',
|
trackPreviewLocation_favorites: 'In favorieten',
|
||||||
trackPreviewLocation_artist: 'In top-tracks van artiest',
|
trackPreviewLocation_artist: 'In top-tracks van artiest',
|
||||||
trackPreviewLocation_randomMix: 'In Random Mix',
|
trackPreviewLocation_randomMix: 'In Random Mix',
|
||||||
preloadMode: 'Volgend nummer vooraf laden',
|
|
||||||
preloadModeDesc: 'Wanneer met het bufferen van het volgende nummer wordt begonnen',
|
|
||||||
nextTrackBufferingTitle: 'Buffering',
|
nextTrackBufferingTitle: 'Buffering',
|
||||||
preloadHotCacheMutualExclusive: 'Preload en Hot Cache dienen hetzelfde doel — slechts één kan tegelijk actief zijn. Het inschakelen van de ene schakelt de andere automatisch uit.',
|
|
||||||
preloadOff: 'Uit',
|
|
||||||
preloadBalanced: 'Gebalanceerd (30 s voor einde)',
|
|
||||||
preloadEarly: 'Vroeg (na 5 s afspelen)',
|
|
||||||
preloadCustom: 'Aangepast',
|
|
||||||
preloadCustomSeconds: 'Seconden voor einde: {{n}}',
|
|
||||||
infiniteQueue: 'Oneindige wachtrij',
|
infiniteQueue: 'Oneindige wachtrij',
|
||||||
infiniteQueueDesc: 'Automatisch willekeurige nummers toevoegen als de wachtrij leeg raakt',
|
infiniteQueueDesc: 'Automatisch willekeurige nummers toevoegen als de wachtrij leeg raakt',
|
||||||
experimental: 'Experimenteel',
|
experimental: 'Experimenteel',
|
||||||
@@ -551,7 +543,7 @@ export const settings = {
|
|||||||
analyticsStrategyAdvancedDesc: 'Agressievere achtergrondscan. Na voltooiing krijg je BPM voor alle tracks plus directe loudness- en waveform-lading.',
|
analyticsStrategyAdvancedDesc: 'Agressievere achtergrondscan. Na voltooiing krijg je BPM voor alle tracks plus directe loudness- en waveform-lading.',
|
||||||
analyticsStrategyPriorityTitle: 'Analyseprioriteit (altijd actief)',
|
analyticsStrategyPriorityTitle: 'Analyseprioriteit (altijd actief)',
|
||||||
analyticsStrategyPriorityHigh: '1 — Nu afspelen: eerst downloaden en analyseren (playback-bytes, loudness-backfill of HTTP indien nodig).',
|
analyticsStrategyPriorityHigh: '1 — Nu afspelen: eerst downloaden en analyseren (playback-bytes, loudness-backfill of HTTP indien nodig).',
|
||||||
analyticsStrategyPriorityMiddle: '2 — Hierna: de volgende ~5 wachtrijtracks, preload-next en hot-cache-buren — analyse vanuit cache/downloads waar mogelijk; HTTP-backfill alleen bij miss.',
|
analyticsStrategyPriorityMiddle: '2 — Hierna: de volgende ~5 wachtrijtracks en hot-cache-buren — analyse vanuit cache/downloads waar mogelijk; HTTP-backfill alleen bij miss.',
|
||||||
analyticsStrategyPriorityLow: '3 — Bibliotheekbatch (alleen Agressief): automatische achtergrondwachtrij achteraan; wordt bijgevuld via watermark (~3× workers) zonder op een lege wachtrij te wachten.',
|
analyticsStrategyPriorityLow: '3 — Bibliotheekbatch (alleen Agressief): automatische achtergrondwachtrij achteraan; wordt bijgevuld via watermark (~3× workers) zonder op een lege wachtrij te wachten.',
|
||||||
analyticsStrategyServerLabel: 'Server',
|
analyticsStrategyServerLabel: 'Server',
|
||||||
analyticsStrategyProgressLabel: 'Analysevoortgang:',
|
analyticsStrategyProgressLabel: 'Analysevoortgang:',
|
||||||
|
|||||||
@@ -492,15 +492,7 @@ export const settings = {
|
|||||||
trackPreviewLocation_favorites: 'În favorite',
|
trackPreviewLocation_favorites: 'În favorite',
|
||||||
trackPreviewLocation_artist: 'În top piese ale artistului',
|
trackPreviewLocation_artist: 'În top piese ale artistului',
|
||||||
trackPreviewLocation_randomMix: 'În Mixul Aleatoriu',
|
trackPreviewLocation_randomMix: 'În Mixul Aleatoriu',
|
||||||
preloadMode: 'Preîncarcă Următoarea Piesă',
|
|
||||||
preloadModeDesc: 'Când să înceapă preîncărcarea următoarei piese din coadă',
|
|
||||||
nextTrackBufferingTitle: 'Preîncarcă',
|
nextTrackBufferingTitle: 'Preîncarcă',
|
||||||
preloadHotCacheMutualExclusive: 'Preîncarcarea și Hot Cache au același scop — doar una poate fi activată deodată. Pornirea unei opțiuni o dezactivează pe cealaltă.',
|
|
||||||
preloadOff: 'Oprit',
|
|
||||||
preloadBalanced: 'Balansat (30 s înainte de sfârșit)',
|
|
||||||
preloadEarly: 'Devreme (după 5 s de redare)',
|
|
||||||
preloadCustom: 'Personalizat',
|
|
||||||
preloadCustomSeconds: 'Secunde înainte de sfârșit: {{n}}',
|
|
||||||
infiniteQueue: 'Coadă Infinită',
|
infiniteQueue: 'Coadă Infinită',
|
||||||
infiniteQueueDesc: 'Adaugă automat piese aleatorii când coada se golește',
|
infiniteQueueDesc: 'Adaugă automat piese aleatorii când coada se golește',
|
||||||
experimental: 'Experimental',
|
experimental: 'Experimental',
|
||||||
@@ -551,7 +543,7 @@ export const settings = {
|
|||||||
analyticsStrategyAdvancedDesc: 'Scanare de fundal mai agresivă. După finalizare, primești BPM pentru toate piesele și încărcare instantă pentru loudness și waveform.',
|
analyticsStrategyAdvancedDesc: 'Scanare de fundal mai agresivă. După finalizare, primești BPM pentru toate piesele și încărcare instantă pentru loudness și waveform.',
|
||||||
analyticsStrategyPriorityTitle: 'Prioritate analiză (mereu activă)',
|
analyticsStrategyPriorityTitle: 'Prioritate analiză (mereu activă)',
|
||||||
analyticsStrategyPriorityHigh: '1 — Redare curentă: descarcă și analizează mai întâi (bytes de playback, loudness backfill sau HTTP când e nevoie).',
|
analyticsStrategyPriorityHigh: '1 — Redare curentă: descarcă și analizează mai întâi (bytes de playback, loudness backfill sau HTTP când e nevoie).',
|
||||||
analyticsStrategyPriorityMiddle: '2 — Urmează: următoarele ~5 piese din coadă, preload pentru următoarea și vecinii din hot-cache — analiză din cache/descărcări când se poate; backfill HTTP doar la miss.',
|
analyticsStrategyPriorityMiddle: '2 — Urmează: următoarele ~5 piese din coadă și vecinii din hot-cache — analiză din cache/descărcări când se poate; backfill HTTP doar la miss.',
|
||||||
analyticsStrategyPriorityLow: '3 — Lot bibliotecă (doar Agresivă): coadă automată în fundal la final; reumplere pe watermark (~3× workers) fără a aștepta golirea cozii.',
|
analyticsStrategyPriorityLow: '3 — Lot bibliotecă (doar Agresivă): coadă automată în fundal la final; reumplere pe watermark (~3× workers) fără a aștepta golirea cozii.',
|
||||||
analyticsStrategyServerLabel: 'Server',
|
analyticsStrategyServerLabel: 'Server',
|
||||||
analyticsStrategyProgressLabel: 'Progres analiză:',
|
analyticsStrategyProgressLabel: 'Progres analiză:',
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ export const settings = {
|
|||||||
'Запускает фоновый анализ всей библиотеки и скачивает треки для анализа.',
|
'Запускает фоновый анализ всей библиотеки и скачивает треки для анализа.',
|
||||||
analyticsStrategyPriorityTitle: 'Приоритет анализа (всегда)',
|
analyticsStrategyPriorityTitle: 'Приоритет анализа (всегда)',
|
||||||
analyticsStrategyPriorityHigh: '1 — Сейчас играет: скачивание и анализ в первую очередь (байты воспроизведения, loudness backfill или HTTP при необходимости).',
|
analyticsStrategyPriorityHigh: '1 — Сейчас играет: скачивание и анализ в первую очередь (байты воспроизведения, loudness backfill или HTTP при необходимости).',
|
||||||
analyticsStrategyPriorityMiddle: '2 — Следующие: ~5 треков в очереди, preload next и соседи hot cache — анализ из уже скачанных байтов, HTTP backfill только при промахе.',
|
analyticsStrategyPriorityMiddle: '2 — Следующие: ~5 треков в очереди и соседи hot cache — анализ из уже скачанных байтов, HTTP backfill только при промахе.',
|
||||||
analyticsStrategyPriorityLow: '3 — Библиотечный batch (только Агрессивный): автоматическая фоновая очередь в хвосте; пополняется по watermark (~3× потоков), не дожидаясь полного опустошения.',
|
analyticsStrategyPriorityLow: '3 — Библиотечный batch (только Агрессивный): автоматическая фоновая очередь в хвосте; пополняется по watermark (~3× потоков), не дожидаясь полного опустошения.',
|
||||||
analyticsStrategyServerLabel: 'Сервер',
|
analyticsStrategyServerLabel: 'Сервер',
|
||||||
analyticsStrategyProgressLabel: 'Прогресс анализа:',
|
analyticsStrategyProgressLabel: 'Прогресс анализа:',
|
||||||
@@ -598,15 +598,7 @@ export const settings = {
|
|||||||
trackPreviewLocation_favorites: 'В избранном',
|
trackPreviewLocation_favorites: 'В избранном',
|
||||||
trackPreviewLocation_artist: 'В топ-треках исполнителя',
|
trackPreviewLocation_artist: 'В топ-треках исполнителя',
|
||||||
trackPreviewLocation_randomMix: 'В Random Mix',
|
trackPreviewLocation_randomMix: 'В Random Mix',
|
||||||
preloadMode: 'Предзагрузка следующего трека',
|
|
||||||
preloadModeDesc: 'Когда начинать буферизацию следующего в очереди',
|
|
||||||
nextTrackBufferingTitle: 'Буферизация',
|
nextTrackBufferingTitle: 'Буферизация',
|
||||||
preloadHotCacheMutualExclusive: 'Предзагрузка и Hot Cache выполняют одну функцию — одновременно может быть активна только одна. Включение одной автоматически отключает другую.',
|
|
||||||
preloadOff: 'Выкл.',
|
|
||||||
preloadBalanced: 'Умеренно (за 30 с до конца)',
|
|
||||||
preloadEarly: 'Рано (через 5 с после старта)',
|
|
||||||
preloadCustom: 'Свой интервал',
|
|
||||||
preloadCustomSeconds: 'Секунд до конца: {{n}}',
|
|
||||||
infiniteQueue: 'Бесконечная очередь',
|
infiniteQueue: 'Бесконечная очередь',
|
||||||
infiniteQueueDesc: 'Подмешивать случайные треки, когда очередь закончится',
|
infiniteQueueDesc: 'Подмешивать случайные треки, когда очередь закончится',
|
||||||
experimental: 'Экспериментально',
|
experimental: 'Экспериментально',
|
||||||
|
|||||||
@@ -486,15 +486,7 @@ export const settings = {
|
|||||||
trackPreviewLocation_favorites: '收藏中',
|
trackPreviewLocation_favorites: '收藏中',
|
||||||
trackPreviewLocation_artist: '艺人热门曲目中',
|
trackPreviewLocation_artist: '艺人热门曲目中',
|
||||||
trackPreviewLocation_randomMix: 'Random Mix 中',
|
trackPreviewLocation_randomMix: 'Random Mix 中',
|
||||||
preloadMode: '预加载下一曲目',
|
|
||||||
preloadModeDesc: '何时开始缓冲队列中的下一曲目',
|
|
||||||
nextTrackBufferingTitle: '缓冲',
|
nextTrackBufferingTitle: '缓冲',
|
||||||
preloadHotCacheMutualExclusive: '预加载与热缓存用途相同——两者不能同时启用。启用其中一个会自动禁用另一个。',
|
|
||||||
preloadOff: '关闭',
|
|
||||||
preloadBalanced: '均衡(结束前30秒)',
|
|
||||||
preloadEarly: '提前(播放5秒后)',
|
|
||||||
preloadCustom: '自定义',
|
|
||||||
preloadCustomSeconds: '结束前秒数:{{n}}',
|
|
||||||
infiniteQueue: '无限队列',
|
infiniteQueue: '无限队列',
|
||||||
infiniteQueueDesc: '队列播完时自动追加随机曲目',
|
infiniteQueueDesc: '队列播完时自动追加随机曲目',
|
||||||
experimental: '实验性',
|
experimental: '实验性',
|
||||||
@@ -550,7 +542,7 @@ export const settings = {
|
|||||||
analyticsStrategyAdvancedDesc: '更激进的后台扫描。完成后可获得所有歌曲 BPM,并可即时加载响度与波形。',
|
analyticsStrategyAdvancedDesc: '更激进的后台扫描。完成后可获得所有歌曲 BPM,并可即时加载响度与波形。',
|
||||||
analyticsStrategyPriorityTitle: '分析优先级(始终生效)',
|
analyticsStrategyPriorityTitle: '分析优先级(始终生效)',
|
||||||
analyticsStrategyPriorityHigh: '1 — 当前播放:优先下载并分析(播放字节、响度回填,或在需要时走 HTTP)。',
|
analyticsStrategyPriorityHigh: '1 — 当前播放:优先下载并分析(播放字节、响度回填,或在需要时走 HTTP)。',
|
||||||
analyticsStrategyPriorityMiddle: '2 — 即将播放:队列中接下来的约 5 首、预加载下一首以及热缓存邻近曲目——优先从缓存/已下载字节分析,未命中时才做 HTTP 回填。',
|
analyticsStrategyPriorityMiddle: '2 — 即将播放:队列中接下来的约 5 首及热缓存邻近曲目——优先从缓存/已下载字节分析,未命中时才做 HTTP 回填。',
|
||||||
analyticsStrategyPriorityLow: '3 — 库批处理(仅“激进”):自动后台队列排在末尾;按水位线(约 3× workers)补充,无需等待队列清空。',
|
analyticsStrategyPriorityLow: '3 — 库批处理(仅“激进”):自动后台队列排在末尾;按水位线(约 3× workers)补充,无需等待队列清空。',
|
||||||
analyticsStrategyServerLabel: '服务器',
|
analyticsStrategyServerLabel: '服务器',
|
||||||
analyticsStrategyProgressLabel: '分析进度:',
|
analyticsStrategyProgressLabel: '分析进度:',
|
||||||
|
|||||||
@@ -207,46 +207,22 @@ export function handleAudioProgress(
|
|||||||
markStoreProgressCommit(nowCommit);
|
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 {
|
const {
|
||||||
gaplessEnabled,
|
gaplessEnabled,
|
||||||
preloadMode,
|
|
||||||
preloadCustomSeconds,
|
|
||||||
hotCacheEnabled,
|
hotCacheEnabled,
|
||||||
crossfadeEnabled,
|
crossfadeEnabled,
|
||||||
crossfadeSecs,
|
crossfadeSecs,
|
||||||
} = useAuthStore.getState();
|
} = useAuthStore.getState();
|
||||||
const remaining = dur - current_time;
|
const remaining = dur - current_time;
|
||||||
|
|
||||||
// Gapless chain: always triggers at 30s regardless of preloadMode.
|
|
||||||
const shouldChainGapless = gaplessEnabled && remaining < 30 && remaining > 0;
|
const shouldChainGapless = gaplessEnabled && remaining < 30 && remaining > 0;
|
||||||
// Byte pre-download: skip when Hot Cache is active (it already handles buffering).
|
// Crossfade needs the next track bytes ready before the fade window.
|
||||||
// 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)
|
|
||||||
);
|
|
||||||
const crossfadeWindowSecs = Math.max(8, Math.min(30, crossfadeSecs + 6));
|
const crossfadeWindowSecs = Math.max(8, Math.min(30, crossfadeSecs + 6));
|
||||||
const shouldBytePreloadForCrossfade =
|
const shouldBytePreloadForCrossfade =
|
||||||
!gaplessEnabled && crossfadeEnabled && remaining < crossfadeWindowSecs && remaining > 0;
|
!hotCacheEnabled && !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
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldChainGapless || shouldBytePreload || shouldPreloadLocalFileAnalysis || gaplessEnabled) {
|
if (shouldChainGapless || shouldBytePreloadForCrossfade || gaplessEnabled) {
|
||||||
const { queueItems, queueIndex, repeatMode } = store;
|
const { queueItems, queueIndex, repeatMode } = store;
|
||||||
const nextIdx = queueIndex + 1;
|
const nextIdx = queueIndex + 1;
|
||||||
// Next track for preload/chain. The resolver bridge keeps the window around
|
// Next track for preload/chain. The resolver bridge keeps the window around
|
||||||
@@ -281,11 +257,10 @@ export function handleAudioProgress(
|
|||||||
const serverId = getPlaybackCacheServerKey();
|
const serverId = getPlaybackCacheServerKey();
|
||||||
const analysisServerId = getPlaybackIndexKey();
|
const analysisServerId = getPlaybackIndexKey();
|
||||||
const nextUrl = resolvePlaybackUrl(nextTrack.id, serverId);
|
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 (
|
if (
|
||||||
(shouldBytePreload || shouldBytePreloadForGaplessBackup || (shouldPreloadLocalFileAnalysis && nextIsLocalFile))
|
(shouldBytePreloadForCrossfade || shouldBytePreloadForGaplessBackup)
|
||||||
&& nextTrack.id !== getBytePreloadingId()
|
&& nextTrack.id !== getBytePreloadingId()
|
||||||
) {
|
) {
|
||||||
setBytePreloadingId(nextTrack.id);
|
setBytePreloadingId(nextTrack.id);
|
||||||
@@ -296,10 +271,8 @@ export function handleAudioProgress(
|
|||||||
console.info('[psysonic][preload-request]', {
|
console.info('[psysonic][preload-request]', {
|
||||||
nextTrackId: nextTrack.id,
|
nextTrackId: nextTrack.id,
|
||||||
nextUrl,
|
nextUrl,
|
||||||
shouldBytePreload,
|
shouldBytePreloadForCrossfade,
|
||||||
shouldBytePreloadForGaplessBackup,
|
shouldBytePreloadForGaplessBackup,
|
||||||
shouldPreloadLocalFileAnalysis,
|
|
||||||
nextIsLocalFile,
|
|
||||||
remaining,
|
remaining,
|
||||||
gaplessEnabled,
|
gaplessEnabled,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,23 +6,19 @@ type SetState = (
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Persistent plumbing settings that don't fit a more specific domain:
|
* Persistent plumbing settings that don't fit a more specific domain:
|
||||||
* runtime logging level, Navidrome `getNowPlaying` toggle, preload
|
* runtime logging level, Navidrome `getNowPlaying` toggle, audiobook
|
||||||
* mode + custom seconds, audiobook exclusion, genre blacklist.
|
* exclusion, genre blacklist.
|
||||||
*/
|
*/
|
||||||
export function createPlumbingSettingsActions(set: SetState): Pick<
|
export function createPlumbingSettingsActions(set: SetState): Pick<
|
||||||
AuthState,
|
AuthState,
|
||||||
| 'setLoggingMode'
|
| 'setLoggingMode'
|
||||||
| 'setNowPlayingEnabled'
|
| 'setNowPlayingEnabled'
|
||||||
| 'setPreloadMode'
|
|
||||||
| 'setPreloadCustomSeconds'
|
|
||||||
| 'setExcludeAudiobooks'
|
| 'setExcludeAudiobooks'
|
||||||
| 'setCustomGenreBlacklist'
|
| 'setCustomGenreBlacklist'
|
||||||
> {
|
> {
|
||||||
return {
|
return {
|
||||||
setLoggingMode: (v) => set({ loggingMode: v }),
|
setLoggingMode: (v) => set({ loggingMode: v }),
|
||||||
setNowPlayingEnabled: (v) => set({ nowPlayingEnabled: v }),
|
setNowPlayingEnabled: (v) => set({ nowPlayingEnabled: v }),
|
||||||
setPreloadMode: (v) => set({ preloadMode: v }),
|
|
||||||
setPreloadCustomSeconds: (v) => set({ preloadCustomSeconds: v }),
|
|
||||||
setExcludeAudiobooks: (v) => set({ excludeAudiobooks: v }),
|
setExcludeAudiobooks: (v) => set({ excludeAudiobooks: v }),
|
||||||
setCustomGenreBlacklist: (v) => set({ customGenreBlacklist: v }),
|
setCustomGenreBlacklist: (v) => set({ customGenreBlacklist: v }),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
* Covers Zustand persist's hydration path (via `useAuthStore.persist.rehydrate()`):
|
* Covers Zustand persist's hydration path (via `useAuthStore.persist.rehydrate()`):
|
||||||
* existing localStorage shapes load, missing fields default to the store's
|
* existing localStorage shapes load, missing fields default to the store's
|
||||||
* initial values, corrupt JSON does not crash bootstrap, the
|
* initial values, corrupt JSON does not crash bootstrap, the
|
||||||
* hotCacheEnabled / preloadMode≠'off' conflicting-legacy migration clears
|
* legacy field stripping, and a few smaller field-level migrations.
|
||||||
* both, and a few smaller field-level migrations.
|
|
||||||
*
|
*
|
||||||
* Also pins the **synchronous storage** invariant called out in `CLAUDE.md`
|
* Also pins the **synchronous storage** invariant called out in `CLAUDE.md`
|
||||||
* ("never switch to async storage") — regression §2 of the pre-refactor
|
* ("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.gaplessEnabled).toBe(false);
|
||||||
expect(s.replayGainEnabled).toBe(false);
|
expect(s.replayGainEnabled).toBe(false);
|
||||||
expect(s.normalizationEngine).toBe('off');
|
expect(s.normalizationEngine).toBe('off');
|
||||||
expect(s.preloadMode).toBe('balanced');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('preserves saved fields verbatim when present', async () => {
|
it('preserves saved fields verbatim when present', async () => {
|
||||||
@@ -94,34 +92,21 @@ describe('hydration — corrupt / unexpected input', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('onRehydrate migrations', () => {
|
describe('onRehydrate migrations', () => {
|
||||||
it('clears the conflicting hotCacheEnabled + preloadMode≠"off" legacy combo', async () => {
|
it('keeps hotCacheEnabled when legacy preloadMode fields are present', async () => {
|
||||||
writePersistedState({
|
writePersistedState({
|
||||||
servers: [],
|
servers: [],
|
||||||
activeServerId: null,
|
activeServerId: null,
|
||||||
hotCacheEnabled: true,
|
hotCacheEnabled: true,
|
||||||
preloadMode: 'balanced',
|
preloadMode: 'balanced',
|
||||||
});
|
preloadCustomSeconds: 45,
|
||||||
|
|
||||||
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',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await useAuthStore.persist.rehydrate();
|
await useAuthStore.persist.rehydrate();
|
||||||
|
|
||||||
const s = useAuthStore.getState();
|
const s = useAuthStore.getState();
|
||||||
expect(s.hotCacheEnabled).toBe(true);
|
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 () => {
|
it('migrates a legacy `waveform` seekbarStyle to `truewave`', async () => {
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ describe('trivial pass-through setters', () => {
|
|||||||
['setMaxCacheMb', 'maxCacheMb', 2048],
|
['setMaxCacheMb', 'maxCacheMb', 2048],
|
||||||
['setHotCacheMaxMb', 'hotCacheMaxMb', 1024],
|
['setHotCacheMaxMb', 'hotCacheMaxMb', 1024],
|
||||||
['setHotCacheDebounceSec', 'hotCacheDebounceSec', 60],
|
['setHotCacheDebounceSec', 'hotCacheDebounceSec', 60],
|
||||||
['setPreloadCustomSeconds', 'preloadCustomSeconds', 45],
|
|
||||||
])('%s stores a numeric value', (setter, key, value) => {
|
])('%s stores a numeric value', (setter, key, value) => {
|
||||||
(useAuthStore.getState() as unknown as Record<string, (v: unknown) => void>)[setter](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);
|
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', () => {
|
describe('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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('setDiscordCoverSource accepts none / apple / server', () => {
|
it('setDiscordCoverSource accepts none / apple / server', () => {
|
||||||
for (const src of ['none', 'apple', 'server'] as const) {
|
for (const src of ['none', 'apple', 'server'] as const) {
|
||||||
useAuthStore.getState().setDiscordCoverSource(src);
|
useAuthStore.getState().setDiscordCoverSource(src);
|
||||||
|
|||||||
@@ -59,8 +59,6 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
trackPreviewLocations: { ...DEFAULT_TRACK_PREVIEW_LOCATIONS },
|
trackPreviewLocations: { ...DEFAULT_TRACK_PREVIEW_LOCATIONS },
|
||||||
trackPreviewStartRatio: 0.33,
|
trackPreviewStartRatio: 0.33,
|
||||||
trackPreviewDurationSec: 30,
|
trackPreviewDurationSec: 30,
|
||||||
preloadMode: 'balanced',
|
|
||||||
preloadCustomSeconds: 30,
|
|
||||||
infiniteQueueEnabled: false,
|
infiniteQueueEnabled: false,
|
||||||
preservePlayNextOrder: false,
|
preservePlayNextOrder: false,
|
||||||
showArtistImages: false,
|
showArtistImages: false,
|
||||||
|
|||||||
@@ -34,11 +34,9 @@ import type {
|
|||||||
* and writes the one-shot Linux smooth-scroll migration sentinel.
|
* and writes the one-shot Linux smooth-scroll migration sentinel.
|
||||||
*/
|
*/
|
||||||
export function computeAuthStoreRehydration(state: AuthState): Partial<AuthState> {
|
export function computeAuthStoreRehydration(state: AuthState): Partial<AuthState> {
|
||||||
// If both hot cache and preload were enabled before mutual exclusion was enforced, reset both.
|
// Drop removed preload-next-track settings from legacy persist blobs.
|
||||||
const conflictingLegacyState =
|
delete (state as { preloadMode?: unknown }).preloadMode;
|
||||||
state.hotCacheEnabled && state.preloadMode !== 'off'
|
delete (state as { preloadCustomSeconds?: unknown }).preloadCustomSeconds;
|
||||||
? { hotCacheEnabled: false, preloadMode: 'off' as const }
|
|
||||||
: {};
|
|
||||||
|
|
||||||
// Migrate lyricsServerFirst + enableNeteaselyrics → lyricsSources (one-time).
|
// Migrate lyricsServerFirst + enableNeteaselyrics → lyricsSources (one-time).
|
||||||
// Only for an *existing* persisted state (upgrade from a build without
|
// Only for an *existing* persisted state (upgrade from a build without
|
||||||
@@ -166,7 +164,6 @@ export function computeAuthStoreRehydration(state: AuthState): Partial<AuthState
|
|||||||
loudnessTargetLufs: targetSan,
|
loudnessTargetLufs: targetSan,
|
||||||
loudnessPreAnalysisAttenuationDb: preSan,
|
loudnessPreAnalysisAttenuationDb: preSan,
|
||||||
loudnessPreIsRefV1: true,
|
loudnessPreIsRefV1: true,
|
||||||
...conflictingLegacyState,
|
|
||||||
...lyricsSourcesMigrated,
|
...lyricsSourcesMigrated,
|
||||||
...youLyPlusMigrated,
|
...youLyPlusMigrated,
|
||||||
...wheelSmoothOneTime,
|
...wheelSmoothOneTime,
|
||||||
|
|||||||
@@ -115,8 +115,6 @@ export interface AuthState {
|
|||||||
trackPreviewStartRatio: number;
|
trackPreviewStartRatio: number;
|
||||||
/** Preview window length in seconds. Default 30 s. */
|
/** Preview window length in seconds. Default 30 s. */
|
||||||
trackPreviewDurationSec: number;
|
trackPreviewDurationSec: number;
|
||||||
preloadMode: 'off' | 'balanced' | 'early' | 'custom';
|
|
||||||
preloadCustomSeconds: number;
|
|
||||||
infiniteQueueEnabled: boolean;
|
infiniteQueueEnabled: boolean;
|
||||||
preservePlayNextOrder: boolean;
|
preservePlayNextOrder: boolean;
|
||||||
showArtistImages: boolean;
|
showArtistImages: boolean;
|
||||||
@@ -307,8 +305,6 @@ export interface AuthState {
|
|||||||
setTrackPreviewLocation: (location: TrackPreviewLocation, enabled: boolean) => void;
|
setTrackPreviewLocation: (location: TrackPreviewLocation, enabled: boolean) => void;
|
||||||
setTrackPreviewStartRatio: (v: number) => void;
|
setTrackPreviewStartRatio: (v: number) => void;
|
||||||
setTrackPreviewDurationSec: (v: number) => void;
|
setTrackPreviewDurationSec: (v: number) => void;
|
||||||
setPreloadMode: (v: 'off' | 'balanced' | 'early' | 'custom') => void;
|
|
||||||
setPreloadCustomSeconds: (v: number) => void;
|
|
||||||
setInfiniteQueueEnabled: (v: boolean) => void;
|
setInfiniteQueueEnabled: (v: boolean) => void;
|
||||||
setPreservePlayNextOrder: (v: boolean) => void;
|
setPreservePlayNextOrder: (v: boolean) => void;
|
||||||
setShowArtistImages: (v: boolean) => void;
|
setShowArtistImages: (v: boolean) => void;
|
||||||
|
|||||||
@@ -46,12 +46,11 @@ export function collectLoudnessBackfillWindowTrackIds(
|
|||||||
return Array.from(ids);
|
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(
|
export function collectPlaybackMiddlePriorityTrackIds(
|
||||||
queue: QueueItemRef[],
|
queue: QueueItemRef[],
|
||||||
queueIndex: number,
|
queueIndex: number,
|
||||||
currentTrack: Track | null,
|
currentTrack: Track | null,
|
||||||
preloadMode: 'off' | 'balanced' | 'early' | 'custom',
|
|
||||||
): string[] {
|
): string[] {
|
||||||
const ids = new Set<string>();
|
const ids = new Set<string>();
|
||||||
const start = Math.max(0, queueIndex + 1);
|
const start = Math.max(0, queueIndex + 1);
|
||||||
@@ -60,13 +59,6 @@ export function collectPlaybackMiddlePriorityTrackIds(
|
|||||||
const tid = queue[i]?.trackId;
|
const tid = queue[i]?.trackId;
|
||||||
if (tid && tid !== currentTrack?.id) ids.add(tid);
|
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);
|
return Array.from(ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user