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
+40 -20
View File
@@ -16,7 +16,7 @@ history + onDeck, last-write-wins) build on this and land in later ships.
import logging
from datetime import datetime, timedelta, timezone
from sqlalchemy import func
from sqlalchemy import func, select, update
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.orm import Session
@@ -61,7 +61,10 @@ def _plex_watch_to_state(
def _rk_to_id(db: Session) -> dict[str, int]:
"""The rating_key → PlexItem.id lookup used to resolve a Plex leaf back to its mirrored row."""
return {rk: iid for rk, iid in db.query(PlexItem.rating_key, PlexItem.id)}
return {
rk: iid
for rk, iid in db.execute(select(PlexItem.rating_key, PlexItem.id)).all()
}
def _scan_plex_states(
@@ -162,7 +165,9 @@ 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.query(PlexLink).filter_by(user_id=user_id).first()
link = db.execute(
select(PlexLink).where(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
@@ -217,7 +222,11 @@ def push_state_to_plex(
# Flag the row synced — but an "unwatched" via status=new deletes the row, so there may be
# nothing to flag; that's fine (unscrobble still happened). Skip the flag if the row was
# re-edited since we captured `expect_ts` (leave it dirty for its own push / the reconcile).
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()
if st is not None and _row_matches(st, expect_ts):
st.synced_to_plex = True
db.commit()
@@ -272,9 +281,11 @@ def push_bulk_state_to_plex(user_id: int, changes: list[tuple[int, str, str]]) -
# Flag every successfully-pushed row that still exists (an "unwatched" deletes the row, so
# only the "watched" changes have a row to flag).
if synced_ids:
db.query(PlexState).filter(
PlexState.user_id == user_id, PlexState.item_id.in_(synced_ids)
).update({PlexState.synced_to_plex: True}, synchronize_session=False)
db.execute(
update(PlexState)
.where(PlexState.user_id == user_id, PlexState.item_id.in_(synced_ids))
.values(synced_to_plex=True)
)
db.commit()
@@ -290,11 +301,13 @@ _LWW_TOLERANCE_S = 90
def _active_sync_links(db: Session) -> list[PlexLink]:
"""Every link with two-way sync actually running (owner MVP = one row). Iterated uniformly so
P5b friend links slot in later without touching this loop."""
return (
db.query(PlexLink)
.filter_by(uses_admin=True, sync_enabled=True, initial_import_done=True)
.all()
)
return db.execute(
select(PlexLink).where(
PlexLink.uses_admin.is_(True),
PlexLink.sync_enabled.is_(True),
PlexLink.initial_import_done.is_(True),
)
).scalars().all()
def _resolve_account_id(db: Session, plex: PlexClient, link: PlexLink) -> int:
@@ -340,7 +353,11 @@ def _pull_apply(
"""Bring one Plex-side positive signal into Siftlode under last-write-wins. Returns
"watched"/"in_progress" if it changed the row, else None. Never clears a row (un-watch is the
full reconcile's job) — this only pulls in watched/resume."""
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()
if _same_state(st, status, pos):
if st is not None and not st.synced_to_plex:
st.synced_to_plex = True # already matches Plex → just settle the flag, no churn
@@ -368,17 +385,18 @@ def _repush_dirty(db: Session, plex: PlexClient, link: PlexLink) -> int:
# re-push doesn't need an extra per-item plex.metadata() round-trip just to fill set_timeline's
# duration argument.
info_by_id: dict[int, tuple[str, int | None]] = {
iid: (rk, dur) for iid, rk, dur in db.query(PlexItem.id, PlexItem.rating_key, PlexItem.duration_s)
iid: (rk, dur)
for iid, rk, dur in db.execute(
select(PlexItem.id, PlexItem.rating_key, PlexItem.duration_s)
).all()
}
dirty = (
db.query(PlexState)
.filter(
dirty = db.execute(
select(PlexState).where(
PlexState.user_id == link.user_id,
PlexState.synced_to_plex.is_(False),
PlexState.status != "hidden",
)
.all()
)
).scalars().all()
n = 0
for st in dirty:
info = info_by_id.get(st.item_id)
@@ -477,7 +495,9 @@ def run_plex_watch_reconcile(db: Session) -> dict:
# Siftlode rows Plex no longer has a record for: clear the ones that were mirrored
# (Plex un-watched them); leave hidden + dirty (dirty gets re-pushed just below).
for st in db.query(PlexState).filter_by(user_id=link.user_id).all():
for st in db.execute(
select(PlexState).where(PlexState.user_id == link.user_id)
).scalars().all():
if st.item_id in plex_states or st.status == "hidden":
continue
if st.synced_to_plex: