feat: asset-based image/video sources, notification sounds, UI improvements
Lint & Test / test (push) Has been cancelled

- Replace URL-based image_source/url fields with image_asset_id/video_asset_id
  on StaticImagePictureSource and VideoCaptureSource (clean break, no migration)
- Resolve asset IDs to file paths at runtime via AssetStore.get_file_path()
- Add EntitySelect asset pickers for image/video in stream editor modal
- Add notification sound configuration (global sound + per-app overrides)
- Unify per-app color and sound overrides into single "Per-App Overrides" section
- Persist notification history between server restarts
- Add asset management system (upload, edit, delete, soft-delete)
- Replace emoji buttons with SVG icons throughout UI
- Various backend improvements: SQLite stores, auth, backup, MQTT, webhooks
This commit is contained in:
2026-03-26 20:40:25 +03:00
parent c0853ce184
commit e2e1107df7
100 changed files with 2935 additions and 992 deletions
+21 -2
View File
@@ -55,9 +55,19 @@ _ENTITY_TABLES = [
"color_strip_processing_templates",
"gradients",
"weather_sources",
"assets",
]
_VALID_TABLES = frozenset(_ENTITY_TABLES) | {"settings", "schema_version"}
def _check_table(table: str) -> None:
"""Raise ValueError if *table* is not a known entity table."""
if table not in _VALID_TABLES:
raise ValueError(f"Invalid table name: {table!r}")
class Database:
"""Thread-safe SQLite connection wrapper with WAL mode.
@@ -169,6 +179,7 @@ class Database:
Returns list of dicts parsed from the ``data`` JSON column.
"""
_check_table(table)
with self._lock:
rows = self._conn.execute(
f"SELECT id, data FROM [{table}]"
@@ -187,6 +198,7 @@ class Database:
Skipped silently when writes are frozen.
"""
_check_table(table)
if _writes_frozen:
return
json_data = json.dumps(data, ensure_ascii=False)
@@ -202,6 +214,7 @@ class Database:
Skipped silently when writes are frozen.
"""
_check_table(table)
if _writes_frozen:
return
with self._lock:
@@ -215,6 +228,7 @@ class Database:
Skipped silently when writes are frozen.
"""
_check_table(table)
if _writes_frozen:
return
with self._lock:
@@ -226,6 +240,7 @@ class Database:
Skipped silently when writes are frozen.
"""
_check_table(table)
if _writes_frozen:
return
with self._lock:
@@ -237,6 +252,7 @@ class Database:
def count(self, table: str) -> int:
"""Count rows in an entity table."""
_check_table(table)
with self._lock:
row = self._conn.execute(
f"SELECT COUNT(*) as cnt FROM [{table}]"
@@ -245,13 +261,15 @@ class Database:
def table_exists_with_data(self, table: str) -> bool:
"""Check if a table exists and has at least one row."""
_check_table(table)
with self._lock:
try:
row = self._conn.execute(
f"SELECT COUNT(*) as cnt FROM [{table}]"
).fetchone()
return row["cnt"] > 0
except sqlite3.OperationalError:
except sqlite3.OperationalError as e:
logger.debug("Table %s does not exist or is inaccessible: %s", table, e)
return False
# -- Settings (key-value) ------------------------------------------------
@@ -266,7 +284,8 @@ class Database:
return None
try:
return json.loads(row["value"])
except json.JSONDecodeError:
except json.JSONDecodeError as e:
logger.warning("Corrupt JSON in setting '%s': %s", key, e)
return None
def set_setting(self, key: str, value: dict) -> None: