mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-23 07:20:33 -04:00
Identify collections from themoviedb
This commit is contained in:
parent
88eb325079
commit
68a83c31be
@ -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)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
31
scanner/providers/types/collection.py
Normal file
31
scanner/providers/types/collection.py
Normal 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),
|
||||||
|
}
|
@ -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):
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -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(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user