Merge chore/code-hygiene: Phase 2 #9 Plex (first pass — transcode config-drift + safe cleanups)

Fix: honor the admin max-concurrent-transcodes setting (was hardcoded). Cleanup:
dead _enabled guard, watch-toggle dedup, dead plex i18n keys. The large Plex
backlog (watch_sync sync-races, plex.py FilterParams dup, PlexPlayer bugs,
sidebar/format dedup) is DEFERRED to a focused follow-up pass. Verified: ruff,
tsc, re-review clean, localdev boots, Plex UI renders.
This commit is contained in:
npeter83 2026-07-11 21:53:25 +02:00
commit 1595580e12
7 changed files with 9 additions and 37 deletions

View file

@ -211,7 +211,7 @@ class Settings(BaseSettings):
plex_watch_reconcile_interval_min: int = 1440 plex_watch_reconcile_interval_min: int = 1440
# Max concurrent transcodes for the P3 fallback. Low by default — CPU-only transcode is # Max concurrent transcodes for the P3 fallback. Low by default — CPU-only transcode is
# expensive; direct-serve (browser-compatible files) has no such limit. # expensive; direct-serve (browser-compatible files) has no such limit.
plex_max_transcodes: int = 1 plex_max_transcodes: int = 4
# Where on-the-fly HLS remux segments are written (transient, reaped). Empty → a `.plex-hls` # Where on-the-fly HLS remux segments are written (transient, reaped). Empty → a `.plex-hls`
# dir under download_root. On localdev the download_root is a slow Windows bind-mount, so point # dir under download_root. On localdev the download_root is a slow Windows bind-mount, so point
# this at a fast container-local path (e.g. /var/tmp/plex-hls); prod's download_root is fast # this at a fast container-local path (e.g. /var/tmp/plex-hls); prod's download_root is fast

View file

@ -23,6 +23,7 @@ from pathlib import Path
from sqlalchemy.orm import Session as DbSession from sqlalchemy.orm import Session as DbSession
from app import sysconfig
from app.config import settings from app.config import settings
from app.models import PlexItem from app.models import PlexItem
from app.plex import paths from app.plex import paths
@ -31,7 +32,6 @@ log = logging.getLogger("siftlode.plex")
_HLS_ROOT = Path(settings.plex_hls_dir) if settings.plex_hls_dir else Path(settings.download_root) / ".plex-hls" _HLS_ROOT = Path(settings.plex_hls_dir) if settings.plex_hls_dir else Path(settings.download_root) / ".plex-hls"
_SEG_SECONDS = 6 _SEG_SECONDS = 6
_MAX_SESSIONS = 4 # concurrent remux sessions (safety valve for the CPU-only host)
_SESSION_IDLE_S = 600 # reap a session with no access for this long _SESSION_IDLE_S = 600 # reap a session with no access for this long
_lock = threading.Lock() _lock = threading.Lock()
@ -168,11 +168,11 @@ def _kill(s: HlsSession) -> None:
shutil.rmtree(s.dir, ignore_errors=True) shutil.rmtree(s.dir, ignore_errors=True)
def _enforce_cap() -> None: def _enforce_cap(cap: int) -> None:
# Caller holds _lock. Drop the least-recently-accessed sessions over the cap. # Caller holds _lock. Drop the least-recently-accessed sessions over the cap.
if len(_sessions) <= _MAX_SESSIONS: if len(_sessions) <= cap:
return return
for key, s in sorted(_sessions.items(), key=lambda kv: kv[1].last_access)[: len(_sessions) - _MAX_SESSIONS]: for key, s in sorted(_sessions.items(), key=lambda kv: kv[1].last_access)[: len(_sessions) - cap]:
_kill(s) _kill(s)
_sessions.pop(key, None) _sessions.pop(key, None)
@ -211,7 +211,9 @@ def start_session(
) )
s = HlsSession(key, directory, proc, start_s, "master.m3u8" if is_multi else "index.m3u8") s = HlsSession(key, directory, proc, start_s, "master.m3u8" if is_multi else "index.m3u8")
_sessions[key] = s _sessions[key] = s
_enforce_cap() # Admin-configurable concurrency cap (Configuration → Plex → Max concurrent transcodes);
# at least 1 so playback can't be capped to zero.
_enforce_cap(max(1, sysconfig.get_int(db, "plex_max_transcodes")))
# Learn the REAL start offset K from the first VIDEO segment (outside the lock — this blocks on # Learn the REAL start offset K from the first VIDEO segment (outside the lock — this blocks on
# ffmpeg producing it, and we must not hold up other sessions). The client reads `media_start_s`, so # ffmpeg producing it, and we must not hold up other sessions). The client reads `media_start_s`, so
# its absolute clock + subtitle shift key off the true keyframe, not the requested offset. On # its absolute clock + subtitle shift key off the true keyframe, not the requested offset. On

