"""Persist the settings an administrator edits in the UI. config.py holds the defaults, but anything changed at runtime has to survive a restart — otherwise editing the brand or the transcode ladder appears to work and is silently reverted the next time the backend starts. Writes are atomic (temp file + replace) so a crash mid-save cannot leave a half-written file that fails to parse on boot. """ import json from pathlib import Path from config import settings STORE_PATH = Path(__file__).resolve().parent.parent / "settings.json" # Settings the UI may change, with the attribute they map to on Settings. _MANAGED = ( "brand_name", "brand_tagline", "brand_logo_url", "load_balance_strategy", "multicast_enabled", "transcode_profiles", "qualities", ) def load_settings() -> None: """Apply saved overrides on top of the config defaults. Never raises.""" if not STORE_PATH.exists(): return try: saved = json.loads(STORE_PATH.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): # A corrupt file must not stop the server from booting; the defaults from # config.py are still usable. return if not isinstance(saved, dict): return for key, value in saved.items(): # Only known keys, so a stale or hand-edited file cannot inject arbitrary # attributes onto the settings object. if key in _MANAGED and value is not None: setattr(settings, key, value) def save_settings() -> None: payload = {key: getattr(settings, key) for key in _MANAGED} temporary = STORE_PATH.with_suffix(".json.tmp") temporary.write_text( json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8", ) temporary.replace(STORE_PATH)