refactor(plex): playlists, playback, Discover and admin through the unit; the plex package is fenced

The last Plex HTTP files do no database work of their own, so ALLOWED is empty: no route builds
SQL, touches the session or a unit's table. Discover's rails move whole into plex/discover.py; a
user's playlists, watch-state writes (the race-safe finish, an early checkpoint, a status, a
season or show at once) and the watch-sync link go through plex/queries.py and plex/service.py.

The plex package joins FENCED: it reads no other unit's tables, says whose a row is through
userscope (the rollup and the watch sync's 8 inline filters moved to owned_by), and commits only in
its named jobs — the library sync, TMDB enrichment, the watch sync and its background pushes.
A playlist's has-item probe keeps the plain key lookup, so a NUL in it is still the 422 a test pins.

Pins 178 -> 0; basedpyright baseline 118 -> 106.
This commit is contained in:
2026-10-19 22:42:46 +02:00
parent 2cf1b688d9
commit 531eb2eef1
18 changed files with 1307 additions and 1052 deletions
+12 -7
View File
@@ -29,6 +29,7 @@ from sqlalchemy.orm import Session
from app import logs, sysconfig
from app.db import SessionLocal
from app.userscope import owned_by
from app.plex.models import PlexItem, PlexLibrary, PlexLink, PlexState
from app.plex.client import PlexClient, PlexError, PlexNotConfigured
from app.plex.sync import (
@@ -196,7 +197,7 @@ def link_for_push(db: Session, user_id: int) -> PlexLink | None:
"""The active owner watch-push link for this user, or None when push is off / not the owner.
Push is gated on `initial_import_done` so we never push a half-known local state over Plex's
authoritative one before the one-time "Plex is master" import has reconciled them."""
link = db.execute(select(PlexLink).where(PlexLink.user_id == user_id)).scalars().first()
link = db.execute(select(PlexLink).where(owned_by(PlexLink.user_id, user_id))).scalars().first()
if link and link.uses_admin and link.sync_enabled and link.initial_import_done:
return link
return None
@@ -253,7 +254,9 @@ def push_state_to_plex(
# re-edited since we captured `expect_ts` (leave it dirty for its own push / the reconcile).
st = (
db.execute(
select(PlexState).where(PlexState.user_id == user_id, PlexState.item_id == item_id)
select(PlexState).where(
owned_by(PlexState.user_id, user_id), PlexState.item_id == item_id
)
)
.scalars()
.first()
@@ -318,7 +321,7 @@ def push_bulk_state_to_plex(user_id: int, changes: list[tuple[int, str, str]]) -
if synced_ids:
db.execute(
update(PlexState)
.where(PlexState.user_id == user_id, PlexState.item_id.in_(synced_ids))
.where(owned_by(PlexState.user_id, user_id), PlexState.item_id.in_(synced_ids))
.values(synced_to_plex=True)
)
db.commit()
@@ -393,7 +396,9 @@ def _pull_apply(
full reconcile's job) — this only pulls in watched/resume."""
st = (
db.execute(
select(PlexState).where(PlexState.user_id == user_id, PlexState.item_id == item_id)
select(PlexState).where(
owned_by(PlexState.user_id, user_id), PlexState.item_id == item_id
)
)
.scalars()
.first()
@@ -449,7 +454,7 @@ def _repush_dirty(db: Session, plex: PlexClient, link: PlexLink) -> int:
dirty = (
db.execute(
select(PlexState).where(
PlexState.user_id == link.user_id,
owned_by(PlexState.user_id, link.user_id),
PlexState.synced_to_plex.is_(False),
PlexState.status != "hidden",
)
@@ -505,7 +510,7 @@ def _reconcile_inprogress_movies(
and aren't affected). Note: like the rest of this job, `metadata()` reads the TOKEN account (the
owner) — a HTPC playing under a managed Plex user (accountID>1) is out of scope here as elsewhere."""
conds = [
PlexState.user_id == link.user_id,
owned_by(PlexState.user_id, link.user_id),
PlexState.status.notin_(("watched", "hidden")),
PlexState.position_seconds > _PROGRESS_MIN_S,
PlexItem.kind == "movie",
@@ -627,7 +632,7 @@ def _clear_unwatched(db: Session, user_id: int, plex_states: dict, settled: set[
select(PlexState)
.join(PlexItem, PlexItem.id == PlexState.item_id)
.join(PlexLibrary, PlexLibrary.id == PlexItem.library_id)
.where(PlexState.user_id == user_id, PlexLibrary.plex_key.in_(settled))
.where(owned_by(PlexState.user_id, user_id), PlexLibrary.plex_key.in_(settled))
)
for st in rows.scalars().all():
if st.item_id in plex_states or st.status == "hidden" or not st.synced_to_plex: