diff --git a/backend/app/config.py b/backend/app/config.py index c83f0f7..3f75965 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -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 diff --git a/backend/app/plex/stream.py b/backend/app/plex/stream.py index 129e050..5d1f5c2 100644 --- a/backend/app/plex/stream.py +++ b/backend/app/plex/stream.py @@ -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 diff --git a/backend/app/routes/plex.py b/backend/app/routes/plex.py index af1a177..3b8b408 100644 --- a/backend/app/routes/plex.py +++ b/backend/app/routes/plex.py @@ -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).""" diff --git a/frontend/src/components/PlexBrowse.tsx b/frontend/src/components/PlexBrowse.tsx index 0164b4c..9a7f341 100644 --- a/frontend/src/components/PlexBrowse.tsx +++ b/frontend/src/components/PlexBrowse.tsx @@ -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)} /> ))} diff --git a/frontend/src/i18n/locales/de/plex.json b/frontend/src/i18n/locales/de/plex.json index 49f8a0d..c239936 100644 --- a/frontend/src/i18n/locales/de/plex.json +++ b/frontend/src/i18n/locales/de/plex.json @@ -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", diff --git a/frontend/src/i18n/locales/en/plex.json b/frontend/src/i18n/locales/en/plex.json index aa75fed..9b78e0a 100644 --- a/frontend/src/i18n/locales/en/plex.json +++ b/frontend/src/i18n/locales/en/plex.json @@ -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", diff --git a/frontend/src/i18n/locales/hu/plex.json b/frontend/src/i18n/locales/hu/plex.json index d8d6a1a..e922099 100644 --- a/frontend/src/i18n/locales/hu/plex.json +++ b/frontend/src/i18n/locales/hu/plex.json @@ -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",