feat: asset-based image/video sources, notification sounds, UI improvements
Some checks failed
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

View File

@@ -2,7 +2,6 @@
import time
import httpx
import numpy as np
from fastapi import APIRouter, HTTPException, Depends, Query, WebSocket, WebSocketDisconnect
@@ -87,8 +86,8 @@ async def create_pp_template(
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Failed to create postprocessing template: {e}")
raise HTTPException(status_code=500, detail=str(e))
logger.error("Failed to create postprocessing template: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")
@router.get("/api/v1/postprocessing-templates/{template_id}", response_model=PostprocessingTemplateResponse, tags=["Postprocessing Templates"])
@@ -130,8 +129,8 @@ async def update_pp_template(
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Failed to update postprocessing template: {e}")
raise HTTPException(status_code=500, detail=str(e))
logger.error("Failed to update postprocessing template: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")
@router.delete("/api/v1/postprocessing-templates/{template_id}", status_code=204, tags=["Postprocessing Templates"])
@@ -162,8 +161,8 @@ async def delete_pp_template(
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Failed to delete postprocessing template: {e}")
raise HTTPException(status_code=500, detail=str(e))
logger.error("Failed to delete postprocessing template: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/api/v1/postprocessing-templates/{template_id}/test", response_model=TemplateTestResponse, tags=["Postprocessing Templates"])
@@ -197,28 +196,21 @@ async def test_pp_template(
from wled_controller.utils.image_codec import (
encode_jpeg_data_uri,
load_image_bytes,
load_image_file,
thumbnail as make_thumbnail,
)
if isinstance(raw_stream, StaticImagePictureSource):
# Static image: load directly
from pathlib import Path
# Static image: load from asset
from wled_controller.api.dependencies import get_asset_store as _get_asset_store
asset_store = _get_asset_store()
image_path = asset_store.get_file_path(raw_stream.image_asset_id) if raw_stream.image_asset_id else None
if not image_path:
raise HTTPException(status_code=400, detail="Image asset not found or missing file")
source = raw_stream.image_source
start_time = time.perf_counter()
if source.startswith(("http://", "https://")):
async with httpx.AsyncClient(timeout=15, follow_redirects=True) as client:
resp = await client.get(source)
resp.raise_for_status()
image = load_image_bytes(resp.content)
else:
path = Path(source)
if not path.exists():
raise HTTPException(status_code=400, detail=f"Image file not found: {source}")
image = load_image_file(path)
image = load_image_file(image_path)
actual_duration = time.perf_counter() - start_time
frame_count = 1
@@ -330,13 +322,14 @@ async def test_pp_template(
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Postprocessing template test failed: {e}")
raise HTTPException(status_code=500, detail=str(e))
logger.error("Postprocessing template test failed: %s", e, exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")
finally:
if stream:
try:
stream.cleanup()
except Exception:
except Exception as e:
logger.debug("PP test capture stream cleanup: %s", e)
pass
@@ -434,6 +427,7 @@ async def test_pp_template_ws(
preview_width=preview_width or None,
)
except WebSocketDisconnect:
logger.debug("PP template test WebSocket disconnected for %s", template_id)
pass
except Exception as e:
logger.error(f"PP template test WS error for {template_id}: {e}")