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.
|
* 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
|
## [1.48.1] - 2026-06-15
|
||||||
|
|
||||||
|
|||||||
@@ -23,13 +23,14 @@ export default function NowPlayingDropdown() {
|
|||||||
const triggerWrapRef = useRef<HTMLDivElement>(null);
|
const triggerWrapRef = useRef<HTMLDivElement>(null);
|
||||||
const panelRef = useRef<HTMLDivElement>(null);
|
const panelRef = useRef<HTMLDivElement>(null);
|
||||||
const [panelPos, setPanelPos] = useState<{ top: number; left: number }>({ top: 0, left: 0 });
|
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
|
// Wall-clock baseline for the last poll: between polls we extrapolate the
|
||||||
// extrapolate the position of `playing` entries locally so the progress bar
|
// position of `playing` entries locally so the progress bar glides instead
|
||||||
// glides instead of snapping. The server already extrapolates positionMs at
|
// of snapping. The server already extrapolates positionMs at fetch time.
|
||||||
// fetch time, so this just continues from there using the reported speed.
|
|
||||||
const fetchedAtRef = useRef(0);
|
const fetchedAtRef = useRef(0);
|
||||||
const [, forceTick] = useState(0);
|
const [, forceTick] = useState(0);
|
||||||
const PANEL_WIDTH = 340;
|
const PANEL_WIDTH = 340;
|
||||||
|
const LIVE_POLL_OPEN_MS = 10_000;
|
||||||
|
const LIVE_POLL_BACKGROUND_MS = 30_000;
|
||||||
|
|
||||||
const formatClock = (totalSec: number) => {
|
const formatClock = (totalSec: number) => {
|
||||||
const s = Math.max(0, Math.floor(totalSec));
|
const s = Math.max(0, Math.floor(totalSec));
|
||||||
@@ -61,8 +62,8 @@ export default function NowPlayingDropdown() {
|
|||||||
setPanelPos({ top, left });
|
setPanelPos({ top, left });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchNowPlaying = async () => {
|
const fetchNowPlaying = useCallback(async (opts?: { silent?: boolean }) => {
|
||||||
setLoading(true);
|
if (!opts?.silent) setLoading(true);
|
||||||
try {
|
try {
|
||||||
const data = await getNowPlaying();
|
const data = await getNowPlaying();
|
||||||
fetchedAtRef.current = Date.now();
|
fetchedAtRef.current = Date.now();
|
||||||
@@ -70,9 +71,9 @@ export default function NowPlayingDropdown() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to load Now Playing', e);
|
console.error('Failed to load Now Playing', e);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
if (!opts?.silent) setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
setSpinning(true);
|
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(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) return;
|
const poll = (silent: boolean) => {
|
||||||
fetchNowPlaying();
|
if (document.visibilityState === 'visible') void fetchNowPlaying({ silent });
|
||||||
const id = setInterval(() => {
|
};
|
||||||
if (document.visibilityState === 'visible') fetchNowPlaying();
|
|
||||||
}, 10000);
|
poll(!isOpen);
|
||||||
return () => clearInterval(id);
|
const intervalMs = isOpen ? LIVE_POLL_OPEN_MS : LIVE_POLL_BACKGROUND_MS;
|
||||||
}, [isOpen]);
|
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
|
// 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(
|
const hasLivePosition = nowPlaying.some(
|
||||||
e => e.state === 'playing' && typeof e.positionMs === 'number',
|
e => e.state === 'playing' && typeof e.positionMs === 'number',
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user