diff --git a/scanner/providers/idmapper.py b/scanner/providers/idmapper.py new file mode 100644 index 00000000..6ce12d5c --- /dev/null +++ b/scanner/providers/idmapper.py @@ -0,0 +1,36 @@ +from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from providers.implementations.themoviedatabase import TheMovieDatabase + +from typing import List, Optional +from providers.types.metadataid import MetadataID + + +class IdMapper: + def init(self, *, language: str, tmdb: Optional[TheMovieDatabase]): + self.language = language + self._tmdb = tmdb + + async def get_show( + self, show: dict[str, MetadataID], *, required: Optional[List[str]] = None + ): + ids = show + + # Only fetch using tmdb if one of the required ids is not already known. + should_fetch = required is not None and any((x not in ids for x in required)) + if self._tmdb and self._tmdb.name in ids and should_fetch: + tmdb_info = await self._tmdb.identify_show( + ids[self._tmdb.name].data_id, + original_language=None, + language=[self.language], + ) + return {**ids, **tmdb_info.external_id} + return ids + + async def get_movie( + self, movie: dict[str, MetadataID], *, required: Optional[List[str]] = None + ): + # TODO: actually do something here + return movie diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index 7995ec0c..9be5eeeb 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -3,6 +3,7 @@ import logging from aiohttp import ClientSession from datetime import datetime from typing import Awaitable, Callable, Dict, List, Optional, Any, TypeVar +from providers.idmapper import IdMapper from providers.implementations.thexem import TheXem from providers.utils import ProviderError @@ -19,10 +20,13 @@ from ..types.collection import Collection, CollectionTranslation class TheMovieDatabase(Provider): - def __init__(self, client: ClientSession, api_key: str, xem: TheXem) -> None: + def __init__( + self, client: ClientSession, api_key: str, xem: TheXem, idmapper: IdMapper + ) -> None: super().__init__() self._client = client self._xem = xem + self._idmapper = idmapper self.base = "https://api.themoviedb.org/3" self.api_key = api_key self.genre_map = { @@ -212,17 +216,20 @@ class TheMovieDatabase(Provider): ret.translations = {lng: translation} return ret - return await self.process_translations(for_language, language) + ret = await self.process_translations(for_language, language) + # If we have more external_ids freely available, add them. + ret.external_id = await self._idmapper.get_movie(ret.external_id) + return ret async def identify_show( self, - pshow: PartialShow, + show_id: str, *, + original_language: Optional[str], language: list[str], ) -> Show: - show_id = pshow.external_id[self.name].data_id - if pshow.original_language not in language: - language.append(pshow.original_language) + if original_language and original_language not in language: + language.append(original_language) async def for_language(lng: str) -> Show: show = await self.get( @@ -290,7 +297,7 @@ class TheMovieDatabase(Provider): show["images"]["posters"] + ( [{"file_path": show["poster_path"]}] - if lng == pshow.original_language + if lng == show["original_language"] else [] ) ), @@ -299,7 +306,7 @@ class TheMovieDatabase(Provider): show["images"]["backdrops"] + ( [{"file_path": show["backdrop_path"]}] - if lng == pshow.original_language + if lng == show["original_language"] else [] ) ), @@ -333,6 +340,8 @@ class TheMovieDatabase(Provider): ret = await self.process_translations( for_language, language, merge_seasons_translations ) + # If we have more external_ids freely available, add them. + ret.external_id = await self._idmapper.get_show(ret.external_id) return ret def to_season( @@ -381,6 +390,14 @@ class TheMovieDatabase(Provider): raise ProviderError(f"No result for a tv show named: {name}") search = self.get_best_result(search_results, name, year) show_id = search["id"] + show = PartialShow( + original_language=search["original_language"], + external_id={ + self.name: MetadataID( + show_id, f"https://www.themoviedb.org/tv/{show_id}" + ) + }, + ) if search["original_language"] not in language: language.append(search["original_language"]) @@ -388,10 +405,18 @@ class TheMovieDatabase(Provider): # For example when name is "Jojo's bizzare adventure - Stone Ocean", with season None, # We want something like season 6 ep 3. if season is None and absolute is not None: - (tvdb_season, tvdb_episode, absolute) = await self._xem.get_episode_override("tvdb", tvdbid, name, absolute) - # Most of the time, tvdb absolute and tmdb absolute are in think so we use that as our souce of truth. - # tvdb_season/episode are not in sync with tmdb so we discard those and use our usual absolute order fetching. - (_, _) = tvdb_season, tvdb_episode + ids = await self._idmapper.get_show(show.external_id, required=["tmdbid"]) + if ids["tvdb"] is not None: + ( + tvdb_season, + tvdb_episode, + absolute, + ) = await self._xem.get_episode_override( + "tvdb", ids["tvdb"].data_id, name, absolute + ) + # Most of the time, tvdb absolute and tmdb absolute are in think so we use that as our souce of truth. + # tvdb_season/episode are not in sync with tmdb so we discard those and use our usual absolute order fetching. + (_, _) = tvdb_season, tvdb_episode if not show_id in self.absolute_episode_cache: await self.get_absolute_order(show_id) @@ -441,15 +466,7 @@ class TheMovieDatabase(Provider): logging.debug("TMDb responded: %s", episode) ret = Episode( - show=PartialShow( - name=search["name"], - original_language=search["original_language"], - external_id={ - self.name: MetadataID( - show_id, f"https://www.themoviedb.org/tv/{show_id}" - ) - }, - ), + show=show, season_number=episode["season_number"], episode_number=episode["episode_number"], absolute_number=absolute, diff --git a/scanner/providers/provider.py b/scanner/providers/provider.py index 69d94238..8ac53906 100644 --- a/scanner/providers/provider.py +++ b/scanner/providers/provider.py @@ -5,7 +5,7 @@ from typing import Optional, TypeVar from providers.utils import ProviderError -from .types.episode import Episode, PartialShow +from .types.episode import Episode from .types.show import Show from .types.movie import Movie from .types.collection import Collection @@ -16,22 +16,35 @@ Self = TypeVar("Self", bound="Provider") class Provider: @classmethod - def get_all(cls: type[Self], client: ClientSession) -> list[Self]: + def get_all( + cls: type[Self], client: ClientSession, languages: list[str] + ) -> list[Self]: providers = [] + from providers.idmapper import IdMapper + + idmapper = IdMapper() + from providers.implementations.thexem import TheXem + xem = TheXem(client) from providers.implementations.themoviedatabase import TheMovieDatabase + tmdb = os.environ.get("THEMOVIEDB_APIKEY") if tmdb: - providers.append(TheMovieDatabase(client, tmdb, xem)) + tmdb = TheMovieDatabase(client, tmdb, xem, idmapper) + providers.append(tmdb) + else: + tmdb = None if not any(providers): raise ProviderError( "No provider configured. You probably forgot to specify an API Key" ) + idmapper.init(tmdb=tmdb, language=languages[0]) + return providers @abstractproperty @@ -45,7 +58,9 @@ class Provider: raise NotImplementedError @abstractmethod - async def identify_show(self, show: PartialShow, *, language: list[str]) -> Show: + async def identify_show( + self, show_id: str, *, original_language: Optional[str], language: list[str] + ) -> Show: raise NotImplementedError @abstractmethod diff --git a/scanner/providers/types/episode.py b/scanner/providers/types/episode.py index 96243b69..078cbd9e 100644 --- a/scanner/providers/types/episode.py +++ b/scanner/providers/types/episode.py @@ -9,7 +9,6 @@ from .metadataid import MetadataID @dataclass class PartialShow: - name: str original_language: str external_id: dict[str, MetadataID] diff --git a/scanner/scanner/scanner.py b/scanner/scanner/scanner.py index 773c0b18..c29f250c 100644 --- a/scanner/scanner/scanner.py +++ b/scanner/scanner/scanner.py @@ -28,7 +28,7 @@ class Scanner: except Exception as e: self._ignore_pattern = re.compile("") logging.error(f"Invalid ignore pattern. Ignoring. Error: {e}") - self.provider = Provider.get_all(client)[0] + self.provider = Provider.get_all(client, languages)[0] self.cache = {"shows": {}, "seasons": {}, "collections": {}} self.languages = languages @@ -156,7 +156,11 @@ class Scanner: async def create_show(_: str): # TODO: Check if a show with the same metadata id exists already on kyoo. show = ( - await self.provider.identify_show(episode.show, language=self.languages) + await self.provider.identify_show( + episode.show.external_id[self.provider.name].data_id, + original_language=episode.show.original_language, + language=self.languages, + ) if isinstance(episode.show, PartialShow) else episode.show )