From 662400da13d4ff7c443e6270626c25fe1ab77871 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 2 Mar 2025 18:53:31 +0100 Subject: [PATCH] Add GET /series/{id} (based on movie's get) --- api/src/controllers/shows/movies.ts | 111 +++------------------------- api/src/controllers/shows/series.ts | 68 ++++++++++++++++- api/src/controllers/shows/shows.ts | 82 ++++++++++++++++++++ api/src/models/movie.ts | 2 +- api/src/models/serie.ts | 8 ++ 5 files changed, 168 insertions(+), 103 deletions(-) diff --git a/api/src/controllers/shows/movies.ts b/api/src/controllers/shows/movies.ts index 4ad04fb9..10bbbec4 100644 --- a/api/src/controllers/shows/movies.ts +++ b/api/src/controllers/shows/movies.ts @@ -1,26 +1,19 @@ import { type SQL, and, eq, exists, sql } from "drizzle-orm"; import { Elysia, t } from "elysia"; import { db } from "~/db"; -import { entries, entryVideoJoin, showTranslations, shows } from "~/db/schema"; -import { sqlarr } from "~/db/utils"; +import { shows } from "~/db/schema"; import { KError } from "~/models/error"; import { bubble } from "~/models/examples"; -import { - FullMovie, - Movie, - type MovieStatus, - MovieTranslation, -} from "~/models/movie"; +import { FullMovie, Movie, MovieTranslation } from "~/models/movie"; import { AcceptLanguage, Filter, Page, createPage, - isUuid, processLanguages, } from "~/models/utils"; import { desc } from "~/models/utils/descriptions"; -import { getShows, showFilters, showSort } from "./shows"; +import { getShow, getShows, showFilters, showSort } from "./shows"; export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) .model({ @@ -37,108 +30,26 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) set, }) => { const langs = processLanguages(languages); - - const ret = await db.query.shows.findFirst({ - columns: { - kind: false, - startAir: false, - endAir: false, - }, - extras: { - airDate: sql`${shows.startAir}`.as("airDate"), - status: sql`${shows.status}`.as("status"), - isAvailable: exists( - db - .select() - .from(entries) - .where( - and( - eq(shows.pk, entries.showPk), - exists( - db - .select() - .from(entryVideoJoin) - .where(eq(entries.pk, entryVideoJoin.entry)), - ), - ), - ), - ).as("isAvailable") as SQL.Aliased, - }, - where: and( - eq(shows.kind, "movie"), - isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id), - ), - with: { - selectedTranslation: { - columns: { - pk: false, - }, - where: !langs.includes("*") - ? eq(showTranslations.language, sql`any(${sqlarr(langs)})`) - : undefined, - orderBy: [ - sql`array_position(${sqlarr(langs)}, ${showTranslations.language})`, - ], - limit: 1, - }, - originalTranslation: { - columns: { - poster: true, - thumbnail: true, - banner: true, - logo: true, - }, - extras: { - // TODO: also fallback on user settings (that's why i made a select here) - preferOriginal: - sql`(select coalesce(${preferOriginal ?? null}::boolean, false))`.as( - "preferOriginal", - ), - }, - }, - ...(relations.includes("translations") && { - translations: { - columns: { - pk: false, - }, - }, - }), - }, + const ret = await getShow(id, { + languages: langs, + preferOriginal, + relations, + filters: eq(shows.kind, "movie"), }); - if (!ret) { return error(404, { status: 404, message: "Movie not found", }); } - const translation = ret.selectedTranslation[0]; - if (!translation) { + if (!ret.language) { return error(422, { status: 422, message: "Accept-Language header could not be satisfied.", }); } - set.headers["content-language"] = translation.language; - const ot = ret.originalTranslation; - return { - ...ret, - ...translation, - ...(ot?.preferOriginal && { - ...(ot.poster && { poster: ot.poster }), - ...(ot.thumbnail && { thumbnail: ot.thumbnail }), - ...(ot.banner && { banner: ot.banner }), - ...(ot.logo && { logo: ot.logo }), - }), - ...(ret.translations && { - translations: Object.fromEntries( - ret.translations.map( - ({ language, ...translation }) => - [language, translation] as const, - ), - ), - }), - }; + set.headers["content-language"] = ret.language; + return ret.show; }, { detail: { diff --git a/api/src/controllers/shows/series.ts b/api/src/controllers/shows/series.ts index 0d3c8c16..36c854ba 100644 --- a/api/src/controllers/shows/series.ts +++ b/api/src/controllers/shows/series.ts @@ -3,7 +3,8 @@ import { Elysia, t } from "elysia"; import { db } from "~/db"; import { shows } from "~/db/schema"; import { KError } from "~/models/error"; -import { Serie, SerieTranslation } from "~/models/serie"; +import { madeInAbyss } from "~/models/examples"; +import { FullSerie, Serie, SerieTranslation } from "~/models/serie"; import { AcceptLanguage, Filter, @@ -12,13 +13,76 @@ import { processLanguages, } from "~/models/utils"; import { desc } from "~/models/utils/descriptions"; -import { getShows, showFilters, showSort } from "./shows"; +import { getShow, getShows, showFilters, showSort } from "./shows"; export const series = new Elysia({ prefix: "/series", tags: ["series"] }) .model({ serie: Serie, "serie-translation": SerieTranslation, }) + .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, "serie"), + }); + if (!ret) { + return error(404, { + status: 404, + message: "Movie 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 serie by id or slug", + }, + params: t.Object({ + id: t.String({ + description: "The id or slug of the serie to retrieve.", + example: madeInAbyss.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: { ...FullSerie, description: "Found" }, + 404: { + ...KError, + description: "No movie found with the given id or slug.", + }, + 422: KError, + }, + }, + ) .get( "random", async ({ error, redirect }) => { diff --git a/api/src/controllers/shows/shows.ts b/api/src/controllers/shows/shows.ts index 44c792a7..b13bd51c 100644 --- a/api/src/controllers/shows/shows.ts +++ b/api/src/controllers/shows/shows.ts @@ -10,6 +10,7 @@ import { Genre, type Image, Sort, + isUuid, keysetPaginate, sortToSql, } from "~/models/utils"; @@ -117,3 +118,84 @@ export async function getShows({ ) .limit(limit); } + +export async function getShow( + id: string, + { + languages, + preferOriginal, + relations, + filters, + }: { + languages: string[]; + preferOriginal: boolean | undefined; + relations: ("translations" | "videos")[]; + filters: SQL | undefined; + }, +) { + const ret = await db.query.shows.findFirst({ + extras: { + airDate: sql`${shows.startAir}`.as("airDate"), + status: sql`${shows.status}`.as("status"), + }, + where: and(isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id), filters), + with: { + selectedTranslation: { + columns: { + pk: false, + }, + where: !languages.includes("*") + ? eq(showTranslations.language, sql`any(${sqlarr(languages)})`) + : undefined, + orderBy: [ + sql`array_position(${sqlarr(languages)}, ${showTranslations.language})`, + ], + limit: 1, + }, + originalTranslation: { + columns: { + poster: true, + thumbnail: true, + banner: true, + logo: true, + }, + extras: { + // TODO: also fallback on user settings (that's why i made a select here) + preferOriginal: + sql`(select coalesce(${preferOriginal ?? null}::boolean, false))`.as( + "preferOriginal", + ), + }, + }, + ...(relations.includes("translations") && { + translations: { + columns: { + pk: false, + }, + }, + }), + }, + }); + if (!ret) return null; + const translation = ret.selectedTranslation[0]; + if (!translation) return { show: null, language: null }; + const ot = ret.originalTranslation; + const show = { + ...ret, + ...translation, + ...(ot?.preferOriginal && { + ...(ot.poster && { poster: ot.poster }), + ...(ot.thumbnail && { thumbnail: ot.thumbnail }), + ...(ot.banner && { banner: ot.banner }), + ...(ot.logo && { logo: ot.logo }), + }), + ...(ret.translations && { + translations: Object.fromEntries( + ret.translations.map( + ({ language, ...translation }) => [language, translation] as const, + ), + ), + }), + }; + return { show, language: translation.language }; +} diff --git a/api/src/models/movie.ts b/api/src/models/movie.ts index 4bed67f3..da62b42a 100644 --- a/api/src/models/movie.ts +++ b/api/src/models/movie.ts @@ -68,7 +68,7 @@ export const FullMovie = t.Intersect([ videos: t.Optional(t.Array(Video)), }), ]); -export type FullMovie = typeof FullMovie.static; +export type FullMovie = Prettify; export const SeedMovie = t.Intersect([ t.Omit(BaseMovie, ["createdAt", "nextRefresh"]), diff --git a/api/src/models/serie.ts b/api/src/models/serie.ts index dafeeefe..029286de 100644 --- a/api/src/models/serie.ts +++ b/api/src/models/serie.ts @@ -61,6 +61,14 @@ export type SerieTranslation = typeof SerieTranslation.static; export const Serie = t.Intersect([Resource(), SerieTranslation, BaseSerie]); export type Serie = Prettify; +export const FullSerie = t.Intersect([ + Serie, + t.Object({ + translations: t.Optional(TranslationRecord(SerieTranslation)), + }), +]); +export type FullMovie = Prettify; + export const SeedSerie = t.Intersect([ t.Omit(BaseSerie, ["createdAt", "nextRefresh"]), t.Object({