mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-31 12:14:46 -04:00
Add GET /series/{id} (based on movie's get)
This commit is contained in:
parent
f143511e14
commit
662400da13
@ -1,26 +1,19 @@
|
|||||||
import { type SQL, and, eq, exists, sql } from "drizzle-orm";
|
import { type SQL, and, eq, exists, sql } from "drizzle-orm";
|
||||||
import { Elysia, t } from "elysia";
|
import { Elysia, t } from "elysia";
|
||||||
import { db } from "~/db";
|
import { db } from "~/db";
|
||||||
import { entries, entryVideoJoin, showTranslations, shows } from "~/db/schema";
|
import { shows } from "~/db/schema";
|
||||||
import { sqlarr } from "~/db/utils";
|
|
||||||
import { KError } from "~/models/error";
|
import { KError } from "~/models/error";
|
||||||
import { bubble } from "~/models/examples";
|
import { bubble } from "~/models/examples";
|
||||||
import {
|
import { FullMovie, Movie, MovieTranslation } from "~/models/movie";
|
||||||
FullMovie,
|
|
||||||
Movie,
|
|
||||||
type MovieStatus,
|
|
||||||
MovieTranslation,
|
|
||||||
} from "~/models/movie";
|
|
||||||
import {
|
import {
|
||||||
AcceptLanguage,
|
AcceptLanguage,
|
||||||
Filter,
|
Filter,
|
||||||
Page,
|
Page,
|
||||||
createPage,
|
createPage,
|
||||||
isUuid,
|
|
||||||
processLanguages,
|
processLanguages,
|
||||||
} from "~/models/utils";
|
} from "~/models/utils";
|
||||||
import { desc } from "~/models/utils/descriptions";
|
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"] })
|
export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
|
||||||
.model({
|
.model({
|
||||||
@ -37,108 +30,26 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
|
|||||||
set,
|
set,
|
||||||
}) => {
|
}) => {
|
||||||
const langs = processLanguages(languages);
|
const langs = processLanguages(languages);
|
||||||
|
const ret = await getShow(id, {
|
||||||
const ret = await db.query.shows.findFirst({
|
languages: langs,
|
||||||
columns: {
|
preferOriginal,
|
||||||
kind: false,
|
relations,
|
||||||
startAir: false,
|
filters: eq(shows.kind, "movie"),
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!ret) {
|
if (!ret) {
|
||||||
return error(404, {
|
return error(404, {
|
||||||
status: 404,
|
status: 404,
|
||||||
message: "Movie not found",
|
message: "Movie not found",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const translation = ret.selectedTranslation[0];
|
if (!ret.language) {
|
||||||
if (!translation) {
|
|
||||||
return error(422, {
|
return error(422, {
|
||||||
status: 422,
|
status: 422,
|
||||||
message: "Accept-Language header could not be satisfied.",
|
message: "Accept-Language header could not be satisfied.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
set.headers["content-language"] = translation.language;
|
set.headers["content-language"] = ret.language;
|
||||||
const ot = ret.originalTranslation;
|
return ret.show;
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
detail: {
|
detail: {
|
||||||
|
@ -3,7 +3,8 @@ import { Elysia, t } from "elysia";
|
|||||||
import { db } from "~/db";
|
import { db } from "~/db";
|
||||||
import { shows } from "~/db/schema";
|
import { shows } from "~/db/schema";
|
||||||
import { KError } from "~/models/error";
|
import { KError } from "~/models/error";
|
||||||
import { Serie, SerieTranslation } from "~/models/serie";
|
import { madeInAbyss } from "~/models/examples";
|
||||||
|
import { FullSerie, Serie, SerieTranslation } from "~/models/serie";
|
||||||
import {
|
import {
|
||||||
AcceptLanguage,
|
AcceptLanguage,
|
||||||
Filter,
|
Filter,
|
||||||
@ -12,13 +13,76 @@ import {
|
|||||||
processLanguages,
|
processLanguages,
|
||||||
} from "~/models/utils";
|
} from "~/models/utils";
|
||||||
import { desc } from "~/models/utils/descriptions";
|
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"] })
|
export const series = new Elysia({ prefix: "/series", tags: ["series"] })
|
||||||
.model({
|
.model({
|
||||||
serie: Serie,
|
serie: Serie,
|
||||||
"serie-translation": SerieTranslation,
|
"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(
|
.get(
|
||||||
"random",
|
"random",
|
||||||
async ({ error, redirect }) => {
|
async ({ error, redirect }) => {
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
Genre,
|
Genre,
|
||||||
type Image,
|
type Image,
|
||||||
Sort,
|
Sort,
|
||||||
|
isUuid,
|
||||||
keysetPaginate,
|
keysetPaginate,
|
||||||
sortToSql,
|
sortToSql,
|
||||||
} from "~/models/utils";
|
} from "~/models/utils";
|
||||||
@ -117,3 +118,84 @@ export async function getShows({
|
|||||||
)
|
)
|
||||||
.limit(limit);
|
.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 };
|
||||||
|
}
|
||||||
|
@ -68,7 +68,7 @@ export const FullMovie = t.Intersect([
|
|||||||
videos: t.Optional(t.Array(Video)),
|
videos: t.Optional(t.Array(Video)),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
export type FullMovie = typeof FullMovie.static;
|
export type FullMovie = Prettify<typeof FullMovie.static>;
|
||||||
|
|
||||||
export const SeedMovie = t.Intersect([
|
export const SeedMovie = t.Intersect([
|
||||||
t.Omit(BaseMovie, ["createdAt", "nextRefresh"]),
|
t.Omit(BaseMovie, ["createdAt", "nextRefresh"]),
|
||||||
|
@ -61,6 +61,14 @@ export type SerieTranslation = typeof SerieTranslation.static;
|
|||||||
export const Serie = t.Intersect([Resource(), SerieTranslation, BaseSerie]);
|
export const Serie = t.Intersect([Resource(), SerieTranslation, BaseSerie]);
|
||||||
export type Serie = Prettify<typeof Serie.static>;
|
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([
|
export const SeedSerie = t.Intersect([
|
||||||
t.Omit(BaseSerie, ["createdAt", "nextRefresh"]),
|
t.Omit(BaseSerie, ["createdAt", "nextRefresh"]),
|
||||||
t.Object({
|
t.Object({
|
||||||
|
Loading…
x
Reference in New Issue
Block a user