feat: add typed qBittorrent scheduled polling

This commit is contained in:
Developer
2026-07-14 15:22:34 +00:00
parent eac9b5d33d
commit a9488af0b4
22 changed files with 2036 additions and 56 deletions
@@ -0,0 +1,118 @@
"""Endpoints for typed scheduled-action status and history."""
from __future__ import annotations
import time
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query, status
from media_library_viewer_api.dependencies import get_settings_store
from media_library_viewer_api.models.scheduler import ( # type: ignore[reportMissingImports]
SchedulerManualRunResponse,
SchedulerRun,
SchedulerRunsResponse,
SchedulerSamplesResponse,
SchedulerStatus,
)
from media_library_viewer_api.services.qbittorrent_store import QbittorrentSampleStore
from media_library_viewer_api.services.scheduler import ( # type: ignore[reportMissingImports]
SchedulerBusyError,
get_scheduler,
)
from media_library_viewer_api.services.scheduler_actions import (
QBITTORRENT_SPEED_ACTION, # type: ignore[reportMissingImports]
)
from media_library_viewer_api.services.scheduler_store import SchedulerRunStore # type: ignore[reportMissingImports]
from media_library_viewer_api.services.settings_store import SettingsStore
router = APIRouter(prefix="/api/scheduler", tags=["scheduler"])
def _safe_int(value: Any, default: int = 0) -> int:
try:
return int(value)
except (TypeError, ValueError):
return default
def _require_qbittorrent(service_id: str, store: SettingsStore) -> dict[str, Any]:
service = store.get_service(service_id)
if not service or service.get("service_type") != "qbittorrent":
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="qBittorrent service not found")
return service
@router.get("/services/{service_id}/status", response_model=SchedulerStatus)
def get_scheduler_status(
service_id: str,
store: SettingsStore = Depends(get_settings_store),
) -> SchedulerStatus:
_require_qbittorrent(service_id, store)
try:
return SchedulerStatus(**get_scheduler().status(service_id, store))
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
@router.get("/services/{service_id}/runs", response_model=SchedulerRunsResponse)
def get_scheduler_runs(
service_id: str,
status_filter: str | None = Query(default=None, alias="status"),
trigger: str | None = None,
limit: int = Query(default=50, ge=1, le=100),
offset: int = Query(default=0, ge=0),
store: SettingsStore = Depends(get_settings_store),
) -> SchedulerRunsResponse:
_require_qbittorrent(service_id, store)
items, total = SchedulerRunStore().list_runs(
service_id,
QBITTORRENT_SPEED_ACTION,
status=status_filter,
trigger=trigger,
limit=limit,
offset=offset,
)
return SchedulerRunsResponse(
items=[SchedulerRun(**item) for item in items],
total=total,
limit=limit,
offset=offset,
)
@router.post("/services/{service_id}/run", response_model=SchedulerManualRunResponse)
def run_scheduler_action(
service_id: str,
store: SettingsStore = Depends(get_settings_store),
) -> SchedulerManualRunResponse:
_require_qbittorrent(service_id, store)
try:
result = get_scheduler().run_now(service_id, store)
except SchedulerBusyError as exc:
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
return SchedulerManualRunResponse(
run=SchedulerRun(**result["run"]),
status=SchedulerStatus(**result["status"]),
)
@router.get("/services/{service_id}/samples", response_model=SchedulerSamplesResponse)
def get_scheduler_samples(
service_id: str,
window_seconds: int = Query(default=1_800, ge=60, le=86_400),
store: SettingsStore = Depends(get_settings_store),
) -> SchedulerSamplesResponse:
_require_qbittorrent(service_id, store)
since_ts = _safe_int(time.time()) - window_seconds
samples = QbittorrentSampleStore().window(service_id, since_ts=since_ts)
return SchedulerSamplesResponse(
service_id=service_id,
window_seconds=window_seconds,
samples=samples,
)
__all__ = ["router"]