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
+5 -2
View File
@@ -8,6 +8,7 @@ from datetime import datetime, timezone
from sqlalchemy import and_, case, func, or_, select
from sqlalchemy.orm import Session, aliased
from app.userscope import owned_by
from app.plex.models import PlexItem, PlexState
from app.plex.cards import ShowState
@@ -92,7 +93,7 @@ def agg_subquery(db: Session, user_id: int, include_resumed: bool = False):
)
return (
select(*cols)
.outerjoin(ps, and_(ps.item_id == PlexItem.id, ps.user_id == user_id))
.outerjoin(ps, and_(ps.item_id == PlexItem.id, owned_by(ps.user_id, user_id)))
.where(PlexItem.kind == "episode", PlexItem.show_id.isnot(None))
.group_by(PlexItem.show_id)
)
@@ -114,6 +115,8 @@ def item_state_map(db: Session, user_id: int, item_ids: list[int]) -> dict[int,
return {
s.item_id: s
for s in db.execute(
select(PlexState).where(PlexState.user_id == user_id, PlexState.item_id.in_(item_ids))
select(PlexState).where(
owned_by(PlexState.user_id, user_id), PlexState.item_id.in_(item_ids)
)
).scalars()
}