fix(now-playing): poll Live listener count every 30s in the background (#1125)

This commit is contained in:
cucadmuh
2026-06-18 21:22:35 +03:00
committed by GitHub
parent fde7ab432f
commit ee044ece1a
2 changed files with 36 additions and 17 deletions
+6
View File
@@ -67,6 +67,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Bumped transitive `form-data` 4.0.5 → 4.0.6 (via axios) to close Dependabot alert [#18](https://github.com/Psychotoxical/psysonic/security/dependabot/18) for CRLF injection in multipart field names (CVE-2026-12143). Psysonic only uses axios for GET requests, so exploitability was low; the lockfile bump clears the advisory.
### Live listener badge stale when the popover was closed
**By [@cucadmuh](https://github.com/cucadmuh), PR [#1125](https://github.com/Psychotoxical/psysonic/pull/1125)**
* The Live header badge only refreshed `getNowPlaying` while the "Who is listening?" popover was open, so the listener count could stay stale or hidden until opened. Poll every 30 s while the window is visible (10 s while the popover is open); background fetches are silent so the header does not flash a loading state.
## [1.48.1] - 2026-06-15
+30 -17
View File
@@ -23,13 +23,14 @@ export default function NowPlayingDropdown() {
const triggerWrapRef = useRef<HTMLDivElement>(null);
const panelRef = useRef<HTMLDivElement>(null);
const [panelPos, setPanelPos] = useState<{ top: number; left: number }>({ top: 0, left: 0 });
// Wall-clock baseline for the last poll: between polls (every 10 s) we
// extrapolate the position of `playing` entries locally so the progress bar
// glides instead of snapping. The server already extrapolates positionMs at
// fetch time, so this just continues from there using the reported speed.
// Wall-clock baseline for the last poll: between polls we extrapolate the
// position of `playing` entries locally so the progress bar glides instead
// of snapping. The server already extrapolates positionMs at fetch time.
const fetchedAtRef = useRef(0);
const [, forceTick] = useState(0);
const PANEL_WIDTH = 340;
const LIVE_POLL_OPEN_MS = 10_000;
const LIVE_POLL_BACKGROUND_MS = 30_000;
const formatClock = (totalSec: number) => {
const s = Math.max(0, Math.floor(totalSec));
@@ -61,8 +62,8 @@ export default function NowPlayingDropdown() {
setPanelPos({ top, left });
}, []);
const fetchNowPlaying = async () => {
setLoading(true);
const fetchNowPlaying = useCallback(async (opts?: { silent?: boolean }) => {
if (!opts?.silent) setLoading(true);
try {
const data = await getNowPlaying();
fetchedAtRef.current = Date.now();
@@ -70,9 +71,9 @@ export default function NowPlayingDropdown() {
} catch (e) {
console.error('Failed to load Now Playing', e);
} finally {
setLoading(false);
if (!opts?.silent) setLoading(false);
}
};
}, []);
const handleRefresh = () => {
setSpinning(true);
@@ -81,18 +82,30 @@ export default function NowPlayingDropdown() {
});
};
// Poll only while the dropdown is open AND the page is visible.
// Poll while the page is visible: every 30 s for the header badge, every 10 s
// while the popover is open so the list stays fresher.
useEffect(() => {
if (!isOpen) return;
fetchNowPlaying();
const id = setInterval(() => {
if (document.visibilityState === 'visible') fetchNowPlaying();
}, 10000);
return () => clearInterval(id);
}, [isOpen]);
const poll = (silent: boolean) => {
if (document.visibilityState === 'visible') void fetchNowPlaying({ silent });
};
poll(!isOpen);
const intervalMs = isOpen ? LIVE_POLL_OPEN_MS : LIVE_POLL_BACKGROUND_MS;
const id = setInterval(() => poll(true), intervalMs);
const onVisibility = () => {
if (document.visibilityState === 'visible') poll(true);
};
document.addEventListener('visibilitychange', onVisibility);
return () => {
clearInterval(id);
document.removeEventListener('visibilitychange', onVisibility);
};
}, [isOpen, fetchNowPlaying]);
// Re-render once per second while a `playing` entry exposes a position, so the
// locally-extrapolated bar advances smoothly between the 10 s polls.
// locally-extrapolated bar advances smoothly between polls.
const hasLivePosition = nowPlaying.some(
e => e.state === 'playing' && typeof e.positionMs === 'number',
);