mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +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:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "psysonic",
|
"name": "psysonic",
|
||||||
"version": "1.34.4",
|
"version": "1.34.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
Generated
+1
-1
@@ -3339,7 +3339,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "psysonic"
|
name = "psysonic"
|
||||||
version = "1.34.4"
|
version = "1.34.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"biquad",
|
"biquad",
|
||||||
"discord-rich-presence",
|
"discord-rich-presence",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "psysonic"
|
name = "psysonic"
|
||||||
version = "1.34.4"
|
version = "1.34.5"
|
||||||
description = "Psysonic Desktop Music Player"
|
description = "Psysonic Desktop Music Player"
|
||||||
authors = []
|
authors = []
|
||||||
license = ""
|
license = ""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Psysonic",
|
"productName": "Psysonic",
|
||||||
"version": "1.34.4",
|
"version": "1.34.5",
|
||||||
"identifier": "dev.psysonic.player",
|
"identifier": "dev.psysonic.player",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
+35
-6
@@ -332,6 +332,19 @@ export async function getAlbum(id: string): Promise<{ album: SubsonicAlbum; song
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MIX_RATING_PREFETCH_CONCURRENCY = 8;
|
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 {
|
function parseEntityUserRating(v: unknown): number | undefined {
|
||||||
if (v === null || v === undefined) return undefined;
|
if (v === null || v === undefined) return undefined;
|
||||||
@@ -348,22 +361,30 @@ export async function prefetchArtistUserRatings(
|
|||||||
const unique = [...new Set(ids.filter(Boolean))];
|
const unique = [...new Set(ids.filter(Boolean))];
|
||||||
const out = new Map<string, number>();
|
const out = new Map<string, number>();
|
||||||
if (!unique.length) return out;
|
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;
|
let next = 0;
|
||||||
async function worker() {
|
async function worker() {
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const i = next++;
|
const i = next++;
|
||||||
if (i >= unique.length) return;
|
if (i >= uncached.length) return;
|
||||||
const id = unique[i];
|
const id = uncached[i];
|
||||||
try {
|
try {
|
||||||
const { artist } = await getArtist(id);
|
const { artist } = await getArtist(id);
|
||||||
const r = parseEntityUserRating(artist.userRating);
|
const r = parseEntityUserRating(artist.userRating);
|
||||||
|
setCachedRating(`artist:${id}`, r);
|
||||||
if (r !== undefined) out.set(id, r);
|
if (r !== undefined) out.set(id, r);
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const nWorkers = Math.min(concurrency, unique.length);
|
const nWorkers = Math.min(concurrency, uncached.length);
|
||||||
await Promise.all(Array.from({ length: nWorkers }, () => worker()));
|
await Promise.all(Array.from({ length: nWorkers }, () => worker()));
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -376,22 +397,30 @@ export async function prefetchAlbumUserRatings(
|
|||||||
const unique = [...new Set(ids.filter(Boolean))];
|
const unique = [...new Set(ids.filter(Boolean))];
|
||||||
const out = new Map<string, number>();
|
const out = new Map<string, number>();
|
||||||
if (!unique.length) return out;
|
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;
|
let next = 0;
|
||||||
async function worker() {
|
async function worker() {
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const i = next++;
|
const i = next++;
|
||||||
if (i >= unique.length) return;
|
if (i >= uncached.length) return;
|
||||||
const id = unique[i];
|
const id = uncached[i];
|
||||||
try {
|
try {
|
||||||
const { album } = await getAlbum(id);
|
const { album } = await getAlbum(id);
|
||||||
const r = parseEntityUserRating(album.userRating);
|
const r = parseEntityUserRating(album.userRating);
|
||||||
|
setCachedRating(`album:${id}`, r);
|
||||||
if (r !== undefined) out.set(id, r);
|
if (r !== undefined) out.set(id, r);
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const nWorkers = Math.min(concurrency, unique.length);
|
const nWorkers = Math.min(concurrency, uncached.length);
|
||||||
await Promise.all(Array.from({ length: nWorkers }, () => worker()));
|
await Promise.all(Array.from({ length: nWorkers }, () => worker()));
|
||||||
return out;
|
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(() => {
|
useEffect(() => {
|
||||||
|
if (!isOpen) return;
|
||||||
fetchNowPlaying();
|
fetchNowPlaying();
|
||||||
const id = setInterval(fetchNowPlaying, 10000);
|
const id = setInterval(() => {
|
||||||
|
if (document.visibilityState === 'visible') fetchNowPlaying();
|
||||||
|
}, 10000);
|
||||||
return () => clearInterval(id);
|
return () => clearInterval(id);
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Refresh immediately when dropdown is opened
|
|
||||||
useEffect(() => {
|
|
||||||
if (isOpen) fetchNowPlaying();
|
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
// Click outside to close
|
// Click outside to close
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export function useConnectionStatus() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
check();
|
check();
|
||||||
intervalRef.current = setInterval(check, 30_000);
|
intervalRef.current = setInterval(check, 120_000);
|
||||||
|
|
||||||
const handleOnline = () => check();
|
const handleOnline = () => check();
|
||||||
const handleOffline = () => setStatus('disconnected');
|
const handleOffline = () => setStatus('disconnected');
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
minimizeToTray: false,
|
minimizeToTray: false,
|
||||||
discordRichPresence: false,
|
discordRichPresence: false,
|
||||||
enableAppleMusicCoversDiscord: false,
|
enableAppleMusicCoversDiscord: false,
|
||||||
useCustomTitlebar: true,
|
useCustomTitlebar: false,
|
||||||
nowPlayingEnabled: false,
|
nowPlayingEnabled: false,
|
||||||
lyricsServerFirst: true,
|
lyricsServerFirst: true,
|
||||||
showFullscreenLyrics: true,
|
showFullscreenLyrics: true,
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ function syncQueueToServer(queue: Track[], currentTrack: Track | null, currentTi
|
|||||||
savePlayQueue(ids, currentTrack?.id, pos).catch(err => {
|
savePlayQueue(ids, currentTrack?.id, pos).catch(err => {
|
||||||
console.error('Failed to sync play queue to server', err);
|
console.error('Failed to sync play queue to server', err);
|
||||||
});
|
});
|
||||||
}, 1500);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Audio event handlers (called from initAudioListeners) ───────────────────
|
// ─── Audio event handlers (called from initAudioListeners) ───────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user