diff --git a/api/src/db/schema/entries.ts b/api/src/db/schema/entries.ts index 99856210..b1bf583b 100644 --- a/api/src/db/schema/entries.ts +++ b/api/src/db/schema/entries.ts @@ -80,7 +80,9 @@ export const entriesTranslation = schema.table( language: language().notNull(), name: text(), description: text(), + // those two are only used if kind === "movie" tagline: text(), + poster: image(), }, (t) => [primaryKey({ columns: [t.pk, t.language] })], ); diff --git a/api/src/models/entry.ts b/api/src/models/entry.ts deleted file mode 100644 index fb818235..00000000 --- a/api/src/models/entry.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { t } from "elysia"; -import { Image } from "./utils/image"; -import { ExternalId, EpisodeId } from "./utils/external-id"; -import { comment } from "../utils"; -import { madeInAbyss, registerExamples } from "./examples"; - -const BaseEntry = t.Object({ - id: t.String({ format: "uuid" }), - slug: t.String(), - name: t.Nullable(t.String()), - description: t.Nullable(t.String()), - airDate: t.Nullable(t.String({ format: "data" })), - runtime: t.Nullable( - t.Number({ minimum: 0, description: "Runtime of the episode in minutes" }), - ), - thumbnail: t.Nullable(Image), - - createdAt: t.String({ format: "date-time" }), - nextRefresh: t.String({ format: "date-time" }), -}); - -export const Episode = t.Intersect([ - BaseEntry, - t.Object({ - kind: t.Literal("episode"), - order: t.Number({ minimum: 1, description: "Absolute playback order." }), - seasonNumber: t.Number(), - episodeNumber: t.Number(), - externalId: EpisodeId, - }), -]); -export type Episode = typeof Episode.static; - -export const MovieEntry = t.Intersect( - [ - t.Omit(BaseEntry, ["thumbnail"]), - t.Object({ - kind: t.Literal("movie"), - order: t.Number({ - minimum: 1, - description: "Absolute playback order. Can be mixed with episodes.", - }), - tagline: t.String(), - poster: BaseEntry.properties.thumbnail, - externalId: ExternalId, - }), - ], - { - description: comment` - If a movie is part of a serie (watching the movie require context from the serie & - the next episode of the serie require you to have seen the movie to understand it.) - `, - }, -); -export type MovieEntry = typeof MovieEntry.static; - -export const Special = t.Intersect( - [ - BaseEntry, - t.Object({ - kind: t.Literal("special"), - order: t.Number({ - minimum: 1, - description: "Absolute playback order. Can be mixed with episodes.", - }), - number: t.Number({ minimum: 1 }), - externalId: EpisodeId, - }), - ], - { - description: comment` - A special is either an OAV episode (side story & co) or an important episode that was released standalone - (outside of a season.) - `, - }, -); -export type Special = typeof Special.static; - -export const ExtraType = t.UnionEnum([ - "other", - "trailers", - "interview", - "behind-the-scenes", - "deleted-scenes", - "bloopers", -]); -export type ExtraType = typeof ExtraType.static; - -export const Extra = t.Intersect( - [ - BaseEntry, - t.Object({ - kind: ExtraType, - // not sure about this id type - externalId: EpisodeId, - }), - ], - { - description: comment` - An extra can be a beyond-the-scene, short-episodes or anything that is in a different format & not required - in the main story plot. - `, - }, -); -export type Extra = typeof Extra.static; - -export const UnknownEntry = t.Intersect( - [ - t.Omit(BaseEntry, ["airDate", "description"]), - t.Object({ - kind: t.Literal("unknown"), - }), - ], - { - description: comment` - A video not releated to any series or movie. This can be due to a matching error but it can be a youtube - video or any other video content. - `, - }, -); -export type UnknownEntry = typeof UnknownEntry.static; - -export const Entry = t.Union([Episode, MovieEntry, Special]); -export type Entry = typeof Entry.static; - -registerExamples( - Episode, - ...madeInAbyss.entries.filter((x) => x.kind === "episode"), -); -registerExamples( - MovieEntry, - ...madeInAbyss.entries.filter((x) => x.kind === "movie"), -); -registerExamples( - Special, - ...madeInAbyss.entries.filter((x) => x.kind === "special"), -); -registerExamples(Extra, ...madeInAbyss.extras); diff --git a/api/src/models/entry/base-entry.ts b/api/src/models/entry/base-entry.ts new file mode 100644 index 00000000..dc7a880c --- /dev/null +++ b/api/src/models/entry/base-entry.ts @@ -0,0 +1,18 @@ +import { t } from "elysia"; +import { Image } from "../utils/image"; + +export const BaseEntry = t.Object({ + airDate: t.Nullable(t.String({ format: "data" })), + runtime: t.Nullable( + t.Number({ minimum: 0, description: "Runtime of the episode in minutes" }), + ), + thumbnail: t.Nullable(Image), + + createdAt: t.String({ format: "date-time" }), + nextRefresh: t.String({ format: "date-time" }), +}); + +export const EntryTranslation = t.Object({ + name: t.Nullable(t.String()), + description: t.Nullable(t.String()), +}); diff --git a/api/src/models/entry/episode.ts b/api/src/models/entry/episode.ts new file mode 100644 index 00000000..d4db26f4 --- /dev/null +++ b/api/src/models/entry/episode.ts @@ -0,0 +1,18 @@ +import { t } from "elysia"; +import { BaseEntry, EntryTranslation } from "./base-entry"; +import { EpisodeId } from "../utils/external-id"; +import { Resource } from "../utils/resource"; + +export const BaseEpisode = t.Intersect([ + BaseEntry, + t.Object({ + kind: t.Literal("episode"), + order: t.Number({ minimum: 1, description: "Absolute playback order." }), + seasonNumber: t.Number(), + episodeNumber: t.Number(), + externalId: EpisodeId, + }), +]); + +export const Episode = t.Intersect([Resource, BaseEpisode, EntryTranslation]); +export type Episode = typeof Episode.static; diff --git a/api/src/models/entry/extra.ts b/api/src/models/entry/extra.ts new file mode 100644 index 00000000..b26f8a6f --- /dev/null +++ b/api/src/models/entry/extra.ts @@ -0,0 +1,37 @@ +import { t } from "elysia"; +import { BaseEntry, EntryTranslation } from "./base-entry"; +import { EpisodeId } from "../utils/external-id"; +import { comment } from "../../utils"; +import { Resource } from "../utils/resource"; + +export const ExtraType = t.UnionEnum([ + "other", + "trailers", + "interview", + "behind-the-scenes", + "deleted-scenes", + "bloopers", +]); +export type ExtraType = typeof ExtraType.static; + +export const BaseExtra = t.Intersect( + [ + BaseEntry, + t.Object({ + kind: ExtraType, + // not sure about this id type + externalId: EpisodeId, + }), + ], + { + description: comment` + An extra can be a beyond-the-scene, short-episodes or anything that is in a different format & not required + in the main story plot. + `, + }, +); + +export const Extra = t.Intersect([Resource, BaseExtra, EntryTranslation]); +export type Extra = typeof Extra.static; + + diff --git a/api/src/models/entry/index.ts b/api/src/models/entry/index.ts new file mode 100644 index 00000000..1df97a0b --- /dev/null +++ b/api/src/models/entry/index.ts @@ -0,0 +1,11 @@ +import { t } from "elysia"; +import { Episode, MovieEntry, Special } from "../entry"; + +export const Entry = t.Union([Episode, MovieEntry, Special]); +export type Entry = typeof Entry.static; + +export * from "./episode"; +export * from "./movie-entry"; +export * from "./special"; +export * from "./extra"; +export * from "./unknown-entry"; diff --git a/api/src/models/entry/movie-entry.ts b/api/src/models/entry/movie-entry.ts new file mode 100644 index 00000000..205c88ef --- /dev/null +++ b/api/src/models/entry/movie-entry.ts @@ -0,0 +1,41 @@ +import { t } from "elysia"; +import { comment } from "../../utils"; +import { ExternalId } from "../utils/external-id"; +import { Image } from "../utils/image"; +import { Resource } from "../utils/resource"; +import { BaseEntry, EntryTranslation } from "./base-entry"; + +export const BaseMovieEntry = t.Intersect( + [ + t.Omit(BaseEntry, ["thumbnail"]), + t.Object({ + kind: t.Literal("movie"), + order: t.Number({ + minimum: 1, + description: "Absolute playback order. Can be mixed with episodes.", + }), + externalId: ExternalId, + }), + ], + { + description: comment` + If a movie is part of a serie (watching the movie require context from the serie & + the next episode of the serie require you to have seen the movie to understand it.) + `, + }, +); + +export const MovieEntryTranslation = t.Intersect([ + EntryTranslation, + t.Object({ + tagline: t.Nullable(t.String()), + thumbnail: t.Nullable(Image), + }), +]); + +export const MovieEntry = t.Intersect([ + Resource, + BaseMovieEntry, + MovieEntryTranslation, +]); +export type MovieEntry = typeof MovieEntry.static; diff --git a/api/src/models/entry/special.ts b/api/src/models/entry/special.ts new file mode 100644 index 00000000..bb1898d2 --- /dev/null +++ b/api/src/models/entry/special.ts @@ -0,0 +1,29 @@ +import { t } from "elysia"; +import { comment } from "../../utils"; +import { EpisodeId } from "../utils/external-id"; +import { Resource } from "../utils/resource"; +import { BaseEntry, EntryTranslation } from "./base-entry"; + +export const BaseSpecial = t.Intersect( + [ + BaseEntry, + t.Object({ + kind: t.Literal("special"), + order: t.Number({ + minimum: 1, + description: "Absolute playback order. Can be mixed with episodes.", + }), + number: t.Number({ minimum: 1 }), + externalId: EpisodeId, + }), + ], + { + description: comment` + A special is either an OAV episode (side story & co) or an important episode that was released standalone + (outside of a season.) + `, + }, +); + +export const Special = t.Intersect([Resource, BaseSpecial, EntryTranslation]); +export type Special = typeof Special.static; diff --git a/api/src/models/entry/unknown-entry.ts b/api/src/models/entry/unknown-entry.ts new file mode 100644 index 00000000..e60d224d --- /dev/null +++ b/api/src/models/entry/unknown-entry.ts @@ -0,0 +1,30 @@ +import { t } from "elysia"; +import { comment } from "../../utils"; +import { Resource } from "../utils/resource"; +import { BaseEntry, EntryTranslation } from "./base-entry"; + +export const BaseUnknownEntry = t.Intersect( + [ + t.Omit(BaseEntry, ["airDate"]), + t.Object({ + kind: t.Literal("unknown"), + }), + ], + { + description: comment` + A video not releated to any series or movie. This can be due to a matching error but it can be a youtube + video or any other video content. + `, + }, +); + +export const UnknownEntryTranslation = t.Omit(EntryTranslation, [ + "description", +]); + +export const UnknownEntry = t.Intersect([ + Resource, + BaseUnknownEntry, + UnknownEntryTranslation, +]); +export type UnknownEntry = typeof UnknownEntry.static;