diff --git a/api/src/models/entry/movie-entry.ts b/api/src/models/entry/movie-entry.ts index 1a8df313..3aa65ee9 100644 --- a/api/src/models/entry/movie-entry.ts +++ b/api/src/models/entry/movie-entry.ts @@ -56,7 +56,7 @@ export type MovieEntry = Prettify; export const SeedMovieEntry = t.Composite([ t.Omit(BaseMovieEntry, ["thumbnail", "nextRefresh"]), t.Object({ - slug: t.Optional(t.String({ format: "slug" })), + slug: t.Optional(t.Nullable(t.String({ format: "slug" }))), thumbnail: t.Nullable(SeedImage), translations: TranslationRecord( t.Intersect([ diff --git a/scanner/scanner/identify.py b/scanner/scanner/identify.py index 661da612..2c290a8f 100644 --- a/scanner/scanner/identify.py +++ b/scanner/scanner/identify.py @@ -41,7 +41,7 @@ async def identify(path: str) -> Video: guess = Guess( title=cast(str, title.value), - kind=cast(Literal["episode"] | Literal["movie"], kind.value), + kind=cast(Literal["episode", "movie"], kind.value), extra_kind=None, years=[cast(int, y.value) for y in years], episodes=[ diff --git a/scanner/scanner/models/collection.py b/scanner/scanner/models/collection.py index 69a08840..d182acd7 100644 --- a/scanner/scanner/models/collection.py +++ b/scanner/scanner/models/collection.py @@ -25,7 +25,7 @@ class CollectionTranslation(Model): aliases: list[str] tags: list[str] - posters: list[str] - thumbnails: list[str] - banner: list[str] - logos: list[str] + poster: str | None + thumbnail: str | None + banner: str | None + logo: str | None diff --git a/scanner/scanner/models/entry.py b/scanner/scanner/models/entry.py new file mode 100644 index 00000000..bada93ee --- /dev/null +++ b/scanner/scanner/models/entry.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from datetime import date +from typing import Literal + +from ..utils import Model +from .metadataid import EpisodeId, MetadataId + + +class Entry(Model): + kind: Literal["episode", "movie", "special"] + order: float + runtime: int | None = None + air_date: date | None = None + thumbnail: str | None = None + + # Movie-specific fields + slug: str | None = None + + # Episode-specific fields + season_number: int | None = None + episode_number: int | None = None + + # Special-specific fields + number: int | None = None + + externalId: dict[str, MetadataId | EpisodeId] + translations: dict[str, EntryTranslation] = {} + videos: list[str] = [] + + +class EntryTranslation(Model): + name: str | None = None + description: str | None = None + tagline: str | None = None + poster: str | None = None diff --git a/scanner/scanner/models/episode.py b/scanner/scanner/models/episode.py deleted file mode 100644 index 8da11c44..00000000 --- a/scanner/scanner/models/episode.py +++ /dev/null @@ -1,54 +0,0 @@ -from datetime import date -from dataclasses import dataclass, field, asdict -from typing import Optional - -from providers.utils import select_translation - -from .show import Show -from .metadataid import MetadataID - - -@dataclass -class PartialShow: - name: str - original_language: Optional[str] - external_id: dict[str, MetadataID] - - -@dataclass -class EpisodeID: - show_id: str - season: Optional[int] - episode: int - link: str - - -@dataclass -class EpisodeTranslation: - name: Optional[str] - overview: Optional[str] = None - - -@dataclass -class Episode: - show: Show | PartialShow - season_number: int - episode_number: int - absolute_number: int - runtime: Optional[int] - release_date: Optional[date | int] - thumbnail: Optional[str] - external_id: dict[str, EpisodeID] - - 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): - trans = select_translation(self) or EpisodeTranslation("") - return { - **asdict(self), - **asdict(trans), - "show": None, - } diff --git a/scanner/scanner/models/extra.py b/scanner/scanner/models/extra.py index f5ca9a41..0b09985f 100644 --- a/scanner/scanner/models/extra.py +++ b/scanner/scanner/models/extra.py @@ -1,10 +1,21 @@ -from typing import Literal +from enum import Enum -type ExtraKind = ( - Literal["other"] - | Literal["trailer"] - | Literal["interview"] - | Literal["behind-the-scene"] - | Literal["deleted-scene"] - | Literal["blooper"] -) +from ..utils import Model + + +class ExtraKind(str, Enum): + OTHER = "other" + TRAILER = "trailer" + INTERVIEW = "interview" + BEHIND_THE_SCENE = "behind-the-scene" + DELETED_SCENE = "deleted-scene" + BLOOPER = "blooper" + + +class Extra(Model): + kind: ExtraKind + slug: str + name: str + runtime: int | None + thumbnail: str | None + video: str diff --git a/scanner/scanner/models/movie.py b/scanner/scanner/models/movie.py index 04a1d35e..72b18eb7 100644 --- a/scanner/scanner/models/movie.py +++ b/scanner/scanner/models/movie.py @@ -13,7 +13,7 @@ from .staff import Staff from .studio import Studio -class Status(str, Enum): +class MovieStatus(str, Enum): UNKNOWN = "unknown" FINISHED = "finished" PLANNED = "planned" @@ -24,7 +24,7 @@ class Movie(Model): original_language: Language | None genres: list[Genre] rating: int | None - status: Status + status: MovieStatus runtime: int | None air_date: date | None @@ -44,11 +44,11 @@ class MovieTranslation(Model): aliases: list[str] tags: list[str] - posters: list[str] - thumbnails: list[str] - banner: list[str] - logos: list[str] - trailers: list[str] + poster: str | None + thumbnail: str | None + banner: str | None + logo: str | None + trailer: str | None class SearchMovie(Model): diff --git a/scanner/scanner/models/season.py b/scanner/scanner/models/season.py index b553dae0..351f08b4 100644 --- a/scanner/scanner/models/season.py +++ b/scanner/scanner/models/season.py @@ -1,38 +1,22 @@ +from __future__ import annotations + from datetime import date -from dataclasses import dataclass, field, asdict -from typing import Optional -from providers.utils import select_translation, select_image - -from .metadataid import MetadataID +from ..utils import Model +from .metadataid import MetadataId -@dataclass -class SeasonTranslation: - name: Optional[str] = None - overview: Optional[str] = None - posters: list[str] = field(default_factory=list) - thumbnails: list[str] = field(default_factory=list) - - -@dataclass -class Season: +class Season(Model): season_number: int - # This is not used by kyoo, this is just used internaly by the TMDB provider. - # maybe this should be moved? - episodes_count: int - start_air: Optional[date | int] = None - end_air: Optional[date | int] = None - external_id: dict[str, MetadataID] = field(default_factory=dict) + start_air: date | None + end_air: date | None + external_id: dict[str, MetadataId] + translations: dict[str, SeasonTranslation] = {} - show_id: Optional[str] = None - translations: dict[str, SeasonTranslation] = field(default_factory=dict) - def to_kyoo(self): - trans = select_translation(self) or SeasonTranslation() - return { - **asdict(self), - **asdict(trans), - "poster": select_image(self, "posters"), - "thumbnail": select_image(self, "thumbnails"), - } +class SeasonTranslation(Model): + name: str | None + description: str | None + poster: str | None + thumbnail: str | None + banner: str | None diff --git a/scanner/scanner/models/serie.py b/scanner/scanner/models/serie.py new file mode 100644 index 00000000..6790d31c --- /dev/null +++ b/scanner/scanner/models/serie.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from datetime import date +from enum import Enum + +from langcodes import Language + +from ..utils import Model +from .collection import Collection +from .entry import Entry +from .extra import Extra +from .genre import Genre +from .metadataid import MetadataId +from .season import Season +from .staff import Staff +from .studio import Studio + + +class SerieStatus(str, Enum): + UNKNOWN = "unknown" + FINISHED = "finished" + AIRING = "airing" + PLANNED = "planned" + + +class Serie(Model): + slug: str + original_language: Language | None + genres: list[Genre] + rating: int | None + status: SerieStatus + runtime: int | None + start_air: date | None + end_air: date | None + + external_id: dict[str, MetadataId] + translations: dict[str, SerieTranslation] = {} + seasons: list[Season] = [] + entries: list[Entry] = [] + extra: list[Extra] = [] + collections: list[Collection] = [] + studios: list[Studio] = [] + staff: list[Staff] = [] + + +class SerieTranslation(Model): + name: str + latin_name: str | None + description: str | None + tagline: str | None + aliases: list[str] + tags: list[str] + + poster: str | None + thumbnail: str | None + banner: str | None + logo: str | None + trailer: str | None + + +class SearchSerie(Model): + slug: str + name: str + description: str | None + start_air: date | None + end_air: date | None + poster: str + original_language: Language | None + external_id: dict[str, MetadataId] diff --git a/scanner/scanner/models/show.py b/scanner/scanner/models/show.py deleted file mode 100644 index df9abba4..00000000 --- a/scanner/scanner/models/show.py +++ /dev/null @@ -1,67 +0,0 @@ -from dataclasses import asdict, dataclass, field -from datetime import date -from typing import Optional -from enum import Enum - -from providers.utils import select_translation, select_image - -from .genre import Genre -from .studio import Studio -from .season import Season -from .metadataid import MetadataID - - -class Status(str, Enum): - UNKNOWN = "unknown" - FINISHED = "finished" - AIRING = "airing" - PLANNED = "planned" - - -@dataclass -class ShowTranslation: - name: str - tagline: Optional[str] = None - tags: list[str] = field(default_factory=list) - overview: Optional[str] = None - - posters: list[str] = field(default_factory=list) - logos: list[str] = field(default_factory=list) - trailers: list[str] = field(default_factory=list) - thumbnails: list[str] = field(default_factory=list) - - -@dataclass -class Show: - original_language: Optional[str] - aliases: list[str] - start_air: Optional[date | int] - end_air: Optional[date | int] - status: Status - rating: Optional[int] - studios: list[Studio] - genres: list[Genre] - seasons: list[Season] - # TODO: handle staff - # staff: list[Staff] - external_id: dict[str, MetadataID] - - translations: dict[str, ShowTranslation] = field(default_factory=dict) - # The title of this show according to it's filename (None only for ease of use in providers) - file_title: Optional[str] = None - - def to_kyoo(self): - trans = select_translation(self) or ShowTranslation(name=self.file_title or "") - return { - **asdict(self), - **asdict(trans), - "rating": self.rating or 0, - "studio": next((x.to_kyoo() for x in self.studios), None), - "seasons": None, - "poster": select_image(self, "posters"), - "thumbnail": select_image(self, "thumbnails"), - "logo": select_image(self, "logos"), - "trailer": select_image(self, "trailers"), - "genres": [x.to_kyoo() for x in self.genres], - "file_title": None, - } diff --git a/scanner/scanner/providers/composite.py b/scanner/scanner/providers/composite.py index e8c5ff41..a1da6c78 100644 --- a/scanner/scanner/providers/composite.py +++ b/scanner/scanner/providers/composite.py @@ -38,7 +38,9 @@ class CompositeProvider(Provider): raise ProviderError( f"Couldn't find a movie with title {title}. (year: {year}" ) - ret = await self.get_movie(search[0].external_id) + ret = await self.get_movie( + {k: v.data_id for k, v in search[0].external_id.items()} + ) if not ret: raise ValueError() return ret @@ -68,4 +70,9 @@ class CompositeProvider(Provider): raise ProviderError( f"Couldn't find a serie with title {title}. (year: {year}" ) - return await self.get_serie(search[0].external_id) + ret = await self.get_serie( + {k: v.data_id for k, v in search[0].external_id.items()} + ) + if not ret: + raise ValueError() + return ret diff --git a/scanner/scanner/requests.py b/scanner/scanner/requests.py index 756a4fdb..76545fdf 100644 --- a/scanner/scanner/requests.py +++ b/scanner/scanner/requests.py @@ -4,12 +4,12 @@ from typing import Literal from .client import KyooClient from .models.videos import Guess -from .utils import Model from .providers.composite import CompositeProvider +from .utils import Model class Request(Model): - kind: Literal["episode"] | Literal["movie"] + kind: Literal["episode", "movie"] title: str year: int | None external_id: dict[str, str] @@ -39,7 +39,9 @@ class RequestProcessor: request: Request = ... if request.kind == "movie": - movie = await providers.get_movie(request.title, request.year, request.external_id) + movie = await providers.get_movie( + request.title, request.year, request.external_id + ) movie.videos = request.videos await self._client.create_movie(movie) else: