from uuid import UUID from fastapi import APIRouter, HTTPException from pydantic import BaseModel from typing import Any import lightsync.main as state router = APIRouter(tags=["timeline"]) class BlockCreate(BaseModel): id: str # UUID as string from frontend crypto.randomUUID() timestamp: float duration: float = 4.0 animation: str | None = None params: dict[str, Any] = {} class BlockUpdate(BaseModel): timestamp: float | None = None duration: float | None = None animation: str | None = None params: dict[str, Any] | None = None @router.post("/shows/{show_id}/tracks/{device_id}/blocks", status_code=201) async def create_block(show_id: str, device_id: str, block: BlockCreate): show = await state.show_store.load(show_id) if show is None: raise HTTPException(404, "Show not found") track = next((t for t in show.tracks if str(t.device_id) == device_id), None) if track is None: # Auto-create track for this device from lightsync.models.show import TrackModel, CueModel cue = CueModel( id=UUID(block.id), timestamp=block.timestamp, duration=block.duration, animation=block.animation, params=block.params ) track = TrackModel(device_id=UUID(device_id), cues=[cue]) show.tracks.append(track) else: from lightsync.models.show import CueModel cue = CueModel( id=UUID(block.id), timestamp=block.timestamp, duration=block.duration, animation=block.animation, params=block.params ) track.cues.append(cue) await state.show_store.save(show) return {"status": "ok"} @router.patch("/shows/{show_id}/tracks/{device_id}/blocks/{block_id}") async def update_block(show_id: str, device_id: str, block_id: str, update: BlockUpdate): show = await state.show_store.load(show_id) if show is None: raise HTTPException(404, "Show not found") track = next((t for t in show.tracks if str(t.device_id) == device_id), None) if track is None: raise HTTPException(404, "Track not found") cue = next((c for c in track.cues if str(c.id) == block_id), None) if cue is None: raise HTTPException(404, "Block not found") if update.timestamp is not None: cue.timestamp = update.timestamp if update.duration is not None: cue.duration = update.duration if update.animation is not None: cue.animation = update.animation if update.params is not None: cue.params = update.params await state.show_store.save(show) return {"status": "ok"} @router.delete("/shows/{show_id}/tracks/{device_id}/blocks/{block_id}") async def delete_block(show_id: str, device_id: str, block_id: str): show = await state.show_store.load(show_id) if show is None: raise HTTPException(404, "Show not found") track = next((t for t in show.tracks if str(t.device_id) == device_id), None) if track is None: raise HTTPException(404, "Track not found") track.cues = [c for c in track.cues if str(c.id) != block_id] await state.show_store.save(show) return {"status": "ok"}