Files
siftlode/backend/app/routes/plex/browse.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

132 lines
4.6 KiB
Python

"""Plex browse: the library list, the unified cross-library browse feed, and its faceted filters."""
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app import sysconfig
from app.auth import current_user
from app.db import get_db
from app.mailtext import lang_for_user
from app.models import User
from app.plex import library as plex_library
from app.plex import queries
from app.plex.cards import display_title, episode_card
from app.plex.query import LibraryFilters
from app.routes.plex._shared import _norm_lang
router = APIRouter(prefix="/api/plex", tags=["plex"])
@router.get("/libraries")
def list_libraries(user: User = Depends(current_user), db: Session = Depends(get_db)) -> dict:
"""The mirrored libraries the user can browse (drives the Plex scope selector). HU overlay
libraries are excluded — they're not browsable, they only feed the EN twin's Hungarian metadata."""
out = [
{"key": lib.plex_key, "title": lib.title, "kind": lib.kind, "count": count}
for lib, count in queries.browsable_libraries(db, lang_for_user(user))
]
return {"enabled": sysconfig.get_bool(db, "plex_enabled"), "libraries": out}
@router.get("/library")
def unified_library(
scope: str = "both",
q: str | None = None,
field: str = "all",
sort: str = "added",
sort_dir: str = "desc",
show: str = "all",
lang: str = "en",
offset: int = 0,
limit: int = Query(default=40, ge=1, le=100),
f: LibraryFilters = Depends(),
user: User = Depends(current_user),
db: Session = Depends(get_db),
) -> dict:
"""Unified cross-library browse: movies + shows in ONE mixed, paginated feed, scoped to
movie|show|both, with the shared filters/search/watch-state (a show's state is the aggregate of
its episodes). On a search that also matches episodes (and shows are in scope), the matching
episodes come back in a separate `episodes` list (grouped result — the 'Episodes' section)."""
offset = max(0, offset)
lang = _norm_lang(lang)
total, rows, eps = plex_library.library_page(
db,
user,
scope=scope,
q=q,
field=field,
sort=sort,
sort_dir=sort_dir,
show=show,
lang=lang,
offset=offset,
limit=limit,
f=f,
)
items = []
for r in rows:
if r.kind == "movie":
items.append(
{
"id": r.rk,
"type": "movie",
"title": r.title,
"year": r.year,
"rating": r.rating,
"summary": r.summary,
"duration_seconds": r.duration_s,
"thumb": f"/api/plex/image/{r.rk}",
"playable": r.playable,
"transcode_hw": r.transcode_hw,
"status": r.status,
"position_seconds": r.position_seconds,
}
)
else:
items.append(
{
"id": r.rk,
"type": "show",
"title": r.title,
"year": r.year,
"rating": r.rating,
"summary": r.summary,
"thumb": f"/api/plex/image/{r.rk}",
"season_count": r.season_count,
"status": r.status,
"resumable": r.resumable,
}
)
episodes = []
for e, s, sh in eps:
card = episode_card(e, s, lang=lang)
card["show_title"] = display_title(sh, lang) if sh is not None else None
episodes.append(card)
return {
"scope": scope,
"total": total,
"offset": offset,
"limit": limit,
"items": items,
"episodes": episodes,
}
@router.get("/facets")
def facets(
scope: str = "both",
show: str = "all",
lang: str = "en",
f: LibraryFilters = Depends(),
user: User = Depends(current_user),
db: Session = Depends(get_db),
) -> dict:
"""Available filter values for the unified library (scope movie|show|both), ADAPTED to the active
filters (faceted search): each group's values/bounds are computed with all the OTHER active
filters applied but NOT its own — so selecting a filter narrows what the other groups offer, while
a multi-select group still lets you add more. Merged across the scope's libraries; duration is
movie-only. (Search text is not factored in — facets track the sidebar filters.)"""
return plex_library.facets(db, user, scope=scope, show=show, lang=lang, f=f)