siftlode/backend/app/plex
npeter83 3a3ba17fb8 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).
2026-07-10 00:17:56 +02:00
..
__init__.py feat(plex): P0 backend foundations — catalog model, config, client 2026-07-05 01:35:08 +02:00
client.py feat(plex): Phase B — Siftlode→Plex watch-state push 2026-07-10 00:17:56 +02:00
paths.py feat(plex): P0 backend foundations — catalog model, config, client 2026-07-05 01:35:08 +02:00
stream.py feat(plex): multi-rendition audio (client-side switch) + reliable resume-on-F5 2026-07-08 22:25:03 +02:00
sync.py feat(plex): Collections Phase 2 — admin collection editing (write-back to Plex) 2026-07-06 17:33:16 +02:00
watch_sync.py feat(plex): Phase B — Siftlode→Plex watch-state push 2026-07-10 00:17:56 +02:00