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:
2026-02-05 03:44:18 +03:00
parent a6cb420eef
commit 1a1cfbaafb
7 changed files with 249 additions and 3 deletions

View File

@@ -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"]

View File

@@ -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.

View File

@@ -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 []