feat(plex): Phase B — Siftlode→Plex watch-state push
Mirror watched/unwatched/resume from Siftlode to a linked (owner) Plex account. - PlexClient: scrobble / unscrobble / set_timeline (/:/scrobble, /:/unscrobble, /:/timeline). - watch_sync: link_for_push() gates on owner + sync_enabled + initial_import_done; best-effort push_state_to_plex() runs as a background task with its own session, never raises (Plex being down must not break the user's action), and flips synced_to_plex on success so Phase C's pull won't bounce it back. - item_state / item_progress schedule the push: watched/unwatched immediately, resume only on a "final" checkpoint (pause / pagehide / unmount) — not every 10s tick — so one watch doesn't spray /:/timeline at the server. `hidden` is Siftlode-only and never touches Plex. - Frontend: plexProgress / plexProgressBeacon carry a `final` flag; the periodic checkpoint is non-final, pause/pagehide/leaving the player are final. Verified live against the real Plex server (scrobble/unscrobble/timeline round-trip + restore; link gating; synced_to_plex flip).
This commit is contained in:
parent
57f521191e
commit
3a3ba17fb8
5 changed files with 167 additions and 12 deletions
|
|
@ -1244,16 +1244,29 @@ export const api = {
|
|||
// cue times onto the HLS session's zero-based clock so subtitles line up with speech after a seek.
|
||||
plexSubtitleUrl: (id: string, ord: number, offset = 0): string =>
|
||||
`/api/plex/subtitle/${encodeURIComponent(id)}/${ord}${offset ? `?offset=${offset}` : ""}`,
|
||||
plexProgress: (id: string, position_seconds: number, duration_seconds: number): Promise<unknown> =>
|
||||
// `final` marks a settled checkpoint (pause / page-hide / leaving the player) as opposed to the
|
||||
// periodic 10s tick — the backend mirrors the resume position to a linked Plex account only on a
|
||||
// final checkpoint, so a single watch doesn't spray Plex with timeline updates.
|
||||
plexProgress: (
|
||||
id: string,
|
||||
position_seconds: number,
|
||||
duration_seconds: number,
|
||||
final = false,
|
||||
): Promise<unknown> =>
|
||||
req(`/api/plex/item/${encodeURIComponent(id)}/progress`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ position_seconds, duration_seconds }),
|
||||
body: JSON.stringify({ position_seconds, duration_seconds, final }),
|
||||
}),
|
||||
// Fire-and-forget progress save that SURVIVES page unload (F5/close/navigate): a normal fetch is
|
||||
// cancelled on unload and React effect cleanup doesn't run on a full reload, so the last position
|
||||
// (esp. right after a seek) would be lost. `keepalive` lets the POST complete during unload; we
|
||||
// replicate req()'s credentials + account header (no retry — there's no page left to retry on).
|
||||
plexProgressBeacon: (id: string, position_seconds: number, duration_seconds: number): void => {
|
||||
plexProgressBeacon: (
|
||||
id: string,
|
||||
position_seconds: number,
|
||||
duration_seconds: number,
|
||||
final = false,
|
||||
): void => {
|
||||
const active = getActiveAccount();
|
||||
try {
|
||||
void fetch(`/api/plex/item/${encodeURIComponent(id)}/progress`, {
|
||||
|
|
@ -1264,7 +1277,7 @@ export const api = {
|
|||
"Content-Type": "application/json",
|
||||
...(active != null ? { [ACTIVE_ACCOUNT_HEADER]: String(active) } : {}),
|
||||
},
|
||||
body: JSON.stringify({ position_seconds, duration_seconds }),
|
||||
body: JSON.stringify({ position_seconds, duration_seconds, final }),
|
||||
}).catch(() => {});
|
||||
} catch {
|
||||
/* ignore */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue