diff --git a/api/src/controllers/shows/collections.ts b/api/src/controllers/shows/collections.ts index f378158b..24f8a394 100644 --- a/api/src/controllers/shows/collections.ts +++ b/api/src/controllers/shows/collections.ts @@ -96,7 +96,7 @@ export const collections = new Elysia({ "random", async ({ error, redirect }) => { const [serie] = await db - .select({ id: shows.id }) + .select({ slug: shows.slug }) .from(shows) .where(eq(shows.kind, "collection")) .orderBy(sql`random()`) @@ -106,7 +106,7 @@ export const collections = new Elysia({ status: 404, message: "No collection in the database.", }); - return redirect(`/collections/${serie.id}`); + return redirect(`/collections/${serie.slug}`); }, { detail: { diff --git a/api/src/controllers/shows/logic.ts b/api/src/controllers/shows/logic.ts index b13bd51c..5b37f15d 100644 --- a/api/src/controllers/shows/logic.ts +++ b/api/src/controllers/shows/logic.ts @@ -86,6 +86,7 @@ export async function getShows({ // movie columns (status is only a typescript hint) status: sql`${shows.status}`, airDate: shows.startAir, + kind: sql`${shows.kind}`, poster: sql`coalesce(${showTranslations.poster}, ${poster})`, thumbnail: sql`coalesce(${showTranslations.thumbnail}, ${thumbnail})`, diff --git a/api/src/controllers/shows/movies.ts b/api/src/controllers/shows/movies.ts index eb011711..7f023f30 100644 --- a/api/src/controllers/shows/movies.ts +++ b/api/src/controllers/shows/movies.ts @@ -1,4 +1,4 @@ -import { type SQL, and, eq, exists, sql } from "drizzle-orm"; +import { and, eq, sql } from "drizzle-orm"; import { Elysia, t } from "elysia"; import { db } from "~/db"; import { shows } from "~/db/schema"; @@ -87,7 +87,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) "random", async ({ error, redirect }) => { const [movie] = await db - .select({ id: shows.id }) + .select({ slug: shows.slug }) .from(shows) .where(eq(shows.kind, "movie")) .orderBy(sql`random()`) @@ -97,7 +97,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) status: 404, message: "No movies in the database.", }); - return redirect(`/movies/${movie.id}`); + return redirect(`/movies/${movie.slug}`); }, { detail: { diff --git a/api/src/controllers/shows/series.ts b/api/src/controllers/shows/series.ts index f903621e..3c3a371b 100644 --- a/api/src/controllers/shows/series.ts +++ b/api/src/controllers/shows/series.ts @@ -87,7 +87,7 @@ export const series = new Elysia({ prefix: "/series", tags: ["series"] }) "random", async ({ error, redirect }) => { const [serie] = await db - .select({ id: shows.id }) + .select({ slug: shows.slug }) .from(shows) .where(eq(shows.kind, "serie")) .orderBy(sql`random()`) @@ -97,7 +97,7 @@ export const series = new Elysia({ prefix: "/series", tags: ["series"] }) status: 404, message: "No series in the database.", }); - return redirect(`/series/${serie.id}`); + return redirect(`/series/${serie.slug}`); }, { detail: { diff --git a/api/src/controllers/shows/shows.ts b/api/src/controllers/shows/shows.ts new file mode 100644 index 00000000..ee0a598f --- /dev/null +++ b/api/src/controllers/shows/shows.ts @@ -0,0 +1,119 @@ +import { and, isNull, sql } from "drizzle-orm"; +import { Elysia, t } from "elysia"; +import { db } from "~/db"; +import { shows } from "~/db/schema"; +import { Collection } from "~/models/collections"; +import { KError } from "~/models/error"; +import { Movie } from "~/models/movie"; +import { Serie } from "~/models/serie"; +import { + AcceptLanguage, + Filter, + Page, + createPage, + processLanguages, +} from "~/models/utils"; +import { desc } from "~/models/utils/descriptions"; +import { getShows, showFilters, showSort } from "./logic"; + +const Show = t.Union([Movie, Serie, Collection]); + +export const showsH = new Elysia({ prefix: "/shows", tags: ["shows"] }) + .model({ + show: Show, + }) + .get( + "random", + async ({ error, redirect }) => { + const [show] = await db + .select({ kind: shows.kind, slug: shows.slug }) + .from(shows) + .orderBy(sql`random()`) + .limit(1); + if (!show) + return error(404, { + status: 404, + message: "No shows in the database.", + }); + return redirect(`/${show.kind}s/${show.slug}`); + }, + { + detail: { + description: "Get a random movie/serie/collection", + }, + response: { + 302: t.Void({ + description: "Redirected to the appropriate get endpoint.", + }), + 404: { + ...KError, + description: "No show in the database.", + }, + }, + }, + ) + .get( + "", + async ({ + query: { + limit, + after, + query, + sort, + filter, + preferOriginal, + ignoreInCollection, + }, + headers: { "accept-language": languages }, + request: { url }, + }) => { + const langs = processLanguages(languages); + const items = await getShows({ + limit, + after, + query, + sort, + filter: and( + ignoreInCollection ? isNull(shows.collectionPk) : undefined, + filter, + ), + languages: langs, + preferOriginal, + }); + return createPage(items, { url, sort, limit }); + }, + { + detail: { description: "Get all movies/series/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, + }), + ), + ignoreInCollection: t.Optional( + t.Boolean({ + description: + "If a movie or serie is part of collection, don't return it.", + default: true, + }), + ), + }), + headers: t.Object({ + "accept-language": AcceptLanguage({ autoFallback: true }), + }), + response: { + 200: Page(Show), + 422: KError, + }, + }, + ); diff --git a/api/src/elysia.ts b/api/src/elysia.ts index 7a86800a..3dd80ac0 100644 --- a/api/src/elysia.ts +++ b/api/src/elysia.ts @@ -5,6 +5,7 @@ import { seed } from "./controllers/seed"; import { collections } from "./controllers/shows/collections"; import { movies } from "./controllers/shows/movies"; import { series } from "./controllers/shows/series"; +import { showsH } from "./controllers/shows/shows"; import { videosH } from "./controllers/videos"; import type { KError } from "./models/error"; @@ -40,6 +41,7 @@ export const base = new Elysia({ name: "base" }) export const app = new Elysia() .use(base) + .use(showsH) .use(movies) .use(series) .use(collections) diff --git a/api/src/index.ts b/api/src/index.ts index c125adf9..9a38beea 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -48,6 +48,11 @@ app }, ], tags: [ + { + name: "shows", + description: + "Routes to list movies, series & collections at the same time", + }, { name: "movies", description: "Routes about movies" }, { name: "series", description: "Routes about series" }, { name: "collections", description: "Routes about collections" }, diff --git a/api/src/models/collections.ts b/api/src/models/collections.ts index e82edce8..99958154 100644 --- a/api/src/models/collections.ts +++ b/api/src/models/collections.ts @@ -12,6 +12,7 @@ import { } from "./utils"; const BaseCollection = t.Object({ + kind: t.Literal("collection"), genres: t.Array(Genre), rating: t.Nullable(t.Integer({ minimum: 0, maximum: 100 })), startAir: t.Nullable( @@ -67,7 +68,13 @@ export const FullCollection = t.Intersect([ export type FullCollection = Prettify; export const SeedCollection = t.Intersect([ - t.Omit(BaseCollection, ["startAir", "endAir", "createdAt", "nextRefresh"]), + t.Omit(BaseCollection, [ + "kind", + "startAir", + "endAir", + "createdAt", + "nextRefresh", + ]), t.Object({ slug: t.String({ format: "slug" }), translations: TranslationRecord( diff --git a/api/src/models/movie.ts b/api/src/models/movie.ts index da62b42a..aaae338f 100644 --- a/api/src/models/movie.ts +++ b/api/src/models/movie.ts @@ -18,6 +18,7 @@ export const MovieStatus = t.UnionEnum(["unknown", "finished", "planned"]); export type MovieStatus = typeof MovieStatus.static; const BaseMovie = t.Object({ + kind: t.Literal("movie"), genres: t.Array(Genre), rating: t.Nullable(t.Integer({ minimum: 0, maximum: 100 })), status: MovieStatus, @@ -71,7 +72,7 @@ export const FullMovie = t.Intersect([ export type FullMovie = Prettify; export const SeedMovie = t.Intersect([ - t.Omit(BaseMovie, ["createdAt", "nextRefresh"]), + t.Omit(BaseMovie, ["kind", "createdAt", "nextRefresh"]), t.Object({ slug: t.String({ format: "slug", examples: ["bubble"] }), translations: TranslationRecord( diff --git a/api/src/models/serie.ts b/api/src/models/serie.ts index 029286de..cc8ff78e 100644 --- a/api/src/models/serie.ts +++ b/api/src/models/serie.ts @@ -19,6 +19,7 @@ export const SerieStatus = t.UnionEnum([ export type SerieStatus = typeof SerieStatus.static; export const BaseSerie = t.Object({ + kind: t.Literal("serie"), genres: t.Array(Genre), rating: t.Nullable(t.Integer({ minimum: 0, maximum: 100 })), status: SerieStatus, @@ -70,7 +71,7 @@ export const FullSerie = t.Intersect([ export type FullMovie = Prettify; export const SeedSerie = t.Intersect([ - t.Omit(BaseSerie, ["createdAt", "nextRefresh"]), + t.Omit(BaseSerie, ["kind", "createdAt", "nextRefresh"]), t.Object({ slug: t.String({ format: "slug" }), translations: TranslationRecord(