refactor(plex): convert service-layer db.query() to 2.0 select() (wave 1)

Mechanical 1.x->2.0 conversion of the self-contained plex modules, behaviour-
equivalent (the characterization net + existing plex tests stay green, 39
passed): app/plex/{sync(15),watch_sync(9),people(1),imgcache(4)} and
app/routes/plex/{admin(4),playback(8),_shared(1)}. Query builders become
select(); .first()/.all()->execute().scalars(); a bulk .update() ->
execute(update()); a single-column .first() -> db.scalar().

browse.py (61) + rollup.agg_subquery (the atomic unit with unified_library)
remain for wave 2.
This commit is contained in:
2026-08-16 05:52:40 +02:00
parent ecbf5737b6
commit 59d50615c4
7 changed files with 174 additions and 67 deletions
+4 -1
View File
@@ -3,6 +3,7 @@
import logging
from fastapi import HTTPException
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.models import (
@@ -13,7 +14,9 @@ log = logging.getLogger("siftlode.plex")
def item_or_404(db: Session, rating_key: str) -> PlexItem:
it = db.query(PlexItem).filter_by(rating_key=str(rating_key)).first()
it = db.execute(
select(PlexItem).where(PlexItem.rating_key == str(rating_key))
).scalars().first()
if it is None:
raise HTTPException(status_code=404, detail="Unknown Plex item")
return it
+13 -6
View File
@@ -4,6 +4,7 @@ from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.orm import Session
from app import sysconfig
@@ -65,9 +66,9 @@ def _sample_plex_path(
"""A real Plex-side media file path to validate the mount + path-map against. Prefers the
already-mirrored catalog (cheap); before the first sync, pulls one movie leaf from an enabled
section straight from Plex."""
row = db.query(PlexItem.file_path).filter(PlexItem.file_path.isnot(None)).first()
if row and row[0]:
return row[0]
fp = db.scalar(select(PlexItem.file_path).where(PlexItem.file_path.isnot(None)))
if fp:
return fp
wanted = plex_sync._enabled_section_keys(db)
for s in sections:
if s.get("type") != "movie":
@@ -117,7 +118,9 @@ def watch_link_status(
user: User = Depends(current_user), db: Session = Depends(get_db)
) -> dict:
"""This user's Plex watch-sync link status (drives the Settings toggle)."""
link = db.query(PlexLink).filter_by(user_id=user.id).first()
link = db.execute(
select(PlexLink).where(PlexLink.user_id == user.id)
).scalars().first()
return _link_dict(link)
@@ -137,7 +140,9 @@ def watch_link_set(
if not sysconfig.get_bool(db, "plex_enabled"):
raise HTTPException(status_code=409, detail="The Plex module is disabled.")
enabled = body.enabled
link = db.query(PlexLink).filter_by(user_id=user.id).first()
link = db.execute(
select(PlexLink).where(PlexLink.user_id == user.id)
).scalars().first()
if link is None:
link = PlexLink(
user_id=user.id, uses_admin=True, linked_at=datetime.now(timezone.utc)
@@ -158,7 +163,9 @@ def watch_import_now(
) -> dict:
"""Admin: (re-)run the "Plex is master" import for this account — Plex re-wins on the
intersection; Siftlode-only states are preserved. Idempotent."""
link = db.query(PlexLink).filter_by(user_id=user.id).first()
link = db.execute(
select(PlexLink).where(PlexLink.user_id == user.id)
).scalars().first()
if link is None or not link.sync_enabled:
raise HTTPException(status_code=409, detail="Enable Plex watch-sync first.")
result = plex_watch.import_owner_watch_state(db, link)
+34 -11
View File
@@ -16,6 +16,7 @@ from fastapi import (
)
from fastapi.responses import FileResponse
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.auth import current_user, resolved_user_id
@@ -207,7 +208,11 @@ def _push_watch(
return
# Capture the row's freshest timestamp NOW so the background push can tell if a newer local edit
# lands before it settles synced_to_plex (which would otherwise strand that newer value).
st = db.query(PlexState).filter_by(user_id=user_id, item_id=item_id).first()
st = db.execute(
select(PlexState).where(
PlexState.user_id == user_id, PlexState.item_id == item_id
)
).scalars().first()
background.add_task(
plex_watch.push_state_to_plex,
user_id,
@@ -246,7 +251,11 @@ def item_progress(
duration = int(body.duration_seconds)
final = body.final
now = datetime.now(timezone.utc)
st = db.query(PlexState).filter_by(user_id=user.id, item_id=it.id).first()
st = db.execute(
select(PlexState).where(
PlexState.user_id == user.id, PlexState.item_id == it.id
)
).scalars().first()
was_watched = st is not None and st.status == "watched"
near_end = duration > 0 and position > duration - _FINISH_MARGIN_S
@@ -316,7 +325,11 @@ def item_state(
status = body.status
if status not in ("new", "watched", "hidden"):
raise HTTPException(status_code=400, detail="Invalid status")
st = db.query(PlexState).filter_by(user_id=user.id, item_id=it.id).first()
st = db.execute(
select(PlexState).where(
PlexState.user_id == user.id, PlexState.item_id == it.id
)
).scalars().first()
prev_status = st.status if st is not None else "new"
if status == "new":
if st is not None:
@@ -351,10 +364,12 @@ def _bulk_state(
now = datetime.now(timezone.utc)
existing = {
s.item_id: s
for s in db.query(PlexState).filter(
PlexState.user_id == user_id,
PlexState.item_id.in_([e.id for e in eps] or [0]),
)
for s in db.execute(
select(PlexState).where(
PlexState.user_id == user_id,
PlexState.item_id.in_([e.id for e in eps] or [0]),
)
).scalars()
}
pushable = plex_watch.link_for_push(db, user_id) is not None
to_push: list[tuple[int, str, str]] = []
@@ -397,11 +412,15 @@ def show_state(
db: Session = Depends(get_db),
) -> dict:
"""Mark a whole show watched/unwatched (all its episodes) for the current user + push to Plex."""
sh = db.query(PlexShow).filter_by(rating_key=str(rating_key)).first()
sh = db.execute(
select(PlexShow).where(PlexShow.rating_key == str(rating_key))
).scalars().first()
if sh is None:
raise HTTPException(status_code=404, detail="Unknown Plex show")
watched = body.watched
eps = db.query(PlexItem).filter_by(show_id=sh.id, kind="episode").all()
eps = db.execute(
select(PlexItem).where(PlexItem.show_id == sh.id, PlexItem.kind == "episode")
).scalars().all()
changed = _bulk_state(background, db, user.id, eps, watched)
return {"changed": changed, "watched": watched}
@@ -415,11 +434,15 @@ def season_state(
db: Session = Depends(get_db),
) -> dict:
"""Mark a whole season watched/unwatched (all its episodes) for the current user + push to Plex."""
se = db.query(PlexSeason).filter_by(rating_key=str(rating_key)).first()
se = db.execute(
select(PlexSeason).where(PlexSeason.rating_key == str(rating_key))
).scalars().first()
if se is None:
raise HTTPException(status_code=404, detail="Unknown Plex season")
watched = body.watched
eps = db.query(PlexItem).filter_by(season_id=se.id, kind="episode").all()
eps = db.execute(
select(PlexItem).where(PlexItem.season_id == se.id, PlexItem.kind == "episode")
).scalars().all()
changed = _bulk_state(background, db, user.id, eps, watched)
return {"changed": changed, "watched": watched}