feat(plex): P0 backend foundations — catalog model, config, client

Optional Plex module groundwork (design locked 2026-07-05): plays the LOCAL
physical file; Plex is metadata + optional watch-sync only.

- models + migration 0044: plex_libraries/plex_shows/plex_seasons/plex_items
  (playable leaf, ffprobe playability class, markers, weighted FTS search_vector)
  + plex_states (per-user watch state, mirrors video_states)
- sysconfig 'plex' group + config.py env defaults (server url/token[secret]/
  path-map/libraries/sync-interval/max-transcodes)
- app/plex/client.py (PlexClient httpx: server info, sections, items, metadata
  +markers, image) + app/plex/paths.py (Plex→local path map + file resolve)
- routes/plex.py admin POST /api/plex/test (verify connection + list sections)
This commit is contained in:
npeter83 2026-07-05 01:35:08 +02:00
parent 401fe90256
commit a88254c841
9 changed files with 552 additions and 0 deletions

View file

@ -182,6 +182,30 @@ class Settings(BaseSettings):
# Default SponsorBlock (skip sponsor segments) for new custom profiles.
sponsorblock_default: bool = False
# --- Plex integration (optional) ---
# The Plex module plays the LOCAL physical media file directly (our own ffmpeg/direct-serve);
# Plex is used only for metadata + optional watch-state sync. Requires the Plex server to be
# reachable from THIS host and its media reachable on a local mount (see plex_path_map).
plex_enabled: bool = False
# Base URL of the Plex Media Server reachable from this host, e.g. http://192.168.1.112:32400.
plex_server_url: str = ""
# Plex server admin token (X-Plex-Token). Server-side only (metadata + image proxy); never
# sent to the browser. Stored encrypted when set via the admin Config page.
plex_server_token: str = ""
# Path map: Plex's on-disk media paths → this host's read-only mount, so Siftlode can open the
# physical file for direct playback. Newline/comma-separated "plexPrefix=localPrefix" pairs;
# the longest matching prefix wins. E.g. "/data=/plex-media". Empty = identical on both sides.
plex_path_map: str = ""
# Which Plex library section keys are exposed (comma-separated). Empty = all enabled sections.
plex_libraries: str = ""
# Stable client identifier Siftlode presents to Plex (X-Plex-Client-Identifier).
plex_client_id: str = "siftlode-server"
# How often the catalog sync job mirrors Plex metadata, in minutes.
plex_sync_interval_min: int = 360
# Max concurrent transcodes for the P3 fallback. Low by default — CPU-only transcode is
# expensive; direct-serve (browser-compatible files) has no such limit.
plex_max_transcodes: int = 1
@property
def allowed_email_set(self) -> set[str]:
return {e.strip().lower() for e in self.allowed_emails.split(",") if e.strip()}