diff --git a/scanner/providers/implementations/themoviedatabase.py b/scanner/providers/implementations/themoviedatabase.py index a6ff485c..c162a5e6 100644 --- a/scanner/providers/implementations/themoviedatabase.py +++ b/scanner/providers/implementations/themoviedatabase.py @@ -1,7 +1,7 @@ import asyncio -from datetime import datetime import logging from aiohttp import ClientSession +from datetime import datetime from typing import Awaitable, Callable, Dict, Optional, Any, TypeVar from ..provider import Provider @@ -175,7 +175,7 @@ class TheMovieDatabase(Provider): f"/tv/{show_id}", params={ "language": lng, - "append_to_response": "alternative_titles,videos,credits,keywords,images", + "append_to_response": "alternative_titles,videos,credits,keywords,images,external_ids", }, ) logging.debug("TMDb responded: %s", show) @@ -183,7 +183,7 @@ class TheMovieDatabase(Provider): ret = Show( original_language=show["original_language"], - aliases=[x["title"] for x in show["alternative_titles"]["titles"]], + aliases=[x["title"] for x in show["alternative_titles"]["results"]], start_air=datetime.strptime(show["first_air_date"], "%Y-%m-%d").date(), end_air=datetime.strptime(show["last_air_date"], "%Y-%m-%d").date(), status=ShowStatus.FINISHED @@ -202,9 +202,10 @@ class TheMovieDatabase(Provider): show["id"], f"https://www.themoviedb.org/tv/{show['id']}" ), "imdb": MetadataID( - show["imdb_id"], - f"https://www.imdb.com/title/{show['imdb_id']}", + show["external_ids"]["imdb_id"], + f"https://www.imdb.com/title/{show['external_ids']['imdb_id']}", ), + "tvdb": MetadataID(show["external_ids"]["tvdb_id"], link=None), }, seasons=[ self.to_season(x, language=lng, show_id=show["id"]) @@ -215,7 +216,7 @@ class TheMovieDatabase(Provider): translation = ShowTranslation( name=show["name"], tagline=show["tagline"], - keywords=list(map(lambda x: x["name"], show["keywords"]["keywords"])), + keywords=list(map(lambda x: x["name"], show["keywords"]["results"])), overview=show["overview"], posters=self.get_image(show["images"]["posters"]), logos=self.get_image(show["images"]["logos"]), @@ -257,8 +258,8 @@ class TheMovieDatabase(Provider): ) -> Season: return Season( season_number=season["season_number"], - start_date=datetime.strptime(season["air_date"], "%Y-%m-%d").date(), - end_date=None, + start_air=datetime.strptime(season["air_date"], "%Y-%m-%d").date(), + end_air=None, external_id={ self.name: MetadataID( season["id"], @@ -269,10 +270,10 @@ class TheMovieDatabase(Provider): language: SeasonTranslation( name=season["name"], overview=season["overview"], - poster=[ + posters=[ f"https://image.tmdb.org/t/p/original{season['poster_path']}" ] - if "poster_path" in season + if season["poster_path"] is not None else [], thumbnails=[], ) @@ -295,7 +296,9 @@ class TheMovieDatabase(Provider): # TODO: Handle absolute episodes if not season or not episode_nbr: - raise NotImplementedError("Absolute order episodes not implemented for the movie database") + raise NotImplementedError( + "Absolute order episodes not implemented for the movie database" + ) async def for_language(lng: str) -> Episode: episode = await self.get( @@ -327,7 +330,7 @@ class TheMovieDatabase(Provider): external_id={ self.name: MetadataID( episode["id"], - f"https://www.themoviedb.org/movie/{episode['id']}", + f"https://www.themoviedb.org/tv/{show_id}/season/{episode['season_number']}/episode/{episode['episode_number']}", ), }, ) diff --git a/scanner/providers/types/episode.py b/scanner/providers/types/episode.py index 5a6c6567..7fd9e0ac 100644 --- a/scanner/providers/types/episode.py +++ b/scanner/providers/types/episode.py @@ -28,7 +28,7 @@ class Episode: season_number: Optional[int] episode_number: Optional[int] absolute_number: Optional[int] - release_date: Optional[date] + release_date: Optional[date | int] thumbnail: Optional[str] external_id: dict[str, MetadataID] @@ -43,5 +43,4 @@ class Episode: return { **asdict(self), **asdict(self.translations[default_language]), - "release_date": format_date(self.release_date), } diff --git a/scanner/providers/types/metadataid.py b/scanner/providers/types/metadataid.py index 8777ff19..9b163e39 100644 --- a/scanner/providers/types/metadataid.py +++ b/scanner/providers/types/metadataid.py @@ -1,7 +1,8 @@ from dataclasses import dataclass +from typing import Optional @dataclass class MetadataID: id: str - link: str + link: Optional[str] diff --git a/scanner/providers/types/movie.py b/scanner/providers/types/movie.py index 7dda4c1e..964382f4 100644 --- a/scanner/providers/types/movie.py +++ b/scanner/providers/types/movie.py @@ -56,7 +56,7 @@ class Movie: ), "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), + "studio": next(iter(self.studios), None), "release_date": None, "startAir": format_date(self.release_date), "title": self.translations[default_language].name, diff --git a/scanner/providers/types/season.py b/scanner/providers/types/season.py index 062561da..99f04e63 100644 --- a/scanner/providers/types/season.py +++ b/scanner/providers/types/season.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 .metadataid import MetadataID @@ -9,15 +11,27 @@ from .metadataid import MetadataID class SeasonTranslation: name: Optional[str] overview: Optional[str] - poster: list[str] + posters: list[str] thumbnails: list[str] @dataclass class Season: season_number: int - start_date: Optional[date | int] - end_date: Optional[date | int] + start_air: Optional[date | int] + end_air: Optional[date | int] external_id: dict[str, MetadataID] translations: dict[str, SeasonTranslation] = 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]), + "poster": next(iter(self.translations[default_language].posters), None), + "thumbnail": next( + iter(self.translations[default_language].thumbnails), None + ), + } diff --git a/scanner/providers/types/show.py b/scanner/providers/types/show.py index 8a587a71..0c558a7a 100644 --- a/scanner/providers/types/show.py +++ b/scanner/providers/types/show.py @@ -10,6 +10,7 @@ from .season import Season from .metadataid import MetadataID from ..utils import format_date + class Status(str, Enum): UNKNOWN = "unknown" FINISHED = "finished" @@ -58,9 +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": format_date(self.start_air), - "endAir": format_date(self.end_air), + "studio": next(iter(self.studios), None), "title": self.translations[default_language].name, "genres": [x.to_kyoo() for x in self.genres], + "seasons": [x.to_kyoo() for x in self.seasons], } diff --git a/scanner/providers/types/studio.py b/scanner/providers/types/studio.py index 28a608a7..6ca6a7a1 100644 --- a/scanner/providers/types/studio.py +++ b/scanner/providers/types/studio.py @@ -8,6 +8,3 @@ class Studio: name: str logos: list[str] = field(default_factory=list) external_id: dict[str, MetadataID] = field(default_factory=dict) - - def to_kyoo(self): - return asdict(self) diff --git a/scanner/providers/utils.py b/scanner/providers/utils.py index 081dd525..c0e6d8a2 100644 --- a/scanner/providers/utils.py +++ b/scanner/providers/utils.py @@ -1,8 +1,9 @@ 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 f"{date}-01-01" return date.isoformat() diff --git a/scanner/requirements.txt b/scanner/requirements.txt index 94681b78..1deb3bf3 100644 --- a/scanner/requirements.txt +++ b/scanner/requirements.txt @@ -1,3 +1,4 @@ guessit aiohttp +jsons black-with-tabs diff --git a/scanner/scanner/__init__.py b/scanner/scanner/__init__.py index 5d5929b7..81a40ac7 100644 --- a/scanner/scanner/__init__.py +++ b/scanner/scanner/__init__.py @@ -5,7 +5,11 @@ async def main(): import os import logging import sys + import jsons + from datetime import date + from typing import Optional from aiohttp import ClientSession + from providers.utils import format_date path = os.environ.get("LIBRARY_ROOT") if not path: @@ -28,7 +32,12 @@ async def main(): if len(sys.argv) > 1 and sys.argv[1] == "-vv": logging.basicConfig(level=logging.DEBUG) - async with ClientSession() as client: + jsons.set_serializer(lambda x, **_: format_date(x), Optional[date | int]) + async with ClientSession( + json_serialize=lambda *args, **kwargs: jsons.dumps( + *args, key_transformer=jsons.KEY_TRANSFORMER_CAMELCASE, **kwargs + ), + ) as client: await Scanner(client, languages=languages.split(","), api_key=api_key).scan( path ) diff --git a/scanner/scanner/scanner.py b/scanner/scanner/scanner.py index c51e4e50..a70ade00 100644 --- a/scanner/scanner/scanner.py +++ b/scanner/scanner/scanner.py @@ -1,13 +1,14 @@ from functools import wraps -import json import os import asyncio import logging +import jsons from aiohttp import ClientSession from pathlib import Path from guessit import guessit from providers.provider import Provider -from providers.types.episode import PartialShow +from providers.types.episode import Episode, PartialShow +from providers.types.show import Show def log_errors(f): @@ -81,7 +82,15 @@ class Scanner: async def post(self, path: str, *, data: object) -> str: url = os.environ.get("KYOO_URL", "http://back:5000") - print(json.dumps(data, indent=4)) + logging.info( + "Sending %s: %s", + path, + jsons.dumps( + data, + key_transformer=jsons.KEY_TRANSFORMER_CAMELCASE, + jdkwargs={"indent": 4}, + ), + ) async with self._client.post( f"{url}/{path}", json=data, headers={"X-API-Key": self._api_key} ) as r: