73 lines
2.3 KiB
Python
73 lines
2.3 KiB
Python
"""Load/save the soundboard configuration as JSON.
|
|
|
|
Pure/headless: no audio or GUI imports, so it is fully unit-testable. Reads are
|
|
defensive — a missing or corrupt file yields a valid default config rather than crashing.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
from typing import Any
|
|
|
|
DEFAULT_CONFIG: dict[str, Any] = {
|
|
"output_device": None, # substring of the device name, e.g. "CABLE Input"
|
|
"volume": 80, # 0-100 (UI scale)
|
|
"buttons": [], # list of {"id", "label", "file_path"}
|
|
}
|
|
|
|
|
|
def default_config() -> dict[str, Any]:
|
|
"""A fresh deep copy of the default config."""
|
|
return json.loads(json.dumps(DEFAULT_CONFIG))
|
|
|
|
|
|
def _coerce(raw: Any) -> dict[str, Any]:
|
|
"""Merge a parsed object onto the defaults, dropping anything malformed."""
|
|
cfg = default_config()
|
|
if not isinstance(raw, dict):
|
|
return cfg
|
|
|
|
if isinstance(raw.get("output_device"), str):
|
|
cfg["output_device"] = raw["output_device"]
|
|
|
|
vol = raw.get("volume")
|
|
if isinstance(vol, (int, float)):
|
|
cfg["volume"] = int(max(0, min(100, vol)))
|
|
|
|
buttons = []
|
|
if isinstance(raw.get("buttons"), list):
|
|
for b in raw["buttons"]:
|
|
if not isinstance(b, dict):
|
|
continue
|
|
bid = b.get("id")
|
|
label = b.get("label")
|
|
path = b.get("file_path")
|
|
if isinstance(bid, str) and isinstance(label, str) and isinstance(path, str):
|
|
buttons.append({"id": bid, "label": label, "file_path": path})
|
|
cfg["buttons"] = buttons
|
|
return cfg
|
|
|
|
|
|
def load_config(path: str) -> dict[str, Any]:
|
|
"""Load config from ``path``; return a valid default if missing or corrupt."""
|
|
if not os.path.exists(path):
|
|
return default_config()
|
|
try:
|
|
with open(path, "r", encoding="utf-8") as fh:
|
|
raw = json.load(fh)
|
|
except (OSError, ValueError):
|
|
return default_config()
|
|
return _coerce(raw)
|
|
|
|
|
|
def save_config(path: str, config: dict[str, Any]) -> None:
|
|
"""Atomically write config to ``path`` (write temp + replace)."""
|
|
config = _coerce(config)
|
|
directory = os.path.dirname(os.path.abspath(path))
|
|
os.makedirs(directory, exist_ok=True)
|
|
tmp = path + ".tmp"
|
|
with open(tmp, "w", encoding="utf-8") as fh:
|
|
json.dump(config, fh, indent=2)
|
|
os.replace(tmp, path)
|