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
# 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
plex_max_transcodes: int = 4
# 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
# 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 app import sysconfig
from app.config import settings
from app.models import PlexItem
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"
_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
_lock = threading.Lock()
@ -168,11 +168,11 @@ def _kill(s: HlsSession) -> None:
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.
if len(_sessions) <= _MAX_SESSIONS:
if len(_sessions) <= cap:
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)
_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")
_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
# 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

View file

@ -197,12 +197,6 @@ def watch_import_now(
# --- 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")
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)."""

View file

@ -211,12 +211,6 @@ export default function PlexBrowse({
await api.plexSetState(card.id, next).catch(() => {});
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.
// 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).
@ -348,7 +342,7 @@ export default function PlexBrowse({
ep={ep}
withShowTitle
onPlay={() => onCard(ep)}
onToggleWatched={() => toggleEpisodeWatched(ep)}
onToggleWatched={() => toggleWatched(ep)}
/>
))}
</div>

View file

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

View file

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

View file

@ -12,7 +12,6 @@
"release": "Megjelenés dátuma"
},
"filter": {
"library": "Könyvtár",
"scope": "Könyvtár",
"scopeOpt": {
"both": "Mind",
@ -67,7 +66,6 @@
"noMatchesFiltered_other": "Nincs találat — {{count}} szűrő is szűkíti az eredményt.",
"loading": "Betöltés…",
"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",
"inProgress": "Folyamatban",
"play": "Lejátszás",
@ -92,7 +90,6 @@
"markSeasonUnwatched": "Évad jelölés visszavonása",
"addShowCollection": "Kollekcióhoz adás"
},
"playerSoon": "A lejátszó hamarosan jön — „{{title}}”",
"collEditor": {
"manage": "Kollekciók",
"title": "Kollekciók — {{title}}",
@ -218,9 +215,6 @@
"playAll": "Összes lejátszása",
"delete": "Lista törlése",
"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",
"layoutTree": "Fa nézet",
"removeShow": "Egész sorozat eltávolítása",