fix(plex): a checkpoint sent just before the finish no longer lands on the watched row
The 10 s tick in flight when the player finishes (or a refusal rollback whose state write in fact committed) wrote its mid-item position onto the freshly watched row and scheduled a resume push after the scrobble, so Plex showed the item in progress again. The checkpoint is now a conditional write that skips a row marked watched in the last 30 s and answers like a finish; a rewatch past that is checkpointed as before. Marking an item (or a season/show) watched also supersedes resume pushes still in flight, as the near-end finish already did.
This commit is contained in:
@@ -234,6 +234,15 @@ def person_image(u: str, request: Request) -> Response:
|
||||
return imgcache.response(data, ct)
|
||||
|
||||
|
||||
def _supersede_resume_pushes(user_id: int, item_id: int, now: datetime) -> None:
|
||||
"""A watched mark supersedes every resume push still in flight for the item: stamping it as the
|
||||
latest push makes `_resume_push_superseded` drop them. Popping the entry instead (to keep the map
|
||||
small) removed the very guard, and an older resume push could then land at Plex AFTER the
|
||||
scrobble and revive a resume point on a watched item. Eviction bounds the map."""
|
||||
with _push_throttle_lock:
|
||||
_last_push_at[(user_id, item_id)] = now
|
||||
|
||||
|
||||
def _push_resume_guarded(
|
||||
user_id: int,
|
||||
item_id: int,
|
||||
@@ -329,12 +338,7 @@ def item_progress(
|
||||
near_end = duration > 0 and position > duration - _FINISH_MARGIN_S
|
||||
if near_end:
|
||||
flipped = service.mark_finished(db, user.id, it.id, now)
|
||||
# The finish supersedes every resume push still in flight for this item: stamping it as the
|
||||
# latest push makes `_resume_push_superseded` drop them. Popping the entry instead (to keep
|
||||
# the map small) removed the very guard, and an older resume push could then land at Plex
|
||||
# AFTER the scrobble and revive a resume point on a watched item. Eviction bounds the map.
|
||||
with _push_throttle_lock:
|
||||
_last_push_at[(user.id, it.id)] = now
|
||||
_supersede_resume_pushes(user.id, it.id, now)
|
||||
db.commit()
|
||||
# Only the checkpoint that flipped the row scrobbles: a later checkpoint of the same finished
|
||||
# item — or a second one racing this one — would otherwise bump Plex's viewCount again.
|
||||
@@ -352,7 +356,11 @@ def item_progress(
|
||||
push_now = _should_push_resume(user.id, it.id, now, final)
|
||||
# Marked dirty before the (background) push so the reconcile knows to re-push if it fails, and so
|
||||
# `push_state_to_plex` can settle synced_to_plex once Plex confirms.
|
||||
service.checkpoint(db, user, it.id, st, position, now, dirty=push_now)
|
||||
if not service.checkpoint(db, user, it.id, st, position, now, dirty=push_now):
|
||||
# Marked watched a moment ago: this is the finishing player's late tick (see
|
||||
# `service.checkpoint`), and the answer is the same one a near-end checkpoint gets.
|
||||
db.commit()
|
||||
return {"ok": True, "status": "watched", "position_seconds": 0}
|
||||
db.commit()
|
||||
if push_now:
|
||||
_push_watch(
|
||||
@@ -408,6 +416,8 @@ def item_state(
|
||||
_push_watch(background, db, user.id, it.id, rating_key, "unwatched")
|
||||
return {"ok": True, "status": "new"}
|
||||
service.set_status(db, user, it.id, st, status)
|
||||
if status == "watched":
|
||||
_supersede_resume_pushes(user.id, it.id, datetime.now(timezone.utc))
|
||||
db.commit()
|
||||
if status == "watched" and prev_status != "watched":
|
||||
_push_watch(background, db, user.id, it.id, rating_key, "watched")
|
||||
@@ -427,6 +437,10 @@ def _bulk_state(
|
||||
always reads the committed marks."""
|
||||
pushable = plex_watch.link_for_push(db, user_id) is not None
|
||||
to_push = service.bulk_state(db, user_id, eps, watched)
|
||||
if watched:
|
||||
now = datetime.now(timezone.utc)
|
||||
for item_id, _rating_key, _action in to_push:
|
||||
_supersede_resume_pushes(user_id, item_id, now)
|
||||
if pushable and to_push:
|
||||
# One coalesced background task for the whole batch (reuses a single session + Plex client),
|
||||
# not one task per episode — a 200-ep show mark is 1 task, not 200.
|
||||
|
||||
Reference in New Issue
Block a user