mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
perf: drastically reduce background API requests to Navidrome (v1.34.5)
- 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 <noreply@anthropic.com>
This commit is contained in:
+35
-6
@@ -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<string, { value: number | undefined; expiresAt: number }>();
|
||||
|
||||
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<string, number>();
|
||||
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<string, number>();
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -227,7 +227,7 @@ export const useAuthStore = create<AuthState>()(
|
||||
minimizeToTray: false,
|
||||
discordRichPresence: false,
|
||||
enableAppleMusicCoversDiscord: false,
|
||||
useCustomTitlebar: true,
|
||||
useCustomTitlebar: false,
|
||||
nowPlayingEnabled: false,
|
||||
lyricsServerFirst: true,
|
||||
showFullscreenLyrics: true,
|
||||
|
||||
@@ -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) ───────────────────
|
||||
|
||||
Reference in New Issue
Block a user