mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
fix(now-playing): poll Live listener count every 30s in the background (#1125)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user