Add kyoo requests for episodes

This commit is contained in:
Zoe Roux 2023-03-25 00:30:37 +09:00
parent 2334afb3eb
commit c3b8595cd7
7 changed files with 73 additions and 32 deletions

View File

@ -41,6 +41,10 @@ class TheMovieDatabase(Provider):
37: Genre.WESTERN, 37: Genre.WESTERN,
} }
@property
def name(self) -> str:
return "themoviedatabase"
async def get(self, path: str, *, params: dict[str, Any] = {}): async def get(self, path: str, *, params: dict[str, Any] = {}):
params = {k: v for k, v in params.items() if v is not None} params = {k: v for k, v in params.items() if v is not None}
async with self._client.get( async with self._client.get(
@ -84,7 +88,7 @@ class TheMovieDatabase(Provider):
if "logo_path" in company if "logo_path" in company
else [], else [],
external_id={ external_id={
"themoviedatabase": MetadataID( self.name: MetadataID(
company["id"], f"https://www.themoviedb.org/company/{company['id']}" company["id"], f"https://www.themoviedb.org/company/{company['id']}"
) )
}, },
@ -127,7 +131,7 @@ class TheMovieDatabase(Provider):
if x["id"] in self.genre_map if x["id"] in self.genre_map
], ],
external_id={ external_id={
"themoviedatabase": MetadataID( self.name: MetadataID(
movie["id"], f"https://www.themoviedb.org/movie/{movie['id']}" movie["id"], f"https://www.themoviedb.org/movie/{movie['id']}"
), ),
"imdb": MetadataID( "imdb": MetadataID(
@ -162,7 +166,7 @@ class TheMovieDatabase(Provider):
*, *,
language: list[str], language: list[str],
) -> Show: ) -> Show:
show_id = show.external_id["themoviedatabase"].id show_id = show.external_id[self.name].id
if show.original_language not in language: if show.original_language not in language:
language.append(show.original_language) language.append(show.original_language)
@ -194,7 +198,7 @@ class TheMovieDatabase(Provider):
if x["id"] in self.genre_map if x["id"] in self.genre_map
], ],
external_id={ external_id={
"themoviedatabase": MetadataID( self.name: MetadataID(
show["id"], f"https://www.themoviedb.org/tv/{show['id']}" show["id"], f"https://www.themoviedb.org/tv/{show['id']}"
), ),
"imdb": MetadataID( "imdb": MetadataID(
@ -256,7 +260,7 @@ class TheMovieDatabase(Provider):
start_date=datetime.strptime(season["air_date"], "%Y-%m-%d").date(), start_date=datetime.strptime(season["air_date"], "%Y-%m-%d").date(),
end_date=None, end_date=None,
external_id={ external_id={
"themoviedatabase": MetadataID( self.name: MetadataID(
season["id"], season["id"],
f"https://www.themoviedb.org/tv/{show_id}/season/{season['season_number']}", f"https://www.themoviedb.org/tv/{show_id}/season/{season['season_number']}",
) )
@ -290,6 +294,8 @@ class TheMovieDatabase(Provider):
language.append(search["original_language"]) language.append(search["original_language"])
# TODO: Handle absolute episodes # 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: async def for_language(lng: str) -> Episode:
episode = await self.get( episode = await self.get(
@ -305,7 +311,7 @@ class TheMovieDatabase(Provider):
name=search["name"], name=search["name"],
original_language=search["original_language"], original_language=search["original_language"],
external_id={ external_id={
"themoviedatabase": MetadataID( self.name: MetadataID(
show_id, f"https://www.themoviedb.org/tv/{show_id}" show_id, f"https://www.themoviedb.org/tv/{show_id}"
) )
}, },
@ -319,7 +325,7 @@ class TheMovieDatabase(Provider):
if "poster_path" in episode if "poster_path" in episode
else None, else None,
external_id={ external_id={
"themoviedatabase": MetadataID( self.name: MetadataID(
episode["id"], episode["id"],
f"https://www.themoviedb.org/movie/{episode['id']}", f"https://www.themoviedb.org/movie/{episode['id']}",
), ),

View File

@ -1,9 +1,10 @@
import os import os
from aiohttp import ClientSession from aiohttp import ClientSession
from abc import abstractmethod from abc import abstractmethod, abstractproperty
from typing import Optional, TypeVar 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 from .types.movie import Movie
@ -23,18 +24,26 @@ class Provider:
return providers return providers
@abstractproperty
def name(self) -> str:
raise NotImplementedError
@abstractmethod @abstractmethod
async def identify_movie( async def identify_movie(
self, name: str, year: Optional[int], *, language: list[str] self, name: str, year: Optional[int], *, language: list[str]
) -> Movie: ) -> Movie:
raise NotImplementedError raise NotImplementedError
@abstractmethod
async def identify_show(self, show: PartialShow, *, language: list[str]) -> Show:
raise NotImplementedError
@abstractmethod @abstractmethod
async def identify_episode( async def identify_episode(
self, self,
name: str, name: str,
season: Optional[int], season: Optional[int],
episode: Optional[int], episode_nbr: Optional[int],
absolute: Optional[int], absolute: Optional[int],
*, *,
language: list[str] language: list[str]

View File

@ -1,7 +1,9 @@
import os
from datetime import date from datetime import date
from dataclasses import dataclass, field from dataclasses import dataclass, field, asdict
from typing import Optional from typing import Optional
from ..utils import format_date
from .show import Show from .show import Show
from .season import Season from .season import Season
from .metadataid import MetadataID from .metadataid import MetadataID
@ -26,9 +28,20 @@ class Episode:
season_number: Optional[int] season_number: Optional[int]
episode_number: Optional[int] episode_number: Optional[int]
absolute_number: Optional[int] absolute_number: Optional[int]
release_date: Optional[date | int] release_date: Optional[date]
thumbnail: Optional[str] thumbnail: Optional[str]
external_id: dict[str, MetadataID] external_id: dict[str, MetadataID]
path: Optional[str] = None path: Optional[str] = None
show_id: Optional[str] = None
translations: dict[str, EpisodeTranslation] = field(default_factory=dict) 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),
}

View File

@ -7,6 +7,7 @@ from enum import Enum
from .genre import Genre from .genre import Genre
from .studio import Studio from .studio import Studio
from .metadataid import MetadataID from .metadataid import MetadataID
from ..utils import format_date
class Status(str, Enum): class Status(str, Enum):
@ -43,13 +44,6 @@ class Movie:
translations: dict[str, MovieTranslation] = field(default_factory=dict) 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): def to_kyoo(self):
# For now, the API of kyoo only support one language so we remove the others. # For now, the API of kyoo only support one language so we remove the others.
default_language = os.environ["LIBRARY_LANGUAGES"].split(",")[0] default_language = os.environ["LIBRARY_LANGUAGES"].split(",")[0]
@ -64,7 +58,7 @@ class Movie:
"trailer": next(iter(self.translations[default_language].trailers), 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(x.to_kyoo() for x in self.studios), None),
"release_date": None, "release_date": None,
"startAir": self.format_date(self.release_date), "startAir": format_date(self.release_date),
"title": self.translations[default_language].name, "title": self.translations[default_language].name,
"genres": [x.to_kyoo() for x in self.genres], "genres": [x.to_kyoo() for x in self.genres],
"isMovie": True, "isMovie": True,

View File

@ -8,6 +8,7 @@ from .genre import Genre
from .studio import Studio from .studio import Studio
from .season import Season from .season import Season
from .metadataid import MetadataID from .metadataid import MetadataID
from ..utils import format_date
class Status(str, Enum): class Status(str, Enum):
UNKNOWN = "unknown" UNKNOWN = "unknown"
@ -45,13 +46,6 @@ class Show:
translations: dict[str, ShowTranslation] = field(default_factory=dict) 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): def to_kyoo(self):
# For now, the API of kyoo only support one language so we remove the others. # For now, the API of kyoo only support one language so we remove the others.
default_language = os.environ["LIBRARY_LANGUAGES"].split(",")[0] default_language = os.environ["LIBRARY_LANGUAGES"].split(",")[0]
@ -65,8 +59,8 @@ class Show:
"logo": next(iter(self.translations[default_language].logos), None), "logo": next(iter(self.translations[default_language].logos), None),
"trailer": next(iter(self.translations[default_language].trailers), 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(x.to_kyoo() for x in self.studios), None),
"startAir": self.format_date(self.start_air), "startAir": format_date(self.start_air),
"endAir": self.format_date(self.end_air), "endAir": format_date(self.end_air),
"title": self.translations[default_language].name, "title": self.translations[default_language].name,
"genres": [x.to_kyoo() for x in self.genres], "genres": [x.to_kyoo() for x in self.genres],
} }

View File

@ -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()

View File

@ -7,6 +7,7 @@ from aiohttp import ClientSession
from pathlib import Path from pathlib import Path
from guessit import guessit from guessit import guessit
from providers.provider import Provider from providers.provider import Provider
from providers.types.episode import PartialShow
def log_errors(f): def log_errors(f):
@ -27,6 +28,7 @@ class Scanner:
self._client = client self._client = client
self._api_key = api_key self._api_key = api_key
self.provider = Provider.get_all(client)[0] self.provider = Provider.get_all(client)[0]
self.cache = {"shows": {}}
self.languages = languages self.languages = languages
async def scan(self, path: str): async def scan(self, path: str):
@ -44,6 +46,7 @@ class Scanner:
# TODO: Add collections support # TODO: Add collections support
if raw["type"] == "movie": if raw["type"] == "movie":
return
movie = await self.provider.identify_movie( movie = await self.provider.identify_movie(
raw["title"], raw.get("year"), language=self.languages raw["title"], raw.get("year"), language=self.languages
) )
@ -51,21 +54,32 @@ class Scanner:
logging.debug("Got movie: %s", movie) logging.debug("Got movie: %s", movie)
await self.post("movies", data=movie.to_kyoo()) await self.post("movies", data=movie.to_kyoo())
elif raw["type"] == "episode": elif raw["type"] == "episode":
# TODO: Identify shows & seasons too.
episode = await self.provider.identify_episode( episode = await self.provider.identify_episode(
raw["title"], raw["title"],
season=raw.get("season"), season=raw.get("season"),
episode=raw.get("episode"), episode_nbr=raw.get("episode"),
absolute=raw.get("episode") if "season" not in raw else None, absolute=raw.get("episode") if "season" not in raw else None,
language=self.languages, language=self.languages,
) )
episode.path = str(path) episode.path = str(path)
logging.debug("Got episode: %s", episode) 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()) await self.post("episodes", data=episode.to_kyoo())
else: else:
logging.warn("Unknown video file type: %s", raw["type"]) 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") url = os.environ.get("KYOO_URL", "http://back:5000")
print(json.dumps(data, indent=4)) print(json.dumps(data, indent=4))
async with self._client.post( async with self._client.post(
@ -74,3 +88,6 @@ class Scanner:
if not r.ok: if not r.ok:
print(await r.text()) print(await r.text())
r.raise_for_status() r.raise_for_status()
ret = await r.json()
return ret["id"]