"""Typed settings accessors backed by the `setting` key/value table. A missing key must never crash anything, so every read falls back to the default and every malformed stored value falls back to the default too. """ from __future__ import annotations import sqlite3 from urllib.parse import urlparse DEFAULTS: dict[str, str] = { "retention_days": "14", "backfill_days": "7", "max_height": "720", "min_duration_seconds": "120", "sponsorblock_mark": "true", "write_subs": "true", "sub_langs": "en.*", "jellyfin_url": "http://127.0.0.1:8096", "jellyfin_api_key": "", "pot_provider_url": "http://127.0.0.1:4416", "max_attempts": "5", "disk_cap_gb": "0", } # Editable through the settings form. Everything else in the table is internal. EDITABLE = tuple(DEFAULTS) # Never rendered, never settable through the web form. SECRET_KEYS = ("admin_password_hash", "session_secret") # Shown as a masked value rather than plaintext. MASKED_KEYS = ("jellyfin_api_key",) _INT_KEYS = ( "retention_days", "backfill_days", "max_height", "min_duration_seconds", "max_attempts", "disk_cap_gb", ) _BOOL_KEYS = ("sponsorblock_mark", "write_subs") _URL_KEYS = ("jellyfin_url", "pot_provider_url") _TRUE = {"1", "true", "yes", "on"} _FALSE = {"0", "false", "no", "off", ""} class Settings: def __init__(self, conn: sqlite3.Connection): self.conn = conn def raw(self, key: str) -> str: row = self.conn.execute( "SELECT value FROM setting WHERE key = ?", (key,) ).fetchone() if row is None: return DEFAULTS.get(key, "") return row["value"] def get_str(self, key: str) -> str: return self.raw(key) def get_int(self, key: str) -> int: try: return int(self.raw(key)) except (TypeError, ValueError): return int(DEFAULTS.get(key, "0") or 0) def get_bool(self, key: str) -> bool: value = self.raw(key).strip().lower() if value in _TRUE: return True if value in _FALSE: return False return DEFAULTS.get(key, "false").lower() in _TRUE def set(self, key: str, value: str) -> None: with self.conn: self.conn.execute( "INSERT INTO setting (key, value) VALUES (?, ?) " "ON CONFLICT(key) DO UPDATE SET value = excluded.value", (key, str(value)), ) def all_editable(self) -> dict[str, str]: return {key: self.raw(key) for key in EDITABLE} def validate(key: str, value: str) -> tuple[bool, str]: """Validate one submitted setting. Returns (ok, message). On failure the caller re-renders the form with the message inline rather than raising. """ value = value.strip() if key in _INT_KEYS: try: number = int(value) except ValueError: return False, "must be a whole number" if number < 0: return False, "must be zero or greater" if key == "retention_days" and number < 1: return False, "must be at least 1 day" if key == "max_height" and number < 144: return False, "must be at least 144" return True, "" if key in _BOOL_KEYS: if value.lower() not in _TRUE | _FALSE: return False, "must be true or false" return True, "" if key in _URL_KEYS: parsed = urlparse(value) if parsed.scheme not in ("http", "https") or not parsed.netloc: return False, "must be a http:// or https:// URL" return True, "" if key == "sub_langs": if not value: return False, "must not be empty" return True, "" if key == "jellyfin_api_key": return True, "" return key in DEFAULTS, "unknown setting" def validate_all(submitted: dict[str, str]) -> dict[str, str]: """Return {key: error} for everything that failed validation.""" errors: dict[str, str] = {} for key, value in submitted.items(): if key not in EDITABLE: continue ok, message = validate(key, value) if not ok: errors[key] = message return errors