From 93bc58a2d01b38ea48e59c1ede2a596ef593ce2c Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Fri, 27 Mar 2026 17:46:55 +0100 Subject: [PATCH] Implement manual metadata refresh --- front/src/components/items/context-menus.tsx | 33 +++++---- front/src/ui/details/header.tsx | 23 +++--- front/src/ui/player/controls/tracks-menu.tsx | 2 +- scanner/scanner/client.py | 10 +++ scanner/scanner/routers/dependencies.py | 5 ++ scanner/scanner/routers/routes.py | 78 +++++++++++++++++++- 6 files changed, 122 insertions(+), 29 deletions(-) diff --git a/front/src/components/items/context-menus.tsx b/front/src/components/items/context-menus.tsx index e007c4a7..c102e7c6 100644 --- a/front/src/components/items/context-menus.tsx +++ b/front/src/components/items/context-menus.tsx @@ -1,11 +1,12 @@ // import Download from "@material-symbols/svg-400/rounded/download.svg"; +import Refresh from "@material-symbols/svg-400/rounded/autorenew.svg"; import Info from "@material-symbols/svg-400/rounded/info.svg"; import MoreVert from "@material-symbols/svg-400/rounded/more_vert.svg"; import MovieInfo from "@material-symbols/svg-400/rounded/movie_info.svg"; import type { ComponentProps } from "react"; import { useTranslation } from "react-i18next"; import { WatchStatusV } from "~/models"; -import { IconButton, Menu, tooltip } from "~/primitives"; +import { HR, IconButton, Menu, tooltip } from "~/primitives"; import { useAccount } from "~/providers/account-context"; import { useMutation } from "~/query"; import { cn } from "~/utils"; @@ -89,11 +90,11 @@ export const ItemContext = ({ invalidate: [kind, slug], }); - // const metadataRefreshMutation = useMutation({ - // method: "POST", - // path: [kind, slug, "refresh"], - // invalidate: null, - // }); + const metadataRefreshMutation = useMutation({ + method: "POST", + path: ["scanner", `${kind}s`, slug, "refresh"], + invalidate: null, + }); return ( )} - {/* {account?.isAdmin === true && ( */} - {/* <> */} - {/*
*/} - {/* metadataRefreshMutation.mutate()} */} - {/* /> */} - {/* */} - {/* )} */} + {account?.isAdmin === true && ( + <> +
+ metadataRefreshMutation.mutate()} + /> + + )}
); }; diff --git a/front/src/ui/details/header.tsx b/front/src/ui/details/header.tsx index f8cd56a7..4b07e629 100644 --- a/front/src/ui/details/header.tsx +++ b/front/src/ui/details/header.tsx @@ -1,3 +1,4 @@ +import Refresh from "@material-symbols/svg-400/rounded/autorenew.svg"; import BookmarkAdd from "@material-symbols/svg-400/rounded/bookmark_add.svg"; import Delete from "@material-symbols/svg-400/rounded/delete.svg"; import MoreHoriz from "@material-symbols/svg-400/rounded/more_horiz.svg"; @@ -83,11 +84,11 @@ const ButtonList = ({ invalidate: ["api", "shows"], }); - // const metadataRefreshMutation = useMutation({ - // method: "POST", - // path: [kind, slug, "refresh"], - // invalidate: null, - // }); + const metadataRefreshMutation = useMutation({ + method: "POST", + path: ["scanner", `${kind}s`, slug, "refresh"], + invalidate: null, + }); return ( @@ -163,6 +164,13 @@ const ButtonList = ({ href={`/${kind === "movie" ? "movies" : "series"}/${slug}/videos`} /> {kind !== "collection" &&
} + {kind !== "collection" && ( + metadataRefreshMutation.mutate()} + /> + )} - {/* metadataRefreshMutation.mutate()} */} - {/* /> */} )} diff --git a/front/src/ui/player/controls/tracks-menu.tsx b/front/src/ui/player/controls/tracks-menu.tsx index 50263c52..9ed05a90 100644 --- a/front/src/ui/player/controls/tracks-menu.tsx +++ b/front/src/ui/player/controls/tracks-menu.tsx @@ -126,7 +126,7 @@ export const VideoMenu = ({ key={x.id} label={getDisplayName({ title: x.label, language: x.language })} selected={x.selected} - onSelect={() => player.selectAudioTrack(x)} + onSelect={() => player.selectVideoTrack(x)} /> ))} diff --git a/scanner/scanner/client.py b/scanner/scanner/client.py index 7abed1ae..b72c6e30 100644 --- a/scanner/scanner/client.py +++ b/scanner/scanner/client.py @@ -97,6 +97,16 @@ class KyooClient(metaclass=Singleton): await self.raise_for_status(r) return Page[Show].model_validate(await r.json()) + async def get_movie(self, slug: str) -> Show: + async with self._client.get(f"movies/{slug}") as r: + await self.raise_for_status(r) + return Show.model_validate(await r.json()) + + async def get_serie(self, slug: str) -> Show: + async with self._client.get(f"series/{slug}") as r: + await self.raise_for_status(r) + return Show.model_validate(await r.json()) + async def link_videos( self, kind: Literal["movie", "serie"], diff --git a/scanner/scanner/routers/dependencies.py b/scanner/scanner/routers/dependencies.py index f78e3a54..344939e2 100644 --- a/scanner/scanner/routers/dependencies.py +++ b/scanner/scanner/routers/dependencies.py @@ -3,6 +3,7 @@ from typing import Annotated from fastapi import Header, Request from langcodes import Language +from ..client import KyooClient from ..database import get_db from ..providers.composite import CompositeProvider from ..requests import RequestCreator @@ -12,6 +13,10 @@ def get_provider(request: Request) -> CompositeProvider: return request.app.state.provider +def get_client(_request: Request) -> KyooClient: + return KyooClient() + + async def get_request_creator(): async with get_db() as db: yield RequestCreator(db) diff --git a/scanner/scanner/routers/routes.py b/scanner/scanner/routers/routes.py index 938b075a..060bb273 100644 --- a/scanner/scanner/routers/routes.py +++ b/scanner/scanner/routers/routes.py @@ -4,23 +4,33 @@ from fastapi import ( APIRouter, BackgroundTasks, Depends, - Request as HttpRequest, Security, ) +from fastapi import ( + Request as HttpRequest, +) +from ..client import KyooClient from ..fsscan import create_scanner from ..identifiers.identify import identify from ..jwt import validate_bearer +from ..models.metadataid import MetadataId from ..models.movie import SearchMovie from ..models.page import Page from ..models.request import CreateRequest, Request, RequestRet from ..models.serie import SearchSerie +from ..models.show import Show from ..models.videos import Video from ..providers.composite import CompositeProvider from ..requests import RequestCreator from ..status import StatusService from ..utils import Language -from .dependencies import get_preferred_languages, get_provider, get_request_creator +from .dependencies import ( + get_client, + get_preferred_languages, + get_provider, + get_request_creator, +) router = APIRouter() @@ -172,3 +182,67 @@ async def create_serie( ] ) return ret + + +@router.post( + "/movies/{slug}/refresh", + status_code=201, + response_description="Movie refresh request created.", +) +async def refresh_movie_by_slug( + slug: str, + client: Annotated[KyooClient, Depends(get_client)], + requests: Annotated[RequestCreator, Depends(get_request_creator)], + _: Annotated[None, Security(validate_bearer, scopes=["scanner.add"])], +) -> RequestRet: + """ + Refresh an existing movie + """ + + show = await client.get_movie(slug) + [ret] = await requests.enqueue( + [ + Request( + kind="movie", + title=show.name, + year=show.air_date.year + if show.air_date is not None + else None, + external_id=MetadataId.map_dict(show.external_id), + videos=[], + ) + ] + ) + return ret + + +@router.post( + "/series/{slug}/refresh", + status_code=201, + response_description="Series refresh request created.", +) +async def refresh_serie_by_slug( + slug: str, + client: Annotated[KyooClient, Depends(get_client)], + requests: Annotated[RequestCreator, Depends(get_request_creator)], + _: Annotated[None, Security(validate_bearer, scopes=["scanner.add"])], +) -> RequestRet: + """ + Refresh an existing serie + """ + + show = await client.get_serie(slug) + [ret] = await requests.enqueue( + [ + Request( + kind="episode", + title=show.name, + year=show.start_air.year + if show.start_air is not None + else None, + external_id=MetadataId.map_dict(show.external_id), + videos=[], + ) + ] + ) + return ret