feat: asset-based image/video sources, notification sounds, UI improvements
Lint & Test / test (push) Has been cancelled
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:
@@ -0,0 +1,80 @@
|
||||
"""Asset data model.
|
||||
|
||||
An Asset represents an uploaded file (sound, image, video, or other)
|
||||
stored on the server. Assets are referenced by ID from other entities
|
||||
(e.g. NotificationColorStripSource uses sound assets for alert sounds).
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
# Map MIME type prefixes to asset_type categories
|
||||
_MIME_TO_ASSET_TYPE = {
|
||||
"audio/": "sound",
|
||||
"image/": "image",
|
||||
"video/": "video",
|
||||
}
|
||||
|
||||
|
||||
def asset_type_from_mime(mime_type: str) -> str:
|
||||
"""Derive asset_type from a MIME type string."""
|
||||
for prefix, asset_type in _MIME_TO_ASSET_TYPE.items():
|
||||
if mime_type.startswith(prefix):
|
||||
return asset_type
|
||||
return "other"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Asset:
|
||||
"""Persistent metadata for an uploaded file asset."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
filename: str # original upload filename
|
||||
stored_filename: str # on-disk filename (uuid-based)
|
||||
mime_type: str # e.g. "audio/wav", "image/png"
|
||||
asset_type: str # "sound" | "image" | "video" | "other"
|
||||
size_bytes: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
description: Optional[str] = None
|
||||
tags: List[str] = field(default_factory=list)
|
||||
prebuilt: bool = False # True for shipped assets
|
||||
deleted: bool = False # soft-delete for prebuilt assets
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"filename": self.filename,
|
||||
"stored_filename": self.stored_filename,
|
||||
"mime_type": self.mime_type,
|
||||
"asset_type": self.asset_type,
|
||||
"size_bytes": self.size_bytes,
|
||||
"description": self.description,
|
||||
"tags": self.tags,
|
||||
"prebuilt": self.prebuilt,
|
||||
"deleted": self.deleted,
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> "Asset":
|
||||
return Asset(
|
||||
id=data["id"],
|
||||
name=data["name"],
|
||||
filename=data.get("filename", ""),
|
||||
stored_filename=data.get("stored_filename", ""),
|
||||
mime_type=data.get("mime_type", "application/octet-stream"),
|
||||
asset_type=data.get("asset_type", "other"),
|
||||
size_bytes=int(data.get("size_bytes", 0)),
|
||||
description=data.get("description"),
|
||||
tags=data.get("tags", []),
|
||||
prebuilt=bool(data.get("prebuilt", False)),
|
||||
deleted=bool(data.get("deleted", False)),
|
||||
created_at=datetime.fromisoformat(data["created_at"]),
|
||||
updated_at=datetime.fromisoformat(data["updated_at"]),
|
||||
)
|
||||
Reference in New Issue
Block a user