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.
132 lines
4.6 KiB
Python
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)
|