"""Scheduler API endpoints for Aniworld. This module provides endpoints for managing scheduled tasks such as automatic anime library rescans. """ import logging from typing import Any, Dict, Optional from fastapi import APIRouter, Depends, HTTPException, status from src.server.models.config import SchedulerConfig from src.server.services.config_service import ConfigServiceError, get_config_service from src.server.services.scheduler_service import get_scheduler_service from src.server.utils.dependencies import require_auth logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/scheduler", tags=["scheduler"]) def _build_response(config: SchedulerConfig) -> Dict[str, Any]: """Build a standardised GET/POST response combining config + runtime status.""" scheduler_service = get_scheduler_service() runtime = scheduler_service.get_status() return { "success": True, "config": { "enabled": config.enabled, "interval_minutes": config.interval_minutes, "schedule_time": config.schedule_time, "schedule_days": config.schedule_days, "auto_download_after_rescan": config.auto_download_after_rescan, }, "status": { "is_running": runtime.get("is_running", False), "next_run": runtime.get("next_run"), "last_run": runtime.get("last_run"), "scan_in_progress": runtime.get("scan_in_progress", False), }, } @router.get("/config") def get_scheduler_config( auth: Optional[dict] = Depends(require_auth), ) -> Dict[str, Any]: """Get current scheduler configuration along with runtime status. Returns: Combined config and status response. Raises: HTTPException: 500 if configuration cannot be loaded. """ try: config_service = get_config_service() app_config = config_service.load_config() return _build_response(app_config.scheduler) except ConfigServiceError as exc: logger.error("Failed to load scheduler config: %s", exc) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to load scheduler configuration: {exc}", ) from exc @router.post("/config") def update_scheduler_config( scheduler_config: SchedulerConfig, auth: dict = Depends(require_auth), ) -> Dict[str, Any]: """Update scheduler configuration and apply changes immediately. Accepts the full SchedulerConfig body; any fields not supplied default to their model defaults (backward compatible). Returns: Combined config and status response reflecting the saved config. Raises: HTTPException: 422 on validation errors (handled by FastAPI/Pydantic), 500 on save or scheduler failure. """ try: config_service = get_config_service() app_config = config_service.load_config() app_config.scheduler = scheduler_config config_service.save_config(app_config) logger.info( "Scheduler config updated by %s: time=%s days=%s auto_dl=%s", auth.get("username", "unknown"), scheduler_config.schedule_time, scheduler_config.schedule_days, scheduler_config.auto_download_after_rescan, ) # Apply changes to the running scheduler without restart try: sched_svc = get_scheduler_service() sched_svc.reload_config(scheduler_config) except Exception as sched_exc: # pylint: disable=broad-exception-caught logger.error("Scheduler reload after config update failed: %s", sched_exc) # Config was saved — don't fail the request, just warn return _build_response(scheduler_config) except ConfigServiceError as exc: logger.error("Failed to update scheduler config: %s", exc) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to update scheduler configuration: {exc}", ) from exc @router.post("/trigger-rescan", response_model=Dict[str, str]) async def trigger_rescan(auth: dict = Depends(require_auth)) -> Dict[str, str]: """Manually trigger a library rescan (and auto-download if configured). Args: auth: Authentication token (required) Returns: Dict with success message Raises: HTTPException: If rescan cannot be triggered """ try: from src.server.utils.dependencies import get_series_app # noqa: PLC0415 series_app = get_series_app() if not series_app: raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="SeriesApp not initialized", ) logger.info( "Manual rescan triggered by %s", auth.get("username", "unknown") ) from src.server.api.anime import trigger_rescan as do_rescan # noqa: PLC0415 return await do_rescan() except HTTPException: raise except Exception as exc: logger.exception("Failed to trigger manual rescan") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to trigger rescan: {exc}", ) from exc