Files
siftlode/backend/app/plex/service.py
T
peter 3b0e365e26 refactor(plex): the browse, detail, people and collection pages read through queries and write through the service
The HTTP files browse, detail, people, collections and _shared do no database work of their own
now. New plex/queries.py (the rows a page shows, a user's watch state through userscope),
plex/service.py (the info pages' enrichment, a collection's editable flag, the row kept after a
failed follow-up read, the person bio's background fetch; none commits), plex/library.py (the
unified browse feed, its search and facets) and plex/collections.py (a collection's source,
editability, card, certificate and member order). Every watch-state join in the moved code names
its owner through owned_by.

Pins 354 -> 178 in 4 files; basedpyright baseline 132 -> 118.
2026-10-19 22:11:20 +02:00

82 lines
3.5 KiB
Python

"""Plex's writes for the HTTP layer, one function per use case. None of them commits: the handler
does, so a page that fails to save an enrichment still renders, and a Plex write and its local
mirror land together.
The rest of the mirror's writes stay with the sync (`plex.sync`): a collection's targeted re-sync
after a write-back, its local cleanup after a delete.
"""
from datetime import datetime, timezone
from sqlalchemy.orm import Session
from app import logs
from app.db import SessionLocal
from app.plex import detail as plex_detail
from app.plex import people as plex_people
from app.plex import tmdb as plex_tmdb
from app.plex.lookups import by_rating_key
from app.plex.models import PlexCollection, PlexItem, PlexLibrary, PlexPerson, PlexShow
log = logs.get_logger(__name__)
def enrich_show(db: Session, sh: PlexShow, meta: dict) -> None:
"""Fill a not-yet-enriched show from its live metadata: the full cast (filterable), IMDb and
tagline, stamped so later views serve from the row."""
plex_people.enrich_row_people(db, sh, plex_people.cast_from_meta(meta))
plex_detail.apply_rich_detail(sh, meta)
sh.people_enriched_at = datetime.now(timezone.utc)
def enrich_movie(db: Session, it: PlexItem, meta: dict) -> None:
"""The same for a movie. The listing sync stored only its top ~3 cast; the live metadata
carries all of them."""
cast = plex_people.cast_from_meta(meta)
if cast:
plex_people.enrich_row_people(db, it, cast)
plex_detail.apply_rich_detail(it, meta)
it.people_enriched_at = datetime.now(timezone.utc)
def set_collection_editable(col: PlexCollection, editable: bool) -> None:
col.editable = editable
def keep_created_collection(
db: Session, lib: PlexLibrary, rating_key: str, title: str, seed: str
) -> None:
"""A collection Plex made, whose follow-up read failed: its row is kept, editable. The full sync
preserves `editable` but creates a missing row read-only, so the collection would drop out of
every "add to" list; and the duplicate-name check reads these rows, so a retry of the same name
would make a second one on Plex. Plex made it with exactly the seed in it, so the seed's row says
so too — otherwise the title showed unticked in the collection it was just added to, and the
collection empty, until that sync."""
db.add(
PlexCollection(
rating_key=rating_key, library_id=lib.id, title=title, editable=True, child_count=1
)
)
Model = PlexItem if lib.kind == "movie" else PlexShow
if row := by_rating_key(db, Model, seed):
row.collection_keys = list(dict.fromkeys([*(row.collection_keys or []), rating_key]))
db.flush()
def enrich_person_bg(person_id: int) -> None:
"""Fetch + persist one person's TMDB bio OFF the request path — a FastAPI background task that runs
AFTER the response is sent, so the outbound TMDB call never sits on the serve latency (the module's
"no live TMDB call at serve time" invariant). The request's session is already closed by then, so
open a fresh one. Best-effort: a TMDB failure just leaves `tmdb_enriched_at` NULL to retry on the
next open, and the bio lands from the row by then."""
db = SessionLocal()
try:
person = db.get(PlexPerson, person_id)
if person is not None:
plex_tmdb.enrich_person(db, person)
except Exception: # noqa: BLE001 — background best-effort; never surfaces to a user
db.rollback()
log.warning("background person bio enrich failed for id=%s", person_id, exc_info=True)
finally:
db.close()