Add search endpoint on the scanner

This commit is contained in:
Zoe Roux 2026-03-22 10:42:22 +01:00
parent dcf7b4e794
commit 49961c341e
No known key found for this signature in database
7 changed files with 108 additions and 9 deletions

View File

@ -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", "users.delete", "apikeys.read", "apikeys.write", "core.read", "core.write", "core.play", "scanner.trigger", "scanner.guess"], "verified": true}'
FIRST_USER_CLAIMS='{"permissions": ["users.read", "users.write", "users.delete", "apikeys.read", "apikeys.write", "core.read", "core.write", "core.play", "scanner.trigger", "scanner.guess", "scanner.search"], "verified": true}'
# Guest (meaning unlogged in users) can be:
# unauthorized (they need to connect before doing anything)

View File

@ -103,7 +103,7 @@ kyoo:
# auth settings
auth:
firstUserClaims: '{"permissions": ["users.read", "users.write", "apikeys.read", "apikeys.write", "users.delete", "core.read", "core.write", "core.play", "scanner.trigger", "scanner.guess"], "verified": true}'
firstUserClaims: '{"permissions": ["users.read", "users.write", "apikeys.read", "apikeys.write", "users.delete", "core.read", "core.write", "core.play", "scanner.trigger", "scanner.guess", "scanner.search"], "verified": true}'
guestClaims: '{"permissions": ["core.read"], "verified": true}'
extraClaims: '{"permissions": ["core.read", "core.play"], "verified": false}'
protectedClaims: "permissions,verified"

View File

@ -18,7 +18,7 @@ from .routers.routes import router
@asynccontextmanager
async def lifespan(_):
async def lifespan(app: FastAPI):
async with (
init_pool() as pool,
get_db() as db,
@ -26,6 +26,7 @@ async def lifespan(_):
TVDB() as tvdb,
TheMovieDatabase() as tmdb,
):
app.state.provider = CompositeProvider(tvdb, tmdb)
# there's no way someone else used the same id, right?
is_master = await db.fetchval("select pg_try_advisory_lock(198347)")
is_http = not is_master and await db.fetchval(
@ -39,7 +40,7 @@ async def lifespan(_):
processor = RequestProcessor(
pool,
client,
CompositeProvider(tvdb, tmdb),
app.state.provider,
)
scanner = FsScanner(client, RequestCreator(db))
tasks = create_task(

View File

@ -111,7 +111,7 @@ class TheMovieDatabase(Provider):
params={
"query": title,
"year": year,
"languages": [str(x) for x in language],
"language": next((str(x) for x in language), None),
},
)
)["results"]
@ -245,7 +245,7 @@ class TheMovieDatabase(Provider):
params={
"query": title,
"year": year,
"languages": [str(x) for x in language],
"language": next((str(x) for x in language), None),
},
)
)["results"]

View File

@ -172,8 +172,22 @@ class TVDB(Provider):
return [
SearchSerie(
slug=x["slug"],
name=x["name"],
description=x.get("overview"),
name=next(
(
x["translations"][lang.to_alpha3()]
for lang in language
if "translations" in x and lang.to_alpha3() in x["translations"]
),
x["name"],
),
description=next(
(
x["overviews"][lang.to_alpha3()]
for lang in language
if "overviews" in x and lang.to_alpha3() in x["overviews"]
),
x.get("overview"),
),
start_air=datetime.strptime(x["first_air_time"], "%Y-%m-%d").date()
if x.get("first_air_time")
else None,

View File

@ -0,0 +1,40 @@
from typing import Annotated
from fastapi import Header, Request
from langcodes import Language
from ..providers.composite import CompositeProvider
def get_provider(request: Request) -> CompositeProvider:
return request.app.state.provider
def get_preferred_languages(
accept_language: Annotated[str | None, Header()] = None,
) -> list[Language]:
if not accept_language:
return []
ret: list[tuple[float, int, Language]] = []
for index, item in enumerate(accept_language.split(",")):
part = item.strip()
if not part:
continue
tag, *params = [x.strip() for x in part.split(";")]
if tag == "*":
continue
try:
q = next((float(x[2:]) for x in params if x.startswith("q=")), 1)
if q <= 0:
continue
language = Language.get(tag)
ret.append((q, index, language))
except Exception:
continue
ret.sort(key=lambda x: (-x[0], x[1]))
return [x for _q, _i, x in ret] + [Language.get("en")]

View File

@ -5,8 +5,14 @@ from fastapi import APIRouter, BackgroundTasks, Depends, Security
from ..fsscan import create_scanner
from ..identifiers.identify import identify
from ..jwt import validate_bearer
from ..models.movie import SearchMovie
from ..models.request import RequestRet
from ..models.serie import SearchSerie
from ..models.videos import Video
from ..providers.composite import CompositeProvider
from ..status import StatusService
from ..utils import Language
from .dependencies import get_preferred_languages, get_provider
router = APIRouter()
@ -52,9 +58,47 @@ async def trigger_scan(
async def get_guess(
path: str,
_: Annotated[None, Security(validate_bearer, scopes=["scanner.guess"])],
):
) -> Video:
"""
Identify a video path and return a serie/movie guess.
"""
return await identify(path)
@router.get(
"/movies",
status_code=200,
response_description="Found movies",
)
async def get_movies(
provider: Annotated[CompositeProvider, Depends(get_provider)],
language: Annotated[list[Language], Depends(get_preferred_languages)],
_: Annotated[None, Security(validate_bearer, scopes=["scanner.search"])],
query: str,
year: int | None = None,
) -> list[SearchMovie]:
"""
Search for a movie
"""
return await provider.search_movies(query, year=year, language=language)
@router.get(
"/series",
status_code=200,
response_description="Found series",
)
async def get_series(
provider: Annotated[CompositeProvider, Depends(get_provider)],
language: Annotated[list[Language], Depends(get_preferred_languages)],
_: Annotated[None, Security(validate_bearer, scopes=["scanner.search"])],
query: str,
year: int | None = None,
) -> list[SearchSerie]:
"""
Search for a serie
"""
return await provider.search_series(query, year=year, language=language)