diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index d411323c..a6ff485c 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -41,6 +41,10 @@ class TheMovieDatabase(Provider): 37: Genre.WESTERN, } + @property + def name(self) -> str: + return "themoviedatabase" + async def get(self, path: str, *, params: dict[str, Any] = {}): params = {k: v for k, v in params.items() if v is not None} async with self._client.get( @@ -84,7 +88,7 @@ class TheMovieDatabase(Provider): if "logo_path" in company else [], external_id={ - "themoviedatabase": MetadataID( + self.name: MetadataID( company["id"], f"https://www.themoviedb.org/company/{company['id']}" ) }, @@ -127,7 +131,7 @@ class TheMovieDatabase(Provider): if x["id"] in self.genre_map ], external_id={ - "themoviedatabase": MetadataID( + self.name: MetadataID( movie["id"], f"https://www.themoviedb.org/movie/{movie['id']}" ), "imdb": MetadataID( @@ -162,7 +166,7 @@ class TheMovieDatabase(Provider): *, language: list[str], ) -> Show: - show_id = show.external_id["themoviedatabase"].id + show_id = show.external_id[self.name].id if show.original_language not in language: language.append(show.original_language) @@ -194,7 +198,7 @@ class TheMovieDatabase(Provider): if x["id"] in self.genre_map ], external_id={ - "themoviedatabase": MetadataID( + self.name: MetadataID( show["id"], f"https://www.themoviedb.org/tv/{show['id']}" ), "imdb": MetadataID( @@ -256,7 +260,7 @@ class TheMovieDatabase(Provider): start_date=datetime.strptime(season["air_date"], "%Y-%m-%d").date(), end_date=None, external_id={ - "themoviedatabase": MetadataID( + self.name: MetadataID( season["id"], f"https://www.themoviedb.org/tv/{show_id}/season/{season['season_number']}", ) @@ -290,6 +294,8 @@ class TheMovieDatabase(Provider): language.append(search["original_language"]) # TODO: Handle absolute episodes + if not season or not episode_nbr: + raise NotImplementedError("Absolute order episodes not implemented for the movie database") async def for_language(lng: str) -> Episode: episode = await self.get( @@ -305,7 +311,7 @@ class TheMovieDatabase(Provider): name=search["name"], original_language=search["original_language"], external_id={ - "themoviedatabase": MetadataID( + self.name: MetadataID( show_id, f"https://www.themoviedb.org/tv/{show_id}" ) }, @@ -319,7 +325,7 @@ class TheMovieDatabase(Provider): if "poster_path" in episode else None, external_id={ - "themoviedatabase": MetadataID( + self.name: MetadataID( episode["id"], f"https://www.themoviedb.org/movie/{episode['id']}", ), diff --git a/scanner/providers/provider.py b/scanner/providers/provider.py index 3275cf5c..6f4dedd4 100644 --- a/scanner/providers/provider.py +++ b/scanner/providers/provider.py @@ -1,9 +1,10 @@ import os from aiohttp import ClientSession -from abc import abstractmethod +from abc import abstractmethod, abstractproperty from typing import Optional, TypeVar -from .types.episode import Episode +from .types.episode import Episode, PartialShow +from .types.show import Show from .types.movie import Movie @@ -23,18 +24,26 @@ class Provider: return providers + @abstractproperty + def name(self) -> str: + raise NotImplementedError + @abstractmethod async def identify_movie( self, name: str, year: Optional[int], *, language: list[str] ) -> Movie: raise NotImplementedError + @abstractmethod + async def identify_show(self, show: PartialShow, *, language: list[str]) -> Show: + raise NotImplementedError + @abstractmethod async def identify_episode( self, name: str, season: Optional[int], - episode: Optional[int], + episode_nbr: Optional[int], absolute: Optional[int], *, language: list[str] diff --git a/scanner/providers/types/episode.py b/scanner/providers/types/episode.py index 939372c5..5a6c6567 100644 --- a/scanner/providers/types/episode.py +++ b/scanner/providers/types/episode.py @@ -1,7 +1,9 @@ +import os from datetime import date -from dataclasses import dataclass, field +from dataclasses import dataclass, field, asdict from typing import Optional +from ..utils import format_date from .show import Show from .season import Season from .metadataid import MetadataID @@ -26,9 +28,20 @@ class Episode: season_number: Optional[int] episode_number: Optional[int] absolute_number: Optional[int] - release_date: Optional[date | int] + release_date: Optional[date] thumbnail: Optional[str] external_id: dict[str, MetadataID] path: Optional[str] = None + show_id: Optional[str] = None translations: dict[str, EpisodeTranslation] = field(default_factory=dict) + + + def to_kyoo(self): + # For now, the API of kyoo only support one language so we remove the others. + default_language = os.environ["LIBRARY_LANGUAGES"].split(",")[0] + return { + **asdict(self), + **asdict(self.translations[default_language]), + "release_date": format_date(self.release_date), + } diff --git a/scanner/providers/types/movie.py b/scanner/providers/types/movie.py index d9255e26..7dda4c1e 100644 --- a/scanner/providers/types/movie.py +++ b/scanner/providers/types/movie.py @@ -7,6 +7,7 @@ from enum import Enum from .genre import Genre from .studio import Studio from .metadataid import MetadataID +from ..utils import format_date class Status(str, Enum): @@ -43,13 +44,6 @@ class Movie: translations: dict[str, MovieTranslation] = field(default_factory=dict) - def format_date(self, date: date | int | None) -> str | None: - if date is None: - return None - if isinstance(date, int): - return f"{date}-01-01T00:00:00Z" - return date.isoformat() - def to_kyoo(self): # For now, the API of kyoo only support one language so we remove the others. default_language = os.environ["LIBRARY_LANGUAGES"].split(",")[0] @@ -64,7 +58,7 @@ class Movie: "trailer": next(iter(self.translations[default_language].trailers), None), "studio": next(iter(x.to_kyoo() for x in self.studios), None), "release_date": None, - "startAir": self.format_date(self.release_date), + "startAir": format_date(self.release_date), "title": self.translations[default_language].name, "genres": [x.to_kyoo() for x in self.genres], "isMovie": True, diff --git a/scanner/providers/types/show.py b/scanner/providers/types/show.py index 87146f6c..8a587a71 100644 --- a/scanner/providers/types/show.py +++ b/scanner/providers/types/show.py @@ -8,6 +8,7 @@ from .genre import Genre from .studio import Studio from .season import Season from .metadataid import MetadataID +from ..utils import format_date class Status(str, Enum): UNKNOWN = "unknown" @@ -45,13 +46,6 @@ class Show: translations: dict[str, ShowTranslation] = field(default_factory=dict) - def format_date(self, date: date | int | None) -> str | None: - if date is None: - return None - if isinstance(date, int): - return f"{date}-01-01T00:00:00Z" - return date.isoformat() - def to_kyoo(self): # For now, the API of kyoo only support one language so we remove the others. default_language = os.environ["LIBRARY_LANGUAGES"].split(",")[0] @@ -65,8 +59,8 @@ class Show: "logo": next(iter(self.translations[default_language].logos), None), "trailer": next(iter(self.translations[default_language].trailers), None), "studio": next(iter(x.to_kyoo() for x in self.studios), None), - "startAir": self.format_date(self.start_air), - "endAir": self.format_date(self.end_air), + "startAir": format_date(self.start_air), + "endAir": format_date(self.end_air), "title": self.translations[default_language].name, "genres": [x.to_kyoo() for x in self.genres], } diff --git a/scanner/providers/utils.py b/scanner/providers/utils.py new file mode 100644 index 00000000..081dd525 --- /dev/null +++ b/scanner/providers/utils.py @@ -0,0 +1,8 @@ +from datetime import date + +def format_date(date: date | int | None) -> str | None: + if date is None: + return None + if isinstance(date, int): + return f"{date}-01-01T00:00:00Z" + return date.isoformat() diff --git a/scanner/scanner/scanner.py b/scanner/scanner/scanner.py index a8bb51f1..c51e4e50 100644 --- a/scanner/scanner/scanner.py +++ b/scanner/scanner/scanner.py @@ -7,6 +7,7 @@ from aiohttp import ClientSession from pathlib import Path from guessit import guessit from providers.provider import Provider +from providers.types.episode import PartialShow def log_errors(f): @@ -27,6 +28,7 @@ class Scanner: self._client = client self._api_key = api_key self.provider = Provider.get_all(client)[0] + self.cache = {"shows": {}} self.languages = languages async def scan(self, path: str): @@ -44,6 +46,7 @@ class Scanner: # TODO: Add collections support if raw["type"] == "movie": + return movie = await self.provider.identify_movie( raw["title"], raw.get("year"), language=self.languages ) @@ -51,21 +54,32 @@ class Scanner: logging.debug("Got movie: %s", movie) await self.post("movies", data=movie.to_kyoo()) elif raw["type"] == "episode": - # TODO: Identify shows & seasons too. episode = await self.provider.identify_episode( raw["title"], season=raw.get("season"), - episode=raw.get("episode"), + episode_nbr=raw.get("episode"), absolute=raw.get("episode") if "season" not in raw else None, language=self.languages, ) episode.path = str(path) logging.debug("Got episode: %s", episode) + + show_provider_id = episode.show.external_id[self.provider.name].id + if ( + isinstance(episode.show, PartialShow) + and show_provider_id not in self.cache["shows"] + ): + show = await self.provider.identify_show( + episode.show, language=self.languages + ) + logging.debug("Got show: %s", episode) + self.cache["shows"][show_provider_id] = await self.post("show", data=show.to_kyoo()) + episode.show_id = self.cache["shows"][show_provider_id] await self.post("episodes", data=episode.to_kyoo()) else: logging.warn("Unknown video file type: %s", raw["type"]) - async def post(self, path: str, *, data: object): + async def post(self, path: str, *, data: object) -> str: url = os.environ.get("KYOO_URL", "http://back:5000") print(json.dumps(data, indent=4)) async with self._client.post( @@ -74,3 +88,6 @@ class Scanner: if not r.ok: print(await r.text()) r.raise_for_status() + ret = await r.json() + return ret["id"] +