feat(plex): P1 backend — catalog sync + browse/search/show/image API

- app/plex/sync.py: full reconcile of enabled movie/show sections into plex_*
  (upsert by rating_key). Reuses Plex's own codec info to classify browser
  playability (direct/remux/transcode) — no ffprobe. Movies + episodes become
  playable leaves; shows/seasons mirrored for the drill-down.
- routes/plex.py: GET /libraries, /browse (FTS search + sort + paging, movie
  leaves or show cards), /show/{rk} (seasons+episodes, per-user state), /image
  (proxies Plex art, no duplication); admin POST /sync. All auth'd, demo-allowed.
- scheduler: plex_sync job (interval settings.plex_sync_interval_min).
- Verified against the real server: 2 libs, 3206 movies, 260 shows/961 seasons/
  14768 episodes synced in 13s; FTS search + drill-down work. Playability:
  ~1% direct, 89% remux, 6% transcode (informs P2/P3).
This commit is contained in:
npeter83 2026-07-05 02:20:23 +02:00
parent 9afb1a9788
commit 63c6e782c8
4 changed files with 483 additions and 16 deletions

View file

@ -14,6 +14,7 @@ from app.config import settings
from app.db import SessionLocal
from app.downloads.gc import run_download_gc
from app.models import SchedulerSetting
from app.plex.sync import sync as run_plex_sync
from app.notifications import create_notification
from app.state import is_sync_paused
from app.sync.autotag import run_autotag_all
@ -61,6 +62,7 @@ JOB_INTERVALS: dict[str, int] = {
"demo_reset": settings.demo_reset_minutes,
"explore_cleanup": settings.explore_cleanup_minutes,
"download_gc": settings.download_gc_minutes,
"plex_sync": settings.plex_sync_interval_min,
}
# Sane bounds for an admin-set interval (minutes).
@ -284,6 +286,12 @@ def _download_gc_job() -> None:
_job("download_gc", run_download_gc)
def _plex_sync_job() -> None:
# Mirror the enabled Plex library sections into the local catalog. Pure metadata (no YouTube
# quota); a no-op when the Plex module is disabled.
_job("plex_sync", run_plex_sync)
# job_id -> wrapper. The single source of truth for which jobs exist and how to run one,
# shared by start_scheduler (recurring registration) and trigger_job (manual "run now").
JOB_FUNCS: dict[str, Callable[[], None]] = {
@ -298,6 +306,7 @@ JOB_FUNCS: dict[str, Callable[[], None]] = {
"demo_reset": _demo_reset_job,
"explore_cleanup": _explore_cleanup_job,
"download_gc": _download_gc_job,
"plex_sync": _plex_sync_job,
}