View file

@ -197,12 +197,6 @@ def watch_import_now(
# --- Read: libraries / browse / show / image -------------------------------------------------- # --- Read: libraries / browse / show / image --------------------------------------------------
def _enabled() -> None:
"""Guard read endpoints when the module is off (avoids leaking a stale mirror)."""
# Read endpoints stay usable as long as a mirror exists; the toggle only gates sync + UI.
return None
@router.get("/libraries") @router.get("/libraries")
def list_libraries(user: User = Depends(current_user), db: Session = Depends(get_db)) -> dict: 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).""" """The mirrored libraries the user can browse (drives the Plex scope selector)."""

View file

@ -211,12 +211,6 @@ export default function PlexBrowse({
await api.plexSetState(card.id, next).catch(() => {}); await api.plexSetState(card.id, next).catch(() => {});
qc.invalidateQueries({ queryKey: ["plex-library"] }); qc.invalidateQueries({ queryKey: ["plex-library"] });
} }
async function toggleEpisodeWatched(ep: PlexCard) {
const next = ep.status === "watched" ? "new" : "watched";
optimisticStatus(ep.id, next);
await api.plexSetState(ep.id, next).catch(() => {});
qc.invalidateQueries({ queryKey: ["plex-library"] });
}
// Apply a metadata filter (clicked on an info page / show hero / cast) then show the unified grid. // Apply a metadata filter (clicked on an info page / show hero / cast) then show the unified grid.
// Array filters (genres/people/studios) UNION with what's set; scalars replace. Clicking a person // Array filters (genres/people/studios) UNION with what's set; scalars replace. Clicking a person
// (cast/crew) widens to the 'both' scope so their movies AND shows show up together (mixed feed). // (cast/crew) widens to the 'both' scope so their movies AND shows show up together (mixed feed).
@ -348,7 +342,7 @@ export default function PlexBrowse({
ep={ep} ep={ep}
withShowTitle withShowTitle
onPlay={() => onCard(ep)} onPlay={() => onCard(ep)}
onToggleWatched={() => toggleEpisodeWatched(ep)} onToggleWatched={() => toggleWatched(ep)}
/> />
))} ))}
</div> </div>

View file

@ -12,7 +12,6 @@
"release": "Erscheinungsdatum" "release": "Erscheinungsdatum"
}, },
"filter": { "filter": {
"library": "Bibliothek",
"scope": "Bibliothek", "scope": "Bibliothek",
"scopeOpt": { "scopeOpt": {
"both": "Alle", "both": "Alle",
@ -67,7 +66,6 @@
"noMatchesFiltered_other": "Keine Treffer — {{count}} Filter schränken die Ergebnisse zusätzlich ein.", "noMatchesFiltered_other": "Keine Treffer — {{count}} Filter schränken die Ergebnisse zusätzlich ein.",
"loading": "Wird geladen…", "loading": "Wird geladen…",
"empty": "Noch nichts hier. Starte eine Plex-Synchronisierung auf der Admin-Konfigurationsseite.", "empty": "Noch nichts hier. Starte eine Plex-Synchronisierung auf der Admin-Konfigurationsseite.",
"loadMore": "Mehr laden",
"watched": "Angesehen", "watched": "Angesehen",
"inProgress": "Läuft", "inProgress": "Läuft",
"play": "Abspielen", "play": "Abspielen",
@ -92,7 +90,6 @@
"markSeasonUnwatched": "Staffel als ungesehen", "markSeasonUnwatched": "Staffel als ungesehen",
"addShowCollection": "Zur Sammlung" "addShowCollection": "Zur Sammlung"
}, },
"playerSoon": "Player kommt bald — „{{title}}“",
"collEditor": { "collEditor": {
"manage": "Sammlungen", "manage": "Sammlungen",
"title": "Sammlungen — {{title}}", "title": "Sammlungen — {{title}}",
@ -218,9 +215,6 @@
"playAll": "Alle abspielen", "playAll": "Alle abspielen",
"delete": "Playlist löschen", "delete": "Playlist löschen",
"empty": "Diese Playlist ist leer. Füge Titel über deren Infoseite hinzu.", "empty": "Diese Playlist ist leer. Füge Titel über deren Infoseite hinzu.",
"up": "Nach oben",
"down": "Nach unten",
"remove": "Entfernen",
"layoutAccordion": "Akkordeon-Ansicht", "layoutAccordion": "Akkordeon-Ansicht",
"layoutTree": "Baum-Ansicht", "layoutTree": "Baum-Ansicht",
"removeShow": "Ganze Serie entfernen", "removeShow": "Ganze Serie entfernen",

View file

@ -12,7 +12,6 @@
"release": "Release date" "release": "Release date"
}, },
"filter": { "filter": {
"library": "Library",
"scope": "Library", "scope": "Library",
"scopeOpt": { "scopeOpt": {
"both": "All", "both": "All",
@ -67,7 +66,6 @@
"noMatchesFiltered_other": "No matches — {{count}} filters are also narrowing the results.", "noMatchesFiltered_other": "No matches — {{count}} filters are also narrowing the results.",
"loading": "Loading…", "loading": "Loading…",
"empty": "Nothing here yet. Run a Plex sync from the admin Config page.", "empty": "Nothing here yet. Run a Plex sync from the admin Config page.",
"loadMore": "Load more",
"watched": "Watched", "watched": "Watched",
"inProgress": "In progress", "inProgress": "In progress",
"play": "Play", "play": "Play",
@ -92,7 +90,6 @@
"markSeasonUnwatched": "Mark season unwatched", "markSeasonUnwatched": "Mark season unwatched",
"addShowCollection": "Add to collection" "addShowCollection": "Add to collection"
}, },
"playerSoon": "Player coming soon — “{{title}}”",
"collEditor": { "collEditor": {
"manage": "Collections", "manage": "Collections",
"title": "Collections — {{title}}", "title": "Collections — {{title}}",
@ -218,9 +215,6 @@
"playAll": "Play all", "playAll": "Play all",
"delete": "Delete playlist", "delete": "Delete playlist",
"empty": "This playlist is empty. Add titles from their info page.", "empty": "This playlist is empty. Add titles from their info page.",
"up": "Move up",
"down": "Move down",
"remove": "Remove",
"layoutAccordion": "Accordion view", "layoutAccordion": "Accordion view",
"layoutTree": "Tree view", "layoutTree": "Tree view",
"removeShow": "Remove whole show", "removeShow": "Remove whole show",

View file

@ -12,7 +12,6 @@
"release": "Megjelenés dátuma" "release": "Megjelenés dátuma"
}, },
"filter": { "filter": {
"library": "Könyvtár",
"scope": "Könyvtár", "scope": "Könyvtár",
"scopeOpt": { "scopeOpt": {
"both": "Mind", "both": "Mind",
@ -67,7 +66,6 @@
"noMatchesFiltered_other": "Nincs találat — {{count}} szűrő is szűkíti az eredményt.", "noMatchesFiltered_other": "Nincs találat — {{count}} szűrő is szűkíti az eredményt.",
"loading": "Betöltés…", "loading": "Betöltés…",
"empty": "Még nincs itt semmi. Futtass egy Plex-szinkront az admin Konfiguráció oldalról.", "empty": "Még nincs itt semmi. Futtass egy Plex-szinkront az admin Konfiguráció oldalról.",
"loadMore": "Több betöltése",
"watched": "Megnézve", "watched": "Megnézve",
"inProgress": "Folyamatban", "inProgress": "Folyamatban",
"play": "Lejátszás", "play": "Lejátszás",
@ -92,7 +90,6 @@
"markSeasonUnwatched": "Évad jelölés visszavonása", "markSeasonUnwatched": "Évad jelölés visszavonása",
"addShowCollection": "Kollekcióhoz adás" "addShowCollection": "Kollekcióhoz adás"
}, },
"playerSoon": "A lejátszó hamarosan jön — „{{title}}”",
"collEditor": { "collEditor": {
"manage": "Kollekciók", "manage": "Kollekciók",
"title": "Kollekciók — {{title}}", "title": "Kollekciók — {{title}}",
@ -218,9 +215,6 @@
"playAll": "Összes lejátszása", "playAll": "Összes lejátszása",
"delete": "Lista törlése", "delete": "Lista törlése",
"empty": "Ez a lista üres. Adj hozzá címeket az info-oldalukról.", "empty": "Ez a lista üres. Adj hozzá címeket az info-oldalukról.",
"up": "Fel",
"down": "Le",
"remove": "Eltávolítás",
"layoutAccordion": "Harmonika nézet", "layoutAccordion": "Harmonika nézet",
"layoutTree": "Fa nézet", "layoutTree": "Fa nézet",
"removeShow": "Egész sorozat eltávolítása", "removeShow": "Egész sorozat eltávolítása",