fix: every first watch state survives a write beside it, and a folded Watch later keeps its pin

A season marked watched, a pull from Plex and a YouTube resume point made their first row by a read
then an add, and failed on the unique key when a first checkpoint or the watched mark made it in
between. They go through the same insert-then-read as the checkpoint (the resume point is one
upsert); a pull that meets a local change still waiting for its push leaves it.

Folding duplicate Watch later lists moved their videos but not a Home pin, which the cascade then
deleted; the first pin of a dropped copy moves onto the kept list.

Marking one notice read trimmed before its own flag was flushed, so the trim left one past the cap.
The route test now crosses the cap and reads the result from a session of its own.
This commit is contained in:
2026-10-20 01:56:30 +02:00
parent b04b2cbd62
commit c5f121139b
10 changed files with 199 additions and 68 deletions
+24 -15
View File
@@ -239,24 +239,33 @@ def clear_state(db: Session, user: User, video_id: str) -> None:
def set_progress(db: Session, user: User, video_id: str, position: int) -> None:
"""The player's resume point. 0 clears it: a row holding nothing else goes, one with a status
(watched, hidden) keeps it with the position zeroed. The status itself is never changed here."""
row = queries.video_state(db, user, video_id)
(watched, hidden) keeps it with the position zeroed. The status itself is never changed here.
A position is one upsert on (user, video): the first checkpoint and the end-of-playback mark
beside it both read "no row", and a plain add let the second one fail on `uq_user_video`."""
now = datetime.now(timezone.utc)
if not position:
if row is None:
return
if row.status == "new":
db.delete(row)
else:
row.position_seconds = 0
row.progress_updated_at = now
db.flush()
if position:
db.execute(
pg_insert(VideoState)
.values(
user_id=user.id,
video_id=video_id,
position_seconds=position,
progress_updated_at=now,
)
.on_conflict_do_update(
constraint="uq_user_video",
set_={"position_seconds": position, "progress_updated_at": now, "updated_at": now},
)
)
return
row = queries.video_state(db, user, video_id)
if row is None:
row = VideoState(user_id=user.id, video_id=video_id)
db.add(row)
row.position_seconds = position
row.progress_updated_at = now
return
if row.status == "new":
db.delete(row)
else:
row.position_seconds = 0
row.progress_updated_at = now
db.flush()