8f10353f0c
CI / lint (push) Has been cancelled
CI / js-syntax (push) Successful in 10m24s
CI / deps-audit (push) Has been cancelled
CI / test (push) Failing after 24m55s
CI / sast-bandit (push) Failing after 11m13s
CI / trivy (push) Failing after 9m32s
Docker Publish / build-and-push (push) Failing after 34m3s
141 lines
4.5 KiB
Python
141 lines
4.5 KiB
Python
"""Runtime, user-toggleable settings (persisted to disk).
|
|
|
|
These are read live (unlike the env-var constants in config.py, which are fixed
|
|
at startup), so the Settings UI can change them without a restart:
|
|
|
|
- `allow_network` — whether StemDeck answers requests from other devices.
|
|
- `max_duration_sec` — longest track accepted for processing.
|
|
- `video_max_height` — max video resolution for MP4 export / YouTube pulls.
|
|
|
|
Defaults fall back to the config.py constants (which honor their env vars), so
|
|
nothing changes until the user overrides a value.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import threading
|
|
|
|
from app.core.config import DATA_DIR, MAX_DURATION_SEC, VIDEO_MAX_HEIGHT
|
|
|
|
_log = logging.getLogger("stemdeck.settings")
|
|
|
|
_SETTINGS_PATH = DATA_DIR / "settings.json"
|
|
_LOCK = threading.RLock()
|
|
_state: dict | None = None # whole settings dict, loaded lazily
|
|
|
|
# Clamp bounds. Max track length is capped at 20 min (the product ceiling).
|
|
_DURATION_MIN, _DURATION_MAX = 60, 1200 # 1 min .. 20 min
|
|
_HEIGHT_MIN, _HEIGHT_MAX = 144, 2160
|
|
_PORT_MIN, _PORT_MAX = 1024, 65535
|
|
DEFAULT_PORT = 8000
|
|
|
|
|
|
def _default_allow_network() -> bool:
|
|
# STEMDECK_ALLOW_NETWORK takes precedence when set explicitly.
|
|
# Otherwise: desktop keeps network off (user opts in via UI toggle);
|
|
# server/Docker deployments open it by default since network access is
|
|
# the entire point of a headless deployment.
|
|
env = os.environ.get("STEMDECK_ALLOW_NETWORK")
|
|
if env is not None:
|
|
return env.strip() == "1"
|
|
return os.environ.get("STEMDECK_DESKTOP") != "1"
|
|
|
|
|
|
def _load() -> dict:
|
|
try:
|
|
data = json.loads(_SETTINGS_PATH.read_text(encoding="utf-8"))
|
|
if isinstance(data, dict):
|
|
return data
|
|
except FileNotFoundError:
|
|
pass # no settings file yet — first run; use defaults
|
|
except Exception:
|
|
# Corrupt/unreadable file: fall back to defaults rather than crash.
|
|
_log.warning("could not read settings from %s", _SETTINGS_PATH, exc_info=True)
|
|
return {}
|
|
|
|
|
|
def _ensure() -> dict:
|
|
global _state
|
|
if _state is None:
|
|
_state = _load()
|
|
return _state
|
|
|
|
|
|
def _save() -> None:
|
|
try:
|
|
_SETTINGS_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
_SETTINGS_PATH.write_text(json.dumps(_ensure()), encoding="utf-8")
|
|
except Exception:
|
|
# Persistence is best-effort (read-only FS, permissions): the in-memory
|
|
# value still applies for this session, so don't fail the request.
|
|
_log.warning("could not persist settings to %s", _SETTINGS_PATH, exc_info=True)
|
|
|
|
|
|
def _num(v: object) -> int | None:
|
|
return int(v) if isinstance(v, (int, float)) and not isinstance(v, bool) else None
|
|
|
|
|
|
# ── allow_network ──
|
|
def get_allow_network() -> bool:
|
|
with _LOCK:
|
|
v = _ensure().get("allow_network")
|
|
return v if isinstance(v, bool) else _default_allow_network()
|
|
|
|
|
|
def set_allow_network(value: bool) -> bool:
|
|
with _LOCK:
|
|
_ensure()["allow_network"] = bool(value)
|
|
_save()
|
|
return bool(value)
|
|
|
|
|
|
# ── max_duration_sec ──
|
|
def get_max_duration_sec() -> int:
|
|
with _LOCK:
|
|
v = _num(_ensure().get("max_duration_sec"))
|
|
return max(_DURATION_MIN, min(_DURATION_MAX, v)) if v is not None else MAX_DURATION_SEC
|
|
|
|
|
|
def set_max_duration_sec(value: int) -> int:
|
|
with _LOCK:
|
|
clamped = max(_DURATION_MIN, min(_DURATION_MAX, int(value)))
|
|
_ensure()["max_duration_sec"] = clamped
|
|
_save()
|
|
return clamped
|
|
|
|
|
|
# ── video_max_height ──
|
|
def get_video_max_height() -> int:
|
|
with _LOCK:
|
|
v = _num(_ensure().get("video_max_height"))
|
|
return max(_HEIGHT_MIN, min(_HEIGHT_MAX, v)) if v is not None else VIDEO_MAX_HEIGHT
|
|
|
|
|
|
def set_video_max_height(value: int) -> int:
|
|
with _LOCK:
|
|
clamped = max(_HEIGHT_MIN, min(_HEIGHT_MAX, int(value)))
|
|
_ensure()["video_max_height"] = clamped
|
|
_save()
|
|
return clamped
|
|
|
|
|
|
# ── port ──
|
|
# The preferred port the server binds on launch. The desktop launcher reads this
|
|
# (default 8000) before spawning the backend; a self-hosted server's --port wins.
|
|
# Changing it needs a restart — the socket is bound at startup.
|
|
def get_port() -> int:
|
|
with _LOCK:
|
|
v = _num(_ensure().get("port"))
|
|
return max(_PORT_MIN, min(_PORT_MAX, v)) if v is not None else DEFAULT_PORT
|
|
|
|
|
|
def set_port(value: int) -> int:
|
|
with _LOCK:
|
|
clamped = max(_PORT_MIN, min(_PORT_MAX, int(value)))
|
|
_ensure()["port"] = clamped
|
|
_save()
|
|
return clamped
|