0f5850ef80
Extends the icon-plate work from devices and output targets to every
remaining card type — 18 new entities, 20 in total. Users can now pick
a curated icon (with optional colour override) for any card on any tab,
and the picker reuses the same modal, recent-strip, search, and
category tabs introduced for the device picker.
Foundation:
- icon-picker.ts — replace the hardcoded 2-entry adapter record with a
Map<EntityType, EntityTypeAdapter> and expose
registerIconEntityType() + makeSimpleIconAdapter() so each feature
module owns its own adapter (~6 lines per type).
- bodyExtras hook on adapters, keyed off id, lets discriminated routes
(output-targets target_type, picture-sources stream_type, audio /
value / color-strip-sources source_type) accept icon-only PUTs.
- core/card-icon.ts — new makeCardIconFields(type, id, entity) helper
spreads iconHtml / iconColor / iconAttrs into a mod-card head in one
line.
- _onDocumentClick now accepts any registered type instead of a
hardcoded device/target check.
Backend (purely additive — no migrations needed thanks to JSON-blob
storage):
- 18 dataclasses gained icon: str = "" + icon_color: str = "" with
emit-when-truthy serialisation and "" defaults on load.
- All matching Create / Update / Response Pydantic schemas gained the
fields with the standard Optional[str] + max_length=64/32 +
description set.
- All routes' response builders use
getattr(entity, "icon", "") or "" so existing rows render unchanged.
- ValueSource and CSS handle icon/icon_color on the base class so all
source-type subclasses inherit them automatically.
Frontend wiring (12 modules):
- streams.ts — picture sources, capture templates, PP templates,
CSPT, audio sources, audio templates, gradients (built-in
gradients keep no plate).
- automations, scene-presets, sync-clocks, weather-sources,
value-sources, mqtt-sources, home-assistant-sources,
game-integration, audio-processing-templates, assets,
color-strips/cards.
- pattern-templates skipped — uses the legacy wrapCard({content,
actions}) string API, separate migration.
Dashboard cards now also display the chosen icon:
- Targets already had it (with device inheritance for LED targets).
- Sync clocks, automations, and scene presets gained the same plate
via a shared _dashboardIconPlate helper that mirrors the mod-card
layout (mod-head--with-icon class flips on when present).
i18n: 20 new device.icon.entity.<type> labels in en/ru/zh.
Verification:
- ruff check src/ tests/ — clean.
- npx tsc --noEmit — clean.
- npm run build — 2.6 MB bundle.
- pytest tests/ --no-cov — 949 passed (no regressions).
Pending: manual smoke test on each card type — open picker, save, and
confirm the channel-color preview matches the live card.
191 lines
7.0 KiB
Python
191 lines
7.0 KiB
Python
"""Audio processing template routes."""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
|
|
from ledgrab.api.auth import AuthRequired
|
|
from ledgrab.api.dependencies import (
|
|
fire_entity_event,
|
|
get_audio_processing_template_store,
|
|
get_audio_source_store,
|
|
get_processor_manager,
|
|
)
|
|
from ledgrab.api.schemas.audio_processing import (
|
|
AudioProcessingTemplateCreate,
|
|
AudioProcessingTemplateListResponse,
|
|
AudioProcessingTemplateResponse,
|
|
AudioProcessingTemplateUpdate,
|
|
)
|
|
from ledgrab.api.schemas.filters import FilterInstanceSchema
|
|
from ledgrab.core.filters.filter_instance import FilterInstance
|
|
from ledgrab.storage.audio_processing_template_store import AudioProcessingTemplateStore
|
|
from ledgrab.storage.base_store import EntityNotFoundError
|
|
from ledgrab.utils import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _apt_to_response(t) -> AudioProcessingTemplateResponse:
|
|
"""Convert an AudioProcessingTemplate to its API response."""
|
|
return AudioProcessingTemplateResponse(
|
|
id=t.id,
|
|
name=t.name,
|
|
filters=[FilterInstanceSchema(filter_id=f.filter_id, options=f.options) for f in t.filters],
|
|
created_at=t.created_at,
|
|
updated_at=t.updated_at,
|
|
description=t.description,
|
|
tags=t.tags,
|
|
icon=getattr(t, "icon", "") or "",
|
|
icon_color=getattr(t, "icon_color", "") or "",
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/api/v1/audio-processing-templates",
|
|
response_model=AudioProcessingTemplateListResponse,
|
|
tags=["Audio Processing Templates"],
|
|
)
|
|
async def list_audio_processing_templates(
|
|
_auth: AuthRequired,
|
|
store: AudioProcessingTemplateStore = Depends(get_audio_processing_template_store),
|
|
):
|
|
"""List all audio processing templates."""
|
|
templates = store.get_all_templates()
|
|
responses = [_apt_to_response(t) for t in templates]
|
|
return AudioProcessingTemplateListResponse(templates=responses, count=len(responses))
|
|
|
|
|
|
@router.post(
|
|
"/api/v1/audio-processing-templates",
|
|
response_model=AudioProcessingTemplateResponse,
|
|
tags=["Audio Processing Templates"],
|
|
status_code=201,
|
|
)
|
|
async def create_audio_processing_template(
|
|
data: AudioProcessingTemplateCreate,
|
|
_auth: AuthRequired,
|
|
store: AudioProcessingTemplateStore = Depends(get_audio_processing_template_store),
|
|
):
|
|
"""Create a new audio processing template."""
|
|
try:
|
|
filters = [FilterInstance(f.filter_id, f.options) for f in data.filters]
|
|
template = store.create_template(
|
|
name=data.name,
|
|
filters=filters,
|
|
description=data.description,
|
|
tags=data.tags,
|
|
icon=data.icon,
|
|
icon_color=data.icon_color,
|
|
)
|
|
fire_entity_event("audio_processing_template", "created", template.id)
|
|
return _apt_to_response(template)
|
|
except EntityNotFoundError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Failed to create audio processing template: %s", e, exc_info=True)
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
|
|
@router.get(
|
|
"/api/v1/audio-processing-templates/{template_id}",
|
|
response_model=AudioProcessingTemplateResponse,
|
|
tags=["Audio Processing Templates"],
|
|
)
|
|
async def get_audio_processing_template(
|
|
template_id: str,
|
|
_auth: AuthRequired,
|
|
store: AudioProcessingTemplateStore = Depends(get_audio_processing_template_store),
|
|
):
|
|
"""Get audio processing template by ID."""
|
|
try:
|
|
template = store.get_template(template_id)
|
|
return _apt_to_response(template)
|
|
except ValueError:
|
|
raise HTTPException(
|
|
status_code=404, detail=f"Audio processing template {template_id} not found"
|
|
)
|
|
|
|
|
|
@router.put(
|
|
"/api/v1/audio-processing-templates/{template_id}",
|
|
response_model=AudioProcessingTemplateResponse,
|
|
tags=["Audio Processing Templates"],
|
|
)
|
|
async def update_audio_processing_template(
|
|
template_id: str,
|
|
data: AudioProcessingTemplateUpdate,
|
|
_auth: AuthRequired,
|
|
store: AudioProcessingTemplateStore = Depends(get_audio_processing_template_store),
|
|
):
|
|
"""Update an audio processing template."""
|
|
try:
|
|
filters = (
|
|
[FilterInstance(f.filter_id, f.options) for f in data.filters]
|
|
if data.filters is not None
|
|
else None
|
|
)
|
|
template = store.update_template(
|
|
template_id=template_id,
|
|
name=data.name,
|
|
filters=filters,
|
|
description=data.description,
|
|
tags=data.tags,
|
|
icon=data.icon,
|
|
icon_color=data.icon_color,
|
|
)
|
|
fire_entity_event("audio_processing_template", "updated", template_id)
|
|
# Hot-update: rebuild filter pipelines for running streams using this template
|
|
try:
|
|
pm = get_processor_manager()
|
|
pm.refresh_audio_filter_pipelines(template_id)
|
|
except Exception as exc:
|
|
logger.warning("Hot-update of audio filter pipelines failed: %s", exc)
|
|
return _apt_to_response(template)
|
|
except EntityNotFoundError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Failed to update audio processing template: %s", e, exc_info=True)
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|
|
|
|
|
|
@router.delete(
|
|
"/api/v1/audio-processing-templates/{template_id}",
|
|
status_code=204,
|
|
tags=["Audio Processing Templates"],
|
|
)
|
|
async def delete_audio_processing_template(
|
|
template_id: str,
|
|
_auth: AuthRequired,
|
|
store: AudioProcessingTemplateStore = Depends(get_audio_processing_template_store),
|
|
):
|
|
"""Delete an audio processing template."""
|
|
try:
|
|
# Check for references from audio sources
|
|
audio_source_store = get_audio_source_store()
|
|
refs = audio_source_store.get_sources_referencing_template(template_id)
|
|
if refs:
|
|
names = ", ".join(r.name for r in refs)
|
|
raise ValueError(f"Template is in use by audio source(s): {names}")
|
|
store.delete_template(template_id)
|
|
fire_entity_event("audio_processing_template", "deleted", template_id)
|
|
# Hot-update: rebuild filter pipelines for running streams that used this template
|
|
try:
|
|
pm = get_processor_manager()
|
|
pm.refresh_audio_filter_pipelines(template_id)
|
|
except Exception as exc:
|
|
logger.warning("Hot-update of audio filter pipelines after delete failed: %s", exc)
|
|
except HTTPException:
|
|
raise
|
|
except EntityNotFoundError as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
except Exception as e:
|
|
logger.error("Failed to delete audio processing template: %s", e, exc_info=True)
|
|
raise HTTPException(status_code=500, detail="Internal server error")
|