Add GET /series/{id} (based on movie's get)

This commit is contained in:
Zoe Roux 2025-03-02 18:53:31 +01:00
parent f143511e14
commit 662400da13
No known key found for this signature in database
5 changed files with 168 additions and 103 deletions

View File

@ -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<string>`${shows.startAir}`.as("airDate"),
status: sql<MovieStatus>`${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<boolean>,
},
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<boolean>`(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: {

View File

@ -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 }) => {

View File

@ -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<string>`${shows.startAir}`.as("airDate"),
status: sql<MovieStatus>`${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<boolean>`(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 };
}

View File

@ -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<typeof FullMovie.static>;
export const SeedMovie = t.Intersect([
t.Omit(BaseMovie, ["createdAt", "nextRefresh"]),

View File

@ -61,6 +61,14 @@ export type SerieTranslation = typeof SerieTranslation.static;
export const Serie = t.Intersect([Resource(), SerieTranslation, BaseSerie]);
export type Serie = Prettify<typeof Serie.static>;
export const FullSerie = t.Intersect([
Serie,
t.Object({
translations: t.Optional(TranslationRecord(SerieTranslation)),
}),
]);
export type FullMovie = Prettify<typeof FullSerie.static>;
export const SeedSerie = t.Intersect([
t.Omit(BaseSerie, ["createdAt", "nextRefresh"]),
t.Object({