feat: asset-based image/video sources, notification sounds, UI improvements
Some checks failed
Lint & Test / test (push) Has been cancelled
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:
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user