"""The near-end checkpoint that finishes a Plex item. - Exactly one scrobble per finish, even when two near-end checkpoints race (the 10 s tick and the debounced `seeked` flush arrive together): the transition is decided by a conditional write, not a read taken before it. - The finish supersedes every resume push still in flight, so none can land at Plex after the scrobble and revive a resume point on a watched item. """ import threading from datetime import datetime, timedelta, timezone from sqlalchemy import select from app.db import SessionLocal from app.models import PlexItem, PlexLibrary, PlexState from app.routes.plex import playback def _item(db) -> PlexItem: lib = PlexLibrary(plex_key="1", title="Movies", kind="movie", enabled=True) db.add(lib) db.flush() item = PlexItem(rating_key="m1", library_id=lib.id, kind="movie", title="A Movie") db.add(item) db.commit() return item def test_two_racing_finishes_flip_the_row_once(db, make_user): user = make_user() item = _item(db) db.add(PlexState(user_id=user.id, item_id=item.id, status="new", position_seconds=5000)) db.commit() now = datetime.now(timezone.utc) first, second = SessionLocal(), SessionLocal() try: assert playback._mark_finished(first, user.id, item.id, now) is True # holds the row lock out: list[bool] = [] racer = threading.Thread( target=lambda: out.append(playback._mark_finished(second, user.id, item.id, now)) ) racer.start() racer.join(0.5) assert racer.is_alive(), "the second writer must wait on the first one's row lock" first.commit() racer.join(5) second.commit() finally: first.close() second.close() assert out == [False], "the second checkpoint saw the committed finish: no second scrobble" def test_a_first_ever_finish_inserts_and_counts_once(db, make_user): user = make_user() item = _item(db) now = datetime.now(timezone.utc) assert playback._mark_finished(db, user.id, item.id, now) is True db.commit() assert playback._mark_finished(db, user.id, item.id, now + timedelta(seconds=10)) is False db.commit() st = db.execute(select(PlexState).where(PlexState.item_id == item.id)).scalars().one() assert (st.status, st.position_seconds) == ("watched", 0) def test_the_finish_supersedes_a_resume_push_still_in_flight(db, as_user, make_user, monkeypatch): user = make_user() item = _item(db) monkeypatch.setattr(playback.plex_stream, "touch_session", lambda key: None) earlier = datetime.now(timezone.utc) - timedelta(seconds=5) # A resume push was scheduled a moment ago and has not reached Plex yet. assert playback._should_push_resume(user.id, item.id, earlier, final=True) r = as_user(user).post( "/api/plex/item/m1/progress", json={"position_seconds": 2995, "duration_seconds": 3000} ) assert r.json() == {"status": "watched", "position_seconds": 0} assert playback._resume_push_superseded(user.id, item.id, earlier) is True