Identify collections from themoviedb

This commit is contained in:
Zoe Roux 2023-11-01 16:35:07 +01:00
parent 88eb325079
commit 68a83c31be
6 changed files with 137 additions and 9 deletions

View File

@ -14,6 +14,7 @@ from ..types.studio import Studio
from ..types.genre import Genre
from ..types.metadataid import MetadataID
from ..types.show import Show, ShowTranslation, Status as ShowStatus
from ..types.collection import Collection, CollectionTranslation
class TheMovieDatabase(Provider):
@ -163,7 +164,19 @@ class TheMovieDatabase(Provider):
if movie["imdb_id"]
else {}
)
)
),
collections=[
Collection(
external_id={
self.name: MetadataID(
movie["belongs_to_collection"]["id"],
f"https://www.themoviedb.org/collection/{movie['belongs_to_collection']['id']}",
)
},
)
]
if movie["belongs_to_collection"] is not None
else [],
# TODO: Add cast information
)
translation = MovieTranslation(
@ -218,7 +231,6 @@ class TheMovieDatabase(Provider):
},
)
logging.debug("TMDb responded: %s", show)
# TODO: Use collection data
ret = Show(
original_language=show["original_language"],
@ -509,3 +521,39 @@ class TheMovieDatabase(Provider):
logging.exception(
"Could not retrieve absolute ordering information", exc_info=e
)
async def identify_collection(
self, provider_id: str, *, language: list[str]
) -> Collection:
async def for_language(lng: str) -> Collection:
collection = await self.get(
f"collection/{provider_id}",
params={
"language": lng,
},
)
logging.debug("TMDb responded: %s", collection)
ret = Collection(
external_id={
self.name: MetadataID(
collection["id"],
f"https://www.themoviedb.org/collection/{collection['id']}",
)
},
)
translation = CollectionTranslation(
name=collection["name"],
overview=collection["overview"],
posters=[
f"https://image.tmdb.org/t/p/original{collection['poster_path']}"
],
logos=[],
thumbnails=[
f"https://image.tmdb.org/t/p/original{collection['backdrop_path']}"
],
)
ret.translations = {lng: translation}
return ret
return await self.process_translations(for_language, language)

View File

@ -8,6 +8,7 @@ from providers.utils import ProviderError
from .types.episode import Episode, PartialShow
from .types.show import Show
from .types.movie import Movie
from .types.collection import Collection
Self = TypeVar("Self", bound="Provider")
@ -57,3 +58,8 @@ class Provider:
language: list[str]
) -> Episode:
raise NotImplementedError
@abstractmethod
async def identify_collection(self, provider_id: str, *, language: list[str]) -> Collection:
raise NotImplementedError

View File

@ -0,0 +1,31 @@
import os
from dataclasses import asdict, dataclass, field
from typing import Optional
from .metadataid import MetadataID
@dataclass
class CollectionTranslation:
name: str
overview: Optional[str] = None
posters: list[str] = field(default_factory=list)
logos: list[str] = field(default_factory=list)
thumbnails: list[str] = field(default_factory=list)
@dataclass
class Collection:
external_id: dict[str, MetadataID]
translations: dict[str, CollectionTranslation] = 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),
"logo": next(iter(self.translations[default_language].logos), None),
}

View File

@ -33,6 +33,7 @@ class Episode:
path: Optional[str] = None
show_id: Optional[str] = None
season_id: Optional[str] = None
translations: dict[str, EpisodeTranslation] = field(default_factory=dict)
def to_kyoo(self):

View File

@ -4,6 +4,7 @@ from datetime import date
from typing import Optional
from enum import Enum
from .collection import Collection
from .genre import Genre
from .studio import Studio
from .metadataid import MetadataID
@ -43,6 +44,7 @@ class Movie:
external_id: dict[str, MetadataID]
path: Optional[str] = None
collections: list[Collection] = field(default_factory=list)
translations: dict[str, MovieTranslation] = field(default_factory=dict)
def to_kyoo(self):
@ -59,4 +61,5 @@ class Movie:
"trailer": next(iter(self.translations[default_language].trailers), None),
"studio": next((x.to_kyoo() for x in self.studios), None),
"genres": [x.to_kyoo() for x in self.genres],
"collections": None,
}

View File

@ -6,8 +6,9 @@ import re
from aiohttp import ClientSession
from pathlib import Path
from guessit import guessit
from typing import List
from typing import List, Literal
from providers.provider import Provider
from providers.types.collection import Collection
from providers.types.episode import Episode, PartialShow
from providers.types.season import Season, SeasonTranslation
from .utils import batch, log_errors, provider_cache, set_in_cache
@ -28,7 +29,7 @@ class Scanner:
self._ignore_pattern = re.compile("")
logging.error(f"Invalid ignore pattern. Ignoring. Error: {e}")
self.provider = Provider.get_all(client)[0]
self.cache = {"shows": {}, "seasons": {}}
self.cache = {"shows": {}, "seasons": {}, "collections": {}}
self.languages = languages
async def scan(self, path: str):
@ -83,14 +84,21 @@ class Scanner:
logging.info("Identied %s: %s", path, raw)
# TODO: Add collections support
if raw["type"] == "movie":
movie = await self.provider.identify_movie(
raw["title"], raw.get("year"), language=self.languages
)
movie.path = str(path)
logging.debug("Got movie: %s", movie)
await self.post("movies", data=movie.to_kyoo())
movie_id = await self.post("movies", data=movie.to_kyoo())
if any(movie.collections):
ids = await asyncio.gather(
*(self.create_or_get_collection(x) for x in movie.collections)
)
await asyncio.gather(
*(self.link_collection(x, "movie", movie_id) for x in ids)
)
elif raw["type"] == "episode":
episode = await self.provider.identify_episode(
raw["title"],
@ -105,7 +113,7 @@ class Scanner:
episode.show_id = await self.create_or_get_show(episode)
if episode.season_number is not None:
await self.register_seasons(
episode.season_id = await self.register_seasons(
show_id=episode.show_id,
season_number=episode.season_number,
)
@ -113,6 +121,36 @@ class Scanner:
else:
logging.warn("Unknown video file type: %s", raw["type"])
async def create_or_get_collection(self, collection: Collection) -> str:
@provider_cache("collection")
async def create_collection(provider_id: str):
# TODO: Check if a collection with the same metadata id exists already on kyoo.
new_collection = (
await self.provider.identify_collection(
provider_id, language=self.languages
)
if not any(collection.translations.keys())
else collection
)
logging.debug("Got collection: %s", new_collection)
return await self.post("collection", data=new_collection.to_kyoo())
# The parameter is only used as a key for the cache.
provider_id = collection.external_id[self.provider.name].data_id
return await create_collection(provider_id)
async def link_collection(
self, collection: str, type: Literal["movie"] | Literal["show"], id: str
):
async with self._client.put(
f"{self._url}/collections/{collection}/{type}/{id}",
headers={"X-API-Key": self._api_key},
) as r:
# Allow 409 and continue as if it worked.
if not r.ok and r.status != 409:
logging.error(f"Request error: {await r.text()}")
r.raise_for_status()
async def create_or_get_show(self, episode: Episode) -> str:
@provider_cache("shows")
async def create_show(_: str):
@ -122,6 +160,7 @@ class Scanner:
if isinstance(episode.show, PartialShow)
else episode.show
)
# TODO: collections
logging.debug("Got show: %s", episode)
ret = await self.post("show", data=show.to_kyoo())
try:
@ -138,14 +177,14 @@ class Scanner:
return await create_show(provider_id)
@provider_cache("seasons")
async def register_seasons(self, show_id: str, season_number: int):
async def register_seasons(self, show_id: str, season_number: int) -> str:
# TODO: fetch season here. this will be useful when a new season of a show is aired after the show has been created on kyoo.
season = Season(
season_number=season_number,
show_id=show_id,
translations={lng: SeasonTranslation() for lng in self.languages},
)
await self.post("seasons", data=season.to_kyoo())
return await self.post("seasons", data=season.to_kyoo())
async def post(self, path: str, *, data: object) -> str:
logging.debug(