From b39fa4262dcd086e4146d9c1d1704d72ac81cdbb Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Mon, 1 Dec 2025 19:45:36 +0100 Subject: [PATCH] Add status api to get scanner's status --- .env.example | 2 +- scanner/scanner/models/request.py | 13 ++++++++++ scanner/scanner/routers/health.py | 16 ++++++++++++ scanner/scanner/routers/routes.py | 43 ++++++++++++------------------- scanner/scanner/status.py | 41 +++++++++++++++++++++++++++++ 5 files changed, 88 insertions(+), 27 deletions(-) create mode 100644 scanner/scanner/routers/health.py create mode 100644 scanner/scanner/status.py diff --git a/.env.example b/.env.example index e54ceb79..97713502 100644 --- a/.env.example +++ b/.env.example @@ -38,7 +38,7 @@ PUBLIC_URL=http://localhost:8901 # Set `verified` to true if you don't wanna manually verify users. EXTRA_CLAIMS='{"permissions": ["core.read", "core.play"], "verified": false}' # This is the permissions of the first user (aka the first user is admin) -FIRST_USER_CLAIMS='{"permissions": ["users.read", "users.write", "apikeys.read", "apikeys.write", "users.delete", "core.read", "core.write", "core.play", "scanner.trigger"], "verified": true}' +FIRST_USER_CLAIMS='{"permissions": ["users.read", "users.write", "users.delete", "apikeys.read", "apikeys.write", "core.read", "core.write", "core.play", "scanner.trigger"], "verified": true}' # Guest (meaning unlogged in users) can be: # unauthorized (they need to connect before doing anything) diff --git a/scanner/scanner/models/request.py b/scanner/scanner/models/request.py index 55b5cb0c..998c55a5 100644 --- a/scanner/scanner/models/request.py +++ b/scanner/scanner/models/request.py @@ -1,4 +1,5 @@ from __future__ import annotations +from datetime import datetime from typing import Literal from pydantic import Field @@ -18,3 +19,15 @@ class Request(Model, extra="allow"): class Video(Model): id: str episodes: list[Guess.Episode] + +class RequestRet(Model): + id: str + kind: Literal["episode", "movie"] + title: str + year: int | None + status: Literal[ + "pending", + "running", + "failed", + ] + started_at: datetime | None diff --git a/scanner/scanner/routers/health.py b/scanner/scanner/routers/health.py new file mode 100644 index 00000000..07814e47 --- /dev/null +++ b/scanner/scanner/routers/health.py @@ -0,0 +1,16 @@ +from fastapi import APIRouter + +router = APIRouter() + + +@router.get("/health") +def get_health(): + return {"status": "healthy"} + + +@router.get("/ready") +def get_ready(): + # child spans (`select 1` & db connection reset) was still logged, + # since i don't really wanna deal with it, let's just do that. + return {"status": "healthy"} + diff --git a/scanner/scanner/routers/routes.py b/scanner/scanner/routers/routes.py index 37fbaee5..07d02f53 100644 --- a/scanner/scanner/routers/routes.py +++ b/scanner/scanner/routers/routes.py @@ -1,9 +1,9 @@ -from typing import Annotated +from typing import Annotated, Literal -from asyncpg import Connection -from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Security +from fastapi import APIRouter, BackgroundTasks, Depends, Security -from scanner.database import get_db_fapi +from scanner.models.request import RequestRet +from scanner.status import StatusService from ..fsscan import create_scanner from ..jwt import validate_bearer @@ -11,6 +11,19 @@ from ..jwt import validate_bearer router = APIRouter() +@router.get("/scan") +async def get_scan_status( + svc: Annotated[StatusService, Depends(StatusService.create)], + _: Annotated[None, Security(validate_bearer, scopes=["scanner.trigger"])], + status: Literal["pending", "running", "failed"] | None = None, +) -> list[RequestRet]: + """ + Get scan status, know what tasks are running, pending or failed. + """ + + return await svc.list_requests(status=status) + + @router.put( "/scan", status_code=204, @@ -29,25 +42,3 @@ async def trigger_scan( await scanner.scan() tasks.add_task(run) - - -@router.get("/health") -def get_health(): - return {"status": "healthy"} - - -@router.get("/ready") -def get_ready(): - # child spans (`select 1` & db connection reset) was still logged, - # since i don't really wanna deal with it, let's just do that. - return {"status": "healthy"} - - -# async def get_ready(db: Annotated[Connection, Depends(get_db_fapi)]): -# try: -# _ = await db.execute("select 1") -# return {"status": "healthy", "database": "healthy"} -# except Exception as e: -# raise HTTPException( -# status_code=500, detail={"status": "unhealthy", "database": str(e)} -# ) diff --git a/scanner/scanner/status.py b/scanner/scanner/status.py new file mode 100644 index 00000000..ed0ecb5d --- /dev/null +++ b/scanner/scanner/status.py @@ -0,0 +1,41 @@ +from typing import Literal + +from asyncpg import Connection +from pydantic import TypeAdapter + +from scanner.database import get_db + +from .models.request import RequestRet + + +class StatusService: + def __init__(self, database: Connection): + self._database = database + + @classmethod + async def create(cls): + async with get_db() as db: + yield StatusService(db) + + async def list_requests( + self, *, status: Literal["pending", "running", "failed"] | None = None + ) -> list[RequestRet]: + ret = await self._database.fetch( + f""" + select + pk::text as id, + kind, + title, + year, + status, + started_at + from + scanner.requests + order by + started_at, + pk + {"where status = $1" if status is not None else ""} + """, + *([status] if status is not None else []), + ) + return TypeAdapter(list[RequestRet]).validate_python(ret)