feat(plex): Plex-authoritative watch-sync — resume write-back + faster pull (S2)
Make Plex the source of truth for watch state: - Write-back during playback: item_progress now pushes the resume position to Plex on the periodic checkpoint too (not only the final flush), throttled to one push per item every ~10s (in-memory, single-process rate limiter). The FE now also fires an immediate (debounced) push on seek. - Faster incremental pull: 30 → 15 min (plex_watch_sync_interval_min). - Plex-authoritative pull: _pull_apply resolves a genuine divergence to Plex's state instead of last-write-wins; the _same_state + synced_to_plex short-circuit still prevents a freshly-pushed row from bouncing. Removed the now-unused _LWW_TOLERANCE_S / _sift_ts. Tests: 5 new in test_plex_watch_sync.py (Plex-wins resume/watched, no-bounce, throttle final vs periodic).
This commit is contained in:
@@ -102,6 +102,16 @@ export function usePlexProgress(opts: {
|
||||
const onPauseFlush = () => flush(true);
|
||||
const video = videoRef.current;
|
||||
video?.addEventListener("pause", onPauseFlush);
|
||||
// A seek is a settled position too (S2: Plex is the source of truth, so it must see the new spot
|
||||
// right away, not up to 10s later). Debounced so scrubbing — many `seeked` events in a row — pushes
|
||||
// just ONCE, once the user lands. `final=true` bypasses the backend's ≥10s push-throttle, which is
|
||||
// exactly why the debounce lives here: it bounds a scrub to one immediate Plex timeline call.
|
||||
let seekTimer: number | undefined;
|
||||
const onSeekFlush = () => {
|
||||
window.clearTimeout(seekTimer);
|
||||
seekTimer = window.setTimeout(() => flush(true), 800);
|
||||
};
|
||||
video?.addEventListener("seeked", onSeekFlush);
|
||||
// Save on page unload too (F5/close/navigate): React effect cleanup does NOT run on a full reload,
|
||||
// so without this the resume point lags up to 10s — e.g. a seek right before F5 was lost. Uses a
|
||||
// keepalive beacon so the POST survives the unload (a final checkpoint — the user is leaving).
|
||||
@@ -112,8 +122,10 @@ export function usePlexProgress(opts: {
|
||||
window.addEventListener("pagehide", onHide);
|
||||
return () => {
|
||||
window.clearInterval(iv);
|
||||
window.clearTimeout(seekTimer);
|
||||
window.removeEventListener("pagehide", onHide);
|
||||
video?.removeEventListener("pause", onPauseFlush);
|
||||
video?.removeEventListener("seeked", onSeekFlush);
|
||||
flush(true);
|
||||
// Back (unmount) persists the position server-side via flush(), but reopening the same item in
|
||||
// the SAME session read react-query's CACHED detail — whose position_seconds predated this
|
||||
|
||||
Reference in New Issue
Block a user