diff --git a/.env.example b/.env.example index 7e8f50ba..58af29c1 100644 --- a/.env.example +++ b/.env.example @@ -99,6 +99,6 @@ RABBITMQ_DEFAULT_PASS=aohohunuhouhuhhoahothonseuhaoensuthoaentsuhha # v5 stuff, does absolutely nothing on master (aka: you can delete this) EXTRA_CLAIMS='{"permissions": ["core.read"], "verified": false}' -FIRST_USER_CLAIMS='{"permissions": ["users.read", "users.write", "apikeys.read", "apikeys.write", "users.delete", "core.read", "core.write"], "verified": true}' +FIRST_USER_CLAIMS='{"permissions": ["users.read", "users.write", "apikeys.read", "apikeys.write", "users.delete", "core.read", "core.write", "scanner.trigger"], "verified": true}' GUEST_CLAIMS='{"permissions": ["core.read"]}' PROTECTED_CLAIMS="permissions,verified" diff --git a/scanner/scanner/__init__.py b/scanner/scanner/__init__.py index eaa412b2..821e6b7e 100644 --- a/scanner/scanner/__init__.py +++ b/scanner/scanner/__init__.py @@ -1,64 +1,43 @@ import asyncio import logging from contextlib import asynccontextmanager -from typing import Annotated -import asyncpg -from fastapi import BackgroundTasks, FastAPI, Security +from fastapi import FastAPI -from .client import KyooClient -from .fsscan import Scanner -from .jwt import validate_bearer -from .providers.composite import CompositeProvider -from .providers.themoviedatabase import TheMovieDatabase -from .requests import RequestCreator, RequestProcessor +from scanner.client import KyooClient +from .database import get_db, init_pool +from scanner.fsscan import Scanner +from scanner.providers.composite import CompositeProvider +from scanner.providers.themoviedatabase import TheMovieDatabase +from scanner.requests import RequestCreator, RequestProcessor logging.basicConfig(level=logging.INFO) logging.getLogger("watchfiles").setLevel(logging.WARNING) logging.getLogger("rebulk").setLevel(logging.WARNING) - -scanner: Scanner - - @asynccontextmanager -async def lifetime(): +async def lifespan(_): + print("starting lifetime") async with ( - await asyncpg.create_pool() as pool, - create_request_processor(pool) as processor, - create_scanner(pool) as (scan, is_master), + init_pool(), + get_db() as db, + KyooClient() as client, + TheMovieDatabase() as tmdb, ): - global scanner - scanner = scan - + processor = RequestProcessor(db, client, CompositeProvider(tmdb)) await processor.listen_for_requests() - if is_master: - _ = await asyncio.gather( - scanner.scan(remove_deleted=True), - scanner.monitor(), - ) - yield - - -@asynccontextmanager -async def create_request_processor(pool: asyncpg.Pool): - async with ( - pool.acquire() as db, - KyooClient() as client, - TheMovieDatabase() as themoviedb, - ): - yield RequestProcessor(db, client, CompositeProvider(themoviedb)) - - -@asynccontextmanager -async def create_scanner(pool: asyncpg.Pool): - async with ( - pool.acquire() as db, - KyooClient() as client, - ): - # there's no way someone else used the same id, right? - is_master: bool = await db.fetchval("select pg_try_advisory_lock(198347)") - yield (Scanner(client, RequestCreator(db)), is_master) + async with ( + get_db() as db, + KyooClient() as client, + ): + scanner = Scanner(client, RequestCreator(db)) + # there's no way someone else used the same id, right? + is_master = await db.fetchval("select pg_try_advisory_lock(198347)") + if is_master: + print("this is master") + _ = await asyncio.create_task(scanner.scan(remove_deleted=True)) + _ = await asyncio.create_task(scanner.monitor()) + yield app = FastAPI( @@ -66,20 +45,5 @@ app = FastAPI( description="API to control the long running scanner or interacting with external databases (themoviedb, tvdb...)\n\n" + "Most of those APIs are for admins only.", root_path="/scanner", - lifetime=lifetime, + lifespan=lifespan, ) - - -@app.put( - "/scan", - status_code=204, - response_description="Scan started.", -) -async def trigger_scan( - tasks: BackgroundTasks, - _: Annotated[None, Security(validate_bearer, scopes=["scanner.trigger"])], -): - """ - Trigger a full scan of the filesystem, trying to find new videos & deleting old ones. - """ - tasks.add_task(scanner.scan) diff --git a/scanner/scanner/database.py b/scanner/scanner/database.py new file mode 100644 index 00000000..f4f47b2d --- /dev/null +++ b/scanner/scanner/database.py @@ -0,0 +1,20 @@ +from contextlib import asynccontextmanager +from typing import cast + +from asyncpg import Connection, Pool, create_pool + +pool: Pool + + +@asynccontextmanager +async def init_pool(): + async with await create_pool() as p: + global pool + pool = p + yield + + +@asynccontextmanager +async def get_db(): + async with pool.acquire() as db: + yield cast(Connection, db) diff --git a/scanner/scanner/fsscan.py b/scanner/scanner/fsscan.py index 362b48f2..8818b686 100644 --- a/scanner/scanner/fsscan.py +++ b/scanner/scanner/fsscan.py @@ -3,7 +3,9 @@ import re from logging import getLogger from mimetypes import guess_file_type from os.path import dirname, exists, isdir, join +from typing import Annotated +from fastapi import Depends from watchfiles import Change, awatch from .client import KyooClient @@ -16,7 +18,11 @@ logger = getLogger(__name__) class Scanner: - def __init__(self, client: KyooClient, requests: RequestCreator): + def __init__( + self, + client: Annotated[KyooClient, Depends], + requests: Annotated[RequestCreator, Depends], + ): self._client = client self._requests = requests self._info: VideoInfo = None # type: ignore diff --git a/scanner/scanner/providers/composite.py b/scanner/scanner/providers/composite.py index dc52af8d..2d79761b 100644 --- a/scanner/scanner/providers/composite.py +++ b/scanner/scanner/providers/composite.py @@ -1,14 +1,19 @@ -from typing import override +from typing import Annotated, override +from fastapi import Depends from langcodes import Language from ..models.movie import Movie, SearchMovie from ..models.serie import SearchSerie, Serie from .provider import Provider +from .themoviedatabase import TheMovieDatabase class CompositeProvider(Provider): - def __init__(self, themoviedb: Provider): + def __init__( + self, + themoviedb: Annotated[TheMovieDatabase, Depends], + ): self._tvdb: Provider = None # type: ignore self._themoviedb = themoviedb diff --git a/scanner/scanner/requests.py b/scanner/scanner/requests.py index d8973f6d..7b794817 100644 --- a/scanner/scanner/requests.py +++ b/scanner/scanner/requests.py @@ -1,12 +1,14 @@ from __future__ import annotations from logging import getLogger -from typing import Literal +from typing import Annotated, Literal from asyncpg import Connection +from fastapi import Depends from pydantic import Field from .client import KyooClient +from .database import get_db from .models.videos import Guess, Resource from .providers.composite import CompositeProvider from .utils import Model @@ -28,7 +30,10 @@ class Request(Model, extra="allow"): class RequestCreator: - def __init__(self, database: Connection): + def __init__( + self, + database: Annotated[Connection, Depends(get_db)], + ): self._database = database async def enqueue(self, requests: list[Request]): @@ -48,9 +53,9 @@ class RequestCreator: class RequestProcessor: def __init__( self, - database: Connection, - client: KyooClient, - providers: CompositeProvider, + database: Annotated[Connection, Depends(get_db)], + client: Annotated[KyooClient, Depends], + providers: Annotated[CompositeProvider, Depends], ): self._database = database self._client = client diff --git a/scanner/scanner/routes.py b/scanner/scanner/routes.py new file mode 100644 index 00000000..58ab3b42 --- /dev/null +++ b/scanner/scanner/routes.py @@ -0,0 +1,23 @@ +from typing import Annotated + +from fastapi import BackgroundTasks, Depends, Security + +from scanner import app +from scanner.fsscan import Scanner +from scanner.jwt import validate_bearer + + +@app.put( + "/scan", + status_code=204, + response_description="Scan started.", +) +async def trigger_scan( + tasks: BackgroundTasks, + scanner: Annotated[Scanner, Depends], + _: Annotated[None, Security(validate_bearer, scopes=["scanner.trigger"])], +): + """ + Trigger a full scan of the filesystem, trying to find new videos & deleting old ones. + """ + tasks.add_task(scanner.scan)