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
|
|
@ -166,6 +166,38 @@ class PlexClient:
|
|||
items = mc.get("Metadata", []) or []
|
||||
return items[0] if items else None
|
||||
|
||||
# --- Watch-state write-back (P5 Phase B — Siftlode→Plex push; acts on the token's account) ---
|
||||
_LIBRARY_IDENTIFIER = "com.plexapp.plugins.library"
|
||||
|
||||
def scrobble(self, rating_key: str) -> None:
|
||||
"""Mark the item *watched* on the token's Plex account (viewCount++, resume cleared)."""
|
||||
self._write(
|
||||
"GET",
|
||||
f"/:/scrobble?identifier={self._LIBRARY_IDENTIFIER}"
|
||||
f"&key={quote(str(rating_key), safe='')}",
|
||||
)
|
||||
|
||||
def unscrobble(self, rating_key: str) -> None:
|
||||
"""Mark the item *unwatched* on the token's Plex account (clears viewCount)."""
|
||||
self._write(
|
||||
"GET",
|
||||
f"/:/unscrobble?identifier={self._LIBRARY_IDENTIFIER}"
|
||||
f"&key={quote(str(rating_key), safe='')}",
|
||||
)
|
||||
|
||||
def set_timeline(
|
||||
self, rating_key: str, time_ms: int, duration_ms: int, state: str = "stopped"
|
||||
) -> None:
|
||||
"""Report a resume position (viewOffset) for the item on the token's account. `state` is a
|
||||
Plex timeline state (playing|paused|stopped); we push the settled position as ``stopped``."""
|
||||
key = quote(f"/library/metadata/{rating_key}", safe="")
|
||||
self._write(
|
||||
"GET",
|
||||
f"/:/timeline?identifier={self._LIBRARY_IDENTIFIER}"
|
||||
f"&ratingKey={quote(str(rating_key), safe='')}&key={key}"
|
||||
f"&state={state}&time={int(time_ms)}&duration={int(duration_ms)}",
|
||||
)
|
||||
|
||||
def raw_get(self, path: str) -> bytes:
|
||||
"""Raw bytes of an arbitrary Plex resource path (e.g. an external subtitle stream `key` like
|
||||
``/library/streams/553184``). Keeps the admin token server-side."""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue