diff --git a/api/src/controllers/shows/collections.ts b/api/src/controllers/shows/collections.ts new file mode 100644 index 00000000..2123dd7b --- /dev/null +++ b/api/src/controllers/shows/collections.ts @@ -0,0 +1,171 @@ +import { and, eq, sql } from "drizzle-orm"; +import { Elysia, t } from "elysia"; +import { db } from "~/db"; +import { shows } from "~/db/schema"; +import { + Collection, + CollectionTranslation, + FullCollection, +} from "~/models/collections"; +import { KError } from "~/models/error"; +import { duneCollection } from "~/models/examples"; +import { + AcceptLanguage, + Filter, + Page, + createPage, + processLanguages, +} from "~/models/utils"; +import { desc } from "~/models/utils/descriptions"; +import { getShow, getShows, showFilters, showSort } from "./shows"; + +export const collections = new Elysia({ + prefix: "/collections", + tags: ["collections"], +}) + .model({ + collection: Collection, + "collection-translation": CollectionTranslation, + }) + .get( + "/:id", + async ({ + params: { id }, + headers: { "accept-language": languages }, + query: { preferOriginal, with: relations }, + error, + set, + }) => { + const langs = processLanguages(languages); + const ret = await getShow(id, { + languages: langs, + preferOriginal, + relations, + filters: eq(shows.kind, "collection"), + }); + if (!ret) { + return error(404, { + status: 404, + message: "Collection not found", + }); + } + if (!ret.language) { + return error(422, { + status: 422, + message: "Accept-Language header could not be satisfied.", + }); + } + set.headers["content-language"] = ret.language; + return ret.show; + }, + { + detail: { + description: "Get a collection by id or slug", + }, + params: t.Object({ + id: t.String({ + description: "The id or slug of the collection to retrieve.", + example: duneCollection.slug, + }), + }), + query: t.Object({ + preferOriginal: t.Optional( + t.Boolean({ description: desc.preferOriginal }), + ), + with: t.Array(t.UnionEnum(["translations"]), { + default: [], + description: "Include related resources in the response.", + }), + }), + headers: t.Object({ + "accept-language": AcceptLanguage(), + }), + response: { + 200: { ...FullCollection, description: "Found" }, + 404: { + ...KError, + description: "No collection found with the given id or slug.", + }, + 422: KError, + }, + }, + ) + .get( + "random", + async ({ error, redirect }) => { + const [serie] = await db + .select({ id: shows.id }) + .from(shows) + .where(eq(shows.kind, "collection")) + .orderBy(sql`random()`) + .limit(1); + if (!serie) + return error(404, { + status: 404, + message: "No collection in the database.", + }); + return redirect(`/collections/${serie.id}`); + }, + { + detail: { + description: "Get a random collection", + }, + response: { + 302: t.Void({ + description: + "Redirected to the [/collections/{id}](#tag/collections/GET/collections/{id}) route.", + }), + 404: { + ...KError, + description: "No collections in the database.", + }, + }, + }, + ) + .get( + "", + async ({ + query: { limit, after, query, sort, filter, preferOriginal }, + headers: { "accept-language": languages }, + request: { url }, + }) => { + const langs = processLanguages(languages); + const items = await getShows({ + limit, + after, + query, + sort, + filter: and(eq(shows.kind, "collection"), filter), + languages: langs, + preferOriginal, + }); + return createPage(items, { url, sort, limit }); + }, + { + detail: { description: "Get all collections" }, + query: t.Object({ + sort: showSort, + filter: t.Optional(Filter({ def: showFilters })), + query: t.Optional(t.String({ description: desc.query })), + limit: t.Integer({ + minimum: 1, + maximum: 250, + default: 50, + description: "Max page size.", + }), + after: t.Optional(t.String({ description: desc.after })), + preferOriginal: t.Optional( + t.Boolean({ + description: desc.preferOriginal, + }), + ), + }), + headers: t.Object({ + "accept-language": AcceptLanguage({ autoFallback: true }), + }), + response: { + 200: Page(Collection), + 422: KError, + }, + }, + ); diff --git a/api/src/elysia.ts b/api/src/elysia.ts index 80ba8ac1..7a86800a 100644 --- a/api/src/elysia.ts +++ b/api/src/elysia.ts @@ -2,10 +2,10 @@ import { Elysia } from "elysia"; import { entriesH } from "./controllers/entries"; import { seasonsH } from "./controllers/seasons"; import { seed } from "./controllers/seed"; +import { collections } from "./controllers/shows/collections"; import { movies } from "./controllers/shows/movies"; import { series } from "./controllers/shows/series"; import { videosH } from "./controllers/videos"; - import type { KError } from "./models/error"; export const base = new Elysia({ name: "base" }) @@ -42,6 +42,7 @@ export const app = new Elysia() .use(base) .use(movies) .use(series) + .use(collections) .use(entriesH) .use(seasonsH) .use(videosH) diff --git a/api/src/index.ts b/api/src/index.ts index 7f2d13ab..c125adf9 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -50,6 +50,7 @@ app tags: [ { name: "movies", description: "Routes about movies" }, { name: "series", description: "Routes about series" }, + { name: "collections", description: "Routes about collections" }, { name: "videos", description: comment` diff --git a/api/src/models/collections.ts b/api/src/models/collections.ts index db393f3e..e82edce8 100644 --- a/api/src/models/collections.ts +++ b/api/src/models/collections.ts @@ -5,6 +5,7 @@ import { ExternalId, Genre, Image, + Language, Resource, SeedImage, TranslationRecord, @@ -25,6 +26,11 @@ const BaseCollection = t.Object({ descrpition: "Date of the last item of the collection", }), ), + originalLanguage: t.Nullable( + Language({ + description: "The language code this movie was made in.", + }), + ), createdAt: t.String({ format: "date-time" }), nextRefresh: t.String({ format: "date-time" }), @@ -52,6 +58,14 @@ export const Collection = t.Intersect([ ]); export type Collection = Prettify; +export const FullCollection = t.Intersect([ + Collection, + t.Object({ + translations: t.Optional(TranslationRecord(CollectionTranslation)), + }), +]); +export type FullCollection = Prettify; + export const SeedCollection = t.Intersect([ t.Omit(BaseCollection, ["startAir", "endAir", "createdAt", "nextRefresh"]), t.Object({ diff --git a/api/src/models/examples/dune-collection.ts b/api/src/models/examples/dune-collection.ts index c0b636aa..4c249975 100644 --- a/api/src/models/examples/dune-collection.ts +++ b/api/src/models/examples/dune-collection.ts @@ -18,6 +18,7 @@ export const duneCollection: SeedCollection = { logo: "https://image.tmdb.org/t/p/original/5nDsd3u1c6kDphbtIqkHseLg7HL.png", }, }, + originalLanguage: "en", genres: ["adventure", "science-fiction"], rating: 80, externalId: {