Add callbacks support for all media actions
- Add CallbackConfig model for callback scripts - Add callbacks section to config for optional command execution - Add turn_on/turn_off/toggle endpoints (callback-only) - Add callbacks for all media actions (play, pause, stop, next, previous, volume, mute, seek) - Update README with callbacks documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
"""API route modules."""
|
||||
|
||||
from .audio import router as audio_router
|
||||
from .health import router as health_router
|
||||
from .media import router as media_router
|
||||
from .scripts import router as scripts_router
|
||||
|
||||
__all__ = ["health_router", "media_router", "scripts_router"]
|
||||
__all__ = ["audio_router", "health_router", "media_router", "scripts_router"]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Media control API endpoints."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
|
||||
@@ -17,6 +18,33 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/media", tags=["media"])
|
||||
|
||||
|
||||
async def _run_callback(callback_name: str) -> None:
|
||||
"""Run a callback if configured. Failures are logged but don't raise."""
|
||||
if not settings.callbacks or callback_name not in settings.callbacks:
|
||||
return
|
||||
|
||||
from .scripts import _run_script
|
||||
|
||||
callback = settings.callbacks[callback_name]
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(
|
||||
None,
|
||||
lambda: _run_script(
|
||||
command=callback.command,
|
||||
timeout=callback.timeout,
|
||||
shell=callback.shell,
|
||||
working_dir=callback.working_dir,
|
||||
),
|
||||
)
|
||||
if result["exit_code"] != 0:
|
||||
logger.warning(
|
||||
"Callback %s failed with exit code %s: %s",
|
||||
callback_name,
|
||||
result["exit_code"],
|
||||
result["stderr"],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/status", response_model=MediaStatus)
|
||||
async def get_media_status(_: str = Depends(verify_token)) -> MediaStatus:
|
||||
"""Get current media playback status.
|
||||
@@ -42,6 +70,7 @@ async def play(_: str = Depends(verify_token)) -> dict:
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Failed to start playback - no active media session",
|
||||
)
|
||||
await _run_callback("on_play")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@@ -59,6 +88,7 @@ async def pause(_: str = Depends(verify_token)) -> dict:
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Failed to pause - no active media session",
|
||||
)
|
||||
await _run_callback("on_pause")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@@ -76,6 +106,7 @@ async def stop(_: str = Depends(verify_token)) -> dict:
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Failed to stop - no active media session",
|
||||
)
|
||||
await _run_callback("on_stop")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@@ -93,6 +124,7 @@ async def next_track(_: str = Depends(verify_token)) -> dict:
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Failed to skip - no active media session",
|
||||
)
|
||||
await _run_callback("on_next")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@@ -110,6 +142,7 @@ async def previous_track(_: str = Depends(verify_token)) -> dict:
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Failed to go back - no active media session",
|
||||
)
|
||||
await _run_callback("on_previous")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@@ -132,6 +165,7 @@ async def set_volume(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Failed to set volume",
|
||||
)
|
||||
await _run_callback("on_volume")
|
||||
return {"success": True, "volume": request.volume}
|
||||
|
||||
|
||||
@@ -144,6 +178,7 @@ async def toggle_mute(_: str = Depends(verify_token)) -> dict:
|
||||
"""
|
||||
controller = get_media_controller()
|
||||
muted = await controller.toggle_mute()
|
||||
await _run_callback("on_mute")
|
||||
return {"success": True, "muted": muted}
|
||||
|
||||
|
||||
@@ -164,9 +199,43 @@ async def seek(request: SeekRequest, _: str = Depends(verify_token)) -> dict:
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail="Failed to seek - no active media session or seek not supported",
|
||||
)
|
||||
await _run_callback("on_seek")
|
||||
return {"success": True, "position": request.position}
|
||||
|
||||
|
||||
@router.post("/turn_on")
|
||||
async def turn_on(_: str = Depends(verify_token)) -> dict:
|
||||
"""Execute turn on callback if configured.
|
||||
|
||||
Returns:
|
||||
Success status
|
||||
"""
|
||||
await _run_callback("on_turn_on")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.post("/turn_off")
|
||||
async def turn_off(_: str = Depends(verify_token)) -> dict:
|
||||
"""Execute turn off callback if configured.
|
||||
|
||||
Returns:
|
||||
Success status
|
||||
"""
|
||||
await _run_callback("on_turn_off")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.post("/toggle")
|
||||
async def toggle(_: str = Depends(verify_token)) -> dict:
|
||||
"""Execute toggle callback if configured.
|
||||
|
||||
Returns:
|
||||
Success status
|
||||
"""
|
||||
await _run_callback("on_toggle")
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@router.get("/artwork")
|
||||
async def get_artwork(_: str = Depends(verify_token_or_query)) -> Response:
|
||||
"""Get the current album artwork.
|
||||
|
||||
@@ -82,7 +82,6 @@ async def execute_script(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Script '{script_name}' not found. Use /api/scripts/list to see available scripts.",
|
||||
)
|
||||
|
||||
script_config = settings.scripts[script_name]
|
||||
args = request.args if request else []
|
||||
|
||||
|
||||
Reference in New Issue
Block a user