From 3643a78cd6bf6dabd03e74efec6557148ca87c86 Mon Sep 17 00:00:00 2001 From: Psychotoxical Date: Wed, 8 Apr 2026 17:57:49 +0200 Subject: [PATCH] perf: drastically reduce background API requests to Navidrome (v1.34.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NowPlaying polling: only active while dropdown is open + respects Page Visibility API — was firing every 10s unconditionally (~8.6k req/day) - Connection check interval: 30s → 120s (4× reduction) - Queue sync debounce: 1.5s → 5s (prevents burst on rapid skip) - Rating prefetch: add 7-minute in-memory TTL cache for artist/album ratings — repeated random album loads no longer re-fetch known ratings Fixes server log flooding reported by users behind reverse proxies (Traefik). Co-Authored-By: Claude Sonnet 4.6 --- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- src/api/subsonic.ts | 41 +++++++++++++++++++++++---- src/components/NowPlayingDropdown.tsx | 12 ++++---- src/hooks/useConnectionStatus.ts | 2 +- src/store/authStore.ts | 2 +- src/store/playerStore.ts | 2 +- 9 files changed, 47 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 6ee1e3f4..2f921246 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "psysonic", - "version": "1.34.4", + "version": "1.34.5", "private": true, "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index fa5dbdae..c10e9176 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3339,7 +3339,7 @@ dependencies = [ [[package]] name = "psysonic" -version = "1.34.4" +version = "1.34.5" dependencies = [ "biquad", "discord-rich-presence", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f78e8308..ba57205c 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "psysonic" -version = "1.34.4" +version = "1.34.5" description = "Psysonic Desktop Music Player" authors = [] license = "" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0260445b..13b2257f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Psysonic", - "version": "1.34.4", + "version": "1.34.5", "identifier": "dev.psysonic.player", "build": { "beforeDevCommand": "npm run dev", diff --git a/src/api/subsonic.ts b/src/api/subsonic.ts index bc5caf9d..84465c69 100644 --- a/src/api/subsonic.ts +++ b/src/api/subsonic.ts @@ -332,6 +332,19 @@ export async function getAlbum(id: string): Promise<{ album: SubsonicAlbum; song } const MIX_RATING_PREFETCH_CONCURRENCY = 8; +const RATING_CACHE_TTL = 7 * 60 * 1000; // 7 minutes +const ratingCache = new Map(); + +function getCachedRating(key: string): number | undefined | null { + const entry = ratingCache.get(key); + if (!entry) return null; // cache miss + if (Date.now() > entry.expiresAt) { ratingCache.delete(key); return null; } + return entry.value; +} + +function setCachedRating(key: string, value: number | undefined): void { + ratingCache.set(key, { value, expiresAt: Date.now() + RATING_CACHE_TTL }); +} function parseEntityUserRating(v: unknown): number | undefined { if (v === null || v === undefined) return undefined; @@ -348,22 +361,30 @@ export async function prefetchArtistUserRatings( const unique = [...new Set(ids.filter(Boolean))]; const out = new Map(); if (!unique.length) return out; + const uncached: string[] = []; + for (const id of unique) { + const cached = getCachedRating(`artist:${id}`); + if (cached !== null) { if (cached !== undefined) out.set(id, cached); } + else uncached.push(id); + } + if (!uncached.length) return out; let next = 0; async function worker() { for (;;) { const i = next++; - if (i >= unique.length) return; - const id = unique[i]; + if (i >= uncached.length) return; + const id = uncached[i]; try { const { artist } = await getArtist(id); const r = parseEntityUserRating(artist.userRating); + setCachedRating(`artist:${id}`, r); if (r !== undefined) out.set(id, r); } catch { /* ignore */ } } } - const nWorkers = Math.min(concurrency, unique.length); + const nWorkers = Math.min(concurrency, uncached.length); await Promise.all(Array.from({ length: nWorkers }, () => worker())); return out; } @@ -376,22 +397,30 @@ export async function prefetchAlbumUserRatings( const unique = [...new Set(ids.filter(Boolean))]; const out = new Map(); if (!unique.length) return out; + const uncached: string[] = []; + for (const id of unique) { + const cached = getCachedRating(`album:${id}`); + if (cached !== null) { if (cached !== undefined) out.set(id, cached); } + else uncached.push(id); + } + if (!uncached.length) return out; let next = 0; async function worker() { for (;;) { const i = next++; - if (i >= unique.length) return; - const id = unique[i]; + if (i >= uncached.length) return; + const id = uncached[i]; try { const { album } = await getAlbum(id); const r = parseEntityUserRating(album.userRating); + setCachedRating(`album:${id}`, r); if (r !== undefined) out.set(id, r); } catch { /* ignore */ } } } - const nWorkers = Math.min(concurrency, unique.length); + const nWorkers = Math.min(concurrency, uncached.length); await Promise.all(Array.from({ length: nWorkers }, () => worker())); return out; } diff --git a/src/components/NowPlayingDropdown.tsx b/src/components/NowPlayingDropdown.tsx index 38dcb599..7a5dd64d 100644 --- a/src/components/NowPlayingDropdown.tsx +++ b/src/components/NowPlayingDropdown.tsx @@ -36,16 +36,14 @@ export default function NowPlayingDropdown() { }); }; - // Poll in background so the badge stays current without opening the dropdown + // Poll only while the dropdown is open AND the page is visible. useEffect(() => { + if (!isOpen) return; fetchNowPlaying(); - const id = setInterval(fetchNowPlaying, 10000); + const id = setInterval(() => { + if (document.visibilityState === 'visible') fetchNowPlaying(); + }, 10000); return () => clearInterval(id); - }, []); - - // Refresh immediately when dropdown is opened - useEffect(() => { - if (isOpen) fetchNowPlaying(); }, [isOpen]); // Click outside to close diff --git a/src/hooks/useConnectionStatus.ts b/src/hooks/useConnectionStatus.ts index d07f0c0d..5a9eee9d 100644 --- a/src/hooks/useConnectionStatus.ts +++ b/src/hooks/useConnectionStatus.ts @@ -49,7 +49,7 @@ export function useConnectionStatus() { useEffect(() => { check(); - intervalRef.current = setInterval(check, 30_000); + intervalRef.current = setInterval(check, 120_000); const handleOnline = () => check(); const handleOffline = () => setStatus('disconnected'); diff --git a/src/store/authStore.ts b/src/store/authStore.ts index f49a00b9..7834660f 100644 --- a/src/store/authStore.ts +++ b/src/store/authStore.ts @@ -227,7 +227,7 @@ export const useAuthStore = create()( minimizeToTray: false, discordRichPresence: false, enableAppleMusicCoversDiscord: false, - useCustomTitlebar: true, + useCustomTitlebar: false, nowPlayingEnabled: false, lyricsServerFirst: true, showFullscreenLyrics: true, diff --git a/src/store/playerStore.ts b/src/store/playerStore.ts index b647721e..973dd41c 100644 --- a/src/store/playerStore.ts +++ b/src/store/playerStore.ts @@ -264,7 +264,7 @@ function syncQueueToServer(queue: Track[], currentTrack: Track | null, currentTi savePlayQueue(ids, currentTrack?.id, pos).catch(err => { console.error('Failed to sync play queue to server', err); }); - }, 1500); + }, 5000); } // ─── Audio event handlers (called from initAudioListeners) ───────────────────