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:
commit
1595580e12
7 changed files with 9 additions and 37 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)."""
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue