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.genre import Genre
from ..types.metadataid import MetadataID from ..types.metadataid import MetadataID
from ..types.show import Show, ShowTranslation, Status as ShowStatus from ..types.show import Show, ShowTranslation, Status as ShowStatus
from ..types.collection import Collection, CollectionTranslation
class TheMovieDatabase(Provider): class TheMovieDatabase(Provider):
@ -163,7 +164,19 @@ class TheMovieDatabase(Provider):
if movie["imdb_id"] if movie["imdb_id"]
else {} 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 # TODO: Add cast information
) )
translation = MovieTranslation( translation = MovieTranslation(
@ -218,7 +231,6 @@ class TheMovieDatabase(Provider):
}, },
) )
logging.debug("TMDb responded: %s", show) logging.debug("TMDb responded: %s", show)
# TODO: Use collection data
ret = Show( ret = Show(
original_language=show["original_language"], original_language=show["original_language"],
@ -509,3 +521,39 @@ class TheMovieDatabase(Provider):
logging.exception( logging.exception(
"Could not retrieve absolute ordering information", exc_info=e "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.episode import Episode, PartialShow
from .types.show import Show from .types.show import Show
from .types.movie import Movie from .types.movie import Movie
from .types.collection import Collection
Self = TypeVar("Self", bound="Provider") Self = TypeVar("Self", bound="Provider")
@ -57,3 +58,8 @@ class Provider:
language: list[str] language: list[str]
) -> Episode: ) -> Episode:
raise NotImplementedError 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 path: Optional[str] = None
show_id: Optional[str] = None show_id: Optional[str] = None
season_id: Optional[str] = None
translations: dict[str, EpisodeTranslation] = field(default_factory=dict) translations: dict[str, EpisodeTranslation] = field(default_factory=dict)
def to_kyoo(self): def to_kyoo(self):

View File

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

View File

@ -6,8 +6,9 @@ import re
from aiohttp import ClientSession from aiohttp import ClientSession
from pathlib import Path from pathlib import Path
from guessit import guessit from guessit import guessit
from typing import List from typing import List, Literal
from providers.provider import Provider from providers.provider import Provider
from providers.types.collection import Collection
from providers.types.episode import Episode, PartialShow from providers.types.episode import Episode, PartialShow
from providers.types.season import Season, SeasonTranslation from providers.types.season import Season, SeasonTranslation
from .utils import batch, log_errors, provider_cache, set_in_cache from .utils import batch, log_errors, provider_cache, set_in_cache
@ -28,7 +29,7 @@ class Scanner:
self._ignore_pattern = re.compile("") self._ignore_pattern = re.compile("")
logging.error(f"Invalid ignore pattern. Ignoring. Error: {e}") logging.error(f"Invalid ignore pattern. Ignoring. Error: {e}")
self.provider = Provider.get_all(client)[0] self.provider = Provider.get_all(client)[0]
self.cache = {"shows": {}, "seasons": {}} self.cache = {"shows": {}, "seasons": {}, "collections": {}}
self.languages = languages self.languages = languages
async def scan(self, path: str): async def scan(self, path: str):
@ -83,14 +84,21 @@ class Scanner:
logging.info("Identied %s: %s", path, raw) logging.info("Identied %s: %s", path, raw)
# TODO: Add collections support
if raw["type"] == "movie": if raw["type"] == "movie":
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
) )
movie.path = str(path) movie.path = str(path)
logging.debug("Got movie: %s", movie) 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": elif raw["type"] == "episode":
episode = await self.provider.identify_episode( episode = await self.provider.identify_episode(
raw["title"], raw["title"],
@ -105,7 +113,7 @@ class Scanner:
episode.show_id = await self.create_or_get_show(episode) episode.show_id = await self.create_or_get_show(episode)
if episode.season_number is not None: if episode.season_number is not None:
await self.register_seasons( episode.season_id = await self.register_seasons(
show_id=episode.show_id, show_id=episode.show_id,
season_number=episode.season_number, season_number=episode.season_number,
) )
@ -113,6 +121,36 @@ class Scanner:
else: else:
logging.warn("Unknown video file type: %s", raw["type"]) 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: async def create_or_get_show(self, episode: Episode) -> str:
@provider_cache("shows") @provider_cache("shows")
async def create_show(_: str): async def create_show(_: str):
@ -122,6 +160,7 @@ class Scanner:
if isinstance(episode.show, PartialShow) if isinstance(episode.show, PartialShow)
else episode.show else episode.show
) )
# TODO: collections
logging.debug("Got show: %s", episode) logging.debug("Got show: %s", episode)
ret = await self.post("show", data=show.to_kyoo()) ret = await self.post("show", data=show.to_kyoo())
try: try:
@ -138,14 +177,14 @@ class Scanner:
return await create_show(provider_id) return await create_show(provider_id)
@provider_cache("seasons") @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. # 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 = Season(
season_number=season_number, season_number=season_number,
show_id=show_id, show_id=show_id,
translations={lng: SeasonTranslation() for lng in self.languages}, 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: async def post(self, path: str, *, data: object) -> str:
logging.debug( logging.debug(