"""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.plex.models import PlexItem, PlexLibrary, PlexState from app.plex import service as plex_service 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 ( plex_service.mark_finished(first, user.id, item.id, now) is True ) # holds the row lock out: list[bool] = [] racer = threading.Thread( target=lambda: out.append(plex_service.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 plex_service.mark_finished(db, user.id, item.id, now) is True db.commit() assert plex_service.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 uid, 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() == {"ok": True, "status": "watched", "position_seconds": 0} assert playback._resume_push_superseded(user.id, item.id, earlier) is True