mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-11-03 19:17:16 -05:00 
			
		
		
		
	Add GET /series (using the same logic as /movies)
This commit is contained in:
		
							parent
							
								
									24f44de7c0
								
							
						
					
					
						commit
						cc221c560d
					
				@ -2,49 +2,25 @@ 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 { entries, entryVideoJoin, showTranslations, shows } from "~/db/schema";
 | 
				
			||||||
import { getColumns, sqlarr } from "~/db/utils";
 | 
					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,
 | 
						FullMovie,
 | 
				
			||||||
	Movie,
 | 
						Movie,
 | 
				
			||||||
	MovieStatus,
 | 
						type MovieStatus,
 | 
				
			||||||
	MovieTranslation,
 | 
						MovieTranslation,
 | 
				
			||||||
} from "~/models/movie";
 | 
					} from "~/models/movie";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
	AcceptLanguage,
 | 
						AcceptLanguage,
 | 
				
			||||||
	Filter,
 | 
						Filter,
 | 
				
			||||||
	type FilterDef,
 | 
					 | 
				
			||||||
	Genre,
 | 
					 | 
				
			||||||
	type Image,
 | 
					 | 
				
			||||||
	Page,
 | 
						Page,
 | 
				
			||||||
	Sort,
 | 
					 | 
				
			||||||
	createPage,
 | 
						createPage,
 | 
				
			||||||
	isUuid,
 | 
						isUuid,
 | 
				
			||||||
	keysetPaginate,
 | 
					 | 
				
			||||||
	processLanguages,
 | 
						processLanguages,
 | 
				
			||||||
	sortToSql,
 | 
					 | 
				
			||||||
} from "~/models/utils";
 | 
					} from "~/models/utils";
 | 
				
			||||||
import { desc } from "~/models/utils/descriptions";
 | 
					import { desc } from "~/models/utils/descriptions";
 | 
				
			||||||
 | 
					import { getShows, showFilters, showSort } from "./shows";
 | 
				
			||||||
const movieFilters: FilterDef = {
 | 
					 | 
				
			||||||
	genres: {
 | 
					 | 
				
			||||||
		column: shows.genres,
 | 
					 | 
				
			||||||
		type: "enum",
 | 
					 | 
				
			||||||
		values: Genre.enum,
 | 
					 | 
				
			||||||
		isArray: true,
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	rating: { column: shows.rating, type: "int" },
 | 
					 | 
				
			||||||
	status: { column: shows.status, type: "enum", values: MovieStatus.enum },
 | 
					 | 
				
			||||||
	runtime: { column: shows.runtime, type: "float" },
 | 
					 | 
				
			||||||
	airDate: { column: shows.startAir, type: "date" },
 | 
					 | 
				
			||||||
	originalLanguage: { column: shows.originalLanguage, type: "string" },
 | 
					 | 
				
			||||||
	tags: {
 | 
					 | 
				
			||||||
		column: sql.raw(`t.${showTranslations.tags.name}`),
 | 
					 | 
				
			||||||
		type: "string",
 | 
					 | 
				
			||||||
		isArray: true,
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
 | 
					export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
 | 
				
			||||||
	.model({
 | 
						.model({
 | 
				
			||||||
@ -236,85 +212,22 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
 | 
				
			|||||||
			request: { url },
 | 
								request: { url },
 | 
				
			||||||
		}) => {
 | 
							}) => {
 | 
				
			||||||
			const langs = processLanguages(languages);
 | 
								const langs = processLanguages(languages);
 | 
				
			||||||
 | 
								const items = await getShows({
 | 
				
			||||||
			// we keep the pk for after handling. it will be removed by elysia's validators after.
 | 
									limit,
 | 
				
			||||||
			const { kind, startAir, endAir, ...moviesCol } = getColumns(shows);
 | 
									after,
 | 
				
			||||||
 | 
									query,
 | 
				
			||||||
			const transQ = db
 | 
									sort,
 | 
				
			||||||
				.selectDistinctOn([showTranslations.pk])
 | 
									filter: and(eq(shows.kind, "movie"), filter),
 | 
				
			||||||
				.from(showTranslations)
 | 
									languages: langs,
 | 
				
			||||||
				.orderBy(
 | 
									preferOriginal,
 | 
				
			||||||
					showTranslations.pk,
 | 
								});
 | 
				
			||||||
					sql`array_position(${sqlarr(langs)}, ${showTranslations.language})`,
 | 
					 | 
				
			||||||
				)
 | 
					 | 
				
			||||||
				.as("t");
 | 
					 | 
				
			||||||
			const { pk, poster, thumbnail, banner, logo, ...transCol } =
 | 
					 | 
				
			||||||
				getColumns(transQ);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			const videoQ = db
 | 
					 | 
				
			||||||
				.select({ showPk: entries.showPk })
 | 
					 | 
				
			||||||
				.from(entries)
 | 
					 | 
				
			||||||
				.where(
 | 
					 | 
				
			||||||
					exists(
 | 
					 | 
				
			||||||
						db
 | 
					 | 
				
			||||||
							.select()
 | 
					 | 
				
			||||||
							.from(entryVideoJoin)
 | 
					 | 
				
			||||||
							.where(eq(entries.pk, entryVideoJoin.entry)),
 | 
					 | 
				
			||||||
					),
 | 
					 | 
				
			||||||
				)
 | 
					 | 
				
			||||||
				.as("video");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			const items = await db
 | 
					 | 
				
			||||||
				.select({
 | 
					 | 
				
			||||||
					...moviesCol,
 | 
					 | 
				
			||||||
					...transCol,
 | 
					 | 
				
			||||||
					status: sql<MovieStatus>`${moviesCol.status}`,
 | 
					 | 
				
			||||||
					airDate: startAir,
 | 
					 | 
				
			||||||
					poster: sql<Image>`coalesce(${showTranslations.poster}, ${poster})`,
 | 
					 | 
				
			||||||
					thumbnail: sql<Image>`coalesce(${showTranslations.thumbnail}, ${thumbnail})`,
 | 
					 | 
				
			||||||
					banner: sql<Image>`coalesce(${showTranslations.banner}, ${banner})`,
 | 
					 | 
				
			||||||
					logo: sql<Image>`coalesce(${showTranslations.logo}, ${logo})`,
 | 
					 | 
				
			||||||
					isAvailable: sql<boolean>`${videoQ.showPk} is not null`.as(
 | 
					 | 
				
			||||||
						"isAvailable",
 | 
					 | 
				
			||||||
					),
 | 
					 | 
				
			||||||
				})
 | 
					 | 
				
			||||||
				.from(shows)
 | 
					 | 
				
			||||||
				.innerJoin(transQ, eq(shows.pk, transQ.pk))
 | 
					 | 
				
			||||||
				.leftJoin(
 | 
					 | 
				
			||||||
					showTranslations,
 | 
					 | 
				
			||||||
					and(
 | 
					 | 
				
			||||||
						eq(shows.pk, showTranslations.pk),
 | 
					 | 
				
			||||||
						eq(showTranslations.language, shows.originalLanguage),
 | 
					 | 
				
			||||||
						// TODO: check user's settings before fallbacking to false.
 | 
					 | 
				
			||||||
						sql`coalesce(${preferOriginal ?? null}::boolean, false)`,
 | 
					 | 
				
			||||||
					),
 | 
					 | 
				
			||||||
				)
 | 
					 | 
				
			||||||
				.leftJoin(videoQ, eq(shows.pk, videoQ.showPk))
 | 
					 | 
				
			||||||
				.where(
 | 
					 | 
				
			||||||
					and(
 | 
					 | 
				
			||||||
						filter,
 | 
					 | 
				
			||||||
						query ? sql`${transQ.name} %> ${query}::text` : undefined,
 | 
					 | 
				
			||||||
						keysetPaginate({ table: shows, after, sort }),
 | 
					 | 
				
			||||||
					),
 | 
					 | 
				
			||||||
				)
 | 
					 | 
				
			||||||
				.orderBy(
 | 
					 | 
				
			||||||
					...(query
 | 
					 | 
				
			||||||
						? [sql`word_similarity(${query}::text, ${transQ.name})`]
 | 
					 | 
				
			||||||
						: sortToSql(sort, shows)),
 | 
					 | 
				
			||||||
					shows.pk,
 | 
					 | 
				
			||||||
				)
 | 
					 | 
				
			||||||
				.limit(limit);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			return createPage(items, { url, sort, limit });
 | 
								return createPage(items, { url, sort, limit });
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			detail: { description: "Get all movies" },
 | 
								detail: { description: "Get all movies" },
 | 
				
			||||||
			query: t.Object({
 | 
								query: t.Object({
 | 
				
			||||||
				sort: Sort(["slug", "rating", "airDate", "createdAt", "nextRefresh"], {
 | 
									sort: showSort,
 | 
				
			||||||
					remap: { airDate: "startAir" },
 | 
									filter: t.Optional(Filter({ def: showFilters })),
 | 
				
			||||||
					default: ["slug"],
 | 
					 | 
				
			||||||
				}),
 | 
					 | 
				
			||||||
				filter: t.Optional(Filter({ def: movieFilters })),
 | 
					 | 
				
			||||||
				query: t.Optional(t.String({ description: desc.query })),
 | 
									query: t.Optional(t.String({ description: desc.query })),
 | 
				
			||||||
				limit: t.Integer({
 | 
									limit: t.Integer({
 | 
				
			||||||
					minimum: 1,
 | 
										minimum: 1,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,67 @@
 | 
				
			|||||||
 | 
					import { and, eq } from "drizzle-orm";
 | 
				
			||||||
import { Elysia, t } from "elysia";
 | 
					import { Elysia, t } from "elysia";
 | 
				
			||||||
import { Serie } from "~/models/serie";
 | 
					import { shows } from "~/db/schema";
 | 
				
			||||||
 | 
					import { KError } from "~/models/error";
 | 
				
			||||||
 | 
					import { Serie, SerieTranslation } from "~/models/serie";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						AcceptLanguage,
 | 
				
			||||||
 | 
						Filter,
 | 
				
			||||||
 | 
						Page,
 | 
				
			||||||
 | 
						createPage,
 | 
				
			||||||
 | 
						processLanguages,
 | 
				
			||||||
 | 
					} from "~/models/utils";
 | 
				
			||||||
 | 
					import { desc } from "~/models/utils/descriptions";
 | 
				
			||||||
 | 
					import { getShows, showFilters, showSort } from "./shows";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const series = new Elysia({ prefix: "/series" })
 | 
					export const series = new Elysia({ prefix: "/series", tags: ["series"] })
 | 
				
			||||||
	.model({
 | 
						.model({
 | 
				
			||||||
		serie: Serie,
 | 
							serie: Serie,
 | 
				
			||||||
		error: t.Object({}),
 | 
							"serie-translation": SerieTranslation,
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	.get("/:id", () => "hello" as unknown as Serie, {
 | 
						.get(
 | 
				
			||||||
		response: { 200: "serie" },
 | 
							"",
 | 
				
			||||||
	});
 | 
							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, "serie"), filter),
 | 
				
			||||||
 | 
									languages: langs,
 | 
				
			||||||
 | 
									preferOriginal,
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								return createPage(items, { url, sort, limit });
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								detail: { description: "Get all series" },
 | 
				
			||||||
 | 
								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(Serie),
 | 
				
			||||||
 | 
									422: KError,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										119
									
								
								api/src/controllers/shows/shows.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								api/src/controllers/shows/shows.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,119 @@
 | 
				
			|||||||
 | 
					import type { StaticDecode } from "@sinclair/typebox";
 | 
				
			||||||
 | 
					import { type SQL, and, eq, sql } from "drizzle-orm";
 | 
				
			||||||
 | 
					import { db } from "~/db";
 | 
				
			||||||
 | 
					import { showTranslations, shows } from "~/db/schema";
 | 
				
			||||||
 | 
					import { getColumns, sqlarr } from "~/db/utils";
 | 
				
			||||||
 | 
					import type { MovieStatus } from "~/models/movie";
 | 
				
			||||||
 | 
					import { SerieStatus } from "~/models/serie";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						type FilterDef,
 | 
				
			||||||
 | 
						Genre,
 | 
				
			||||||
 | 
						type Image,
 | 
				
			||||||
 | 
						Sort,
 | 
				
			||||||
 | 
						keysetPaginate,
 | 
				
			||||||
 | 
						sortToSql,
 | 
				
			||||||
 | 
					} from "~/models/utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const showFilters: FilterDef = {
 | 
				
			||||||
 | 
						genres: {
 | 
				
			||||||
 | 
							column: shows.genres,
 | 
				
			||||||
 | 
							type: "enum",
 | 
				
			||||||
 | 
							values: Genre.enum,
 | 
				
			||||||
 | 
							isArray: true,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						rating: { column: shows.rating, type: "int" },
 | 
				
			||||||
 | 
						status: { column: shows.status, type: "enum", values: SerieStatus.enum },
 | 
				
			||||||
 | 
						runtime: { column: shows.runtime, type: "float" },
 | 
				
			||||||
 | 
						airDate: { column: shows.startAir, type: "date" },
 | 
				
			||||||
 | 
						startAir: { column: shows.startAir, type: "date" },
 | 
				
			||||||
 | 
						endAir: { column: shows.startAir, type: "date" },
 | 
				
			||||||
 | 
						originalLanguage: { column: shows.originalLanguage, type: "string" },
 | 
				
			||||||
 | 
						tags: {
 | 
				
			||||||
 | 
							column: sql.raw(`t.${showTranslations.tags.name}`),
 | 
				
			||||||
 | 
							type: "string",
 | 
				
			||||||
 | 
							isArray: true,
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					export const showSort = Sort(
 | 
				
			||||||
 | 
						[
 | 
				
			||||||
 | 
							"slug",
 | 
				
			||||||
 | 
							"rating",
 | 
				
			||||||
 | 
							"airDate",
 | 
				
			||||||
 | 
							"startAir",
 | 
				
			||||||
 | 
							"endAir",
 | 
				
			||||||
 | 
							"createdAt",
 | 
				
			||||||
 | 
							"nextRefresh",
 | 
				
			||||||
 | 
						],
 | 
				
			||||||
 | 
						{
 | 
				
			||||||
 | 
							remap: { airDate: "startAir" },
 | 
				
			||||||
 | 
							default: ["slug"],
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function getShows({
 | 
				
			||||||
 | 
						after,
 | 
				
			||||||
 | 
						limit,
 | 
				
			||||||
 | 
						query,
 | 
				
			||||||
 | 
						sort,
 | 
				
			||||||
 | 
						filter,
 | 
				
			||||||
 | 
						languages,
 | 
				
			||||||
 | 
						preferOriginal,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
						after: string | undefined;
 | 
				
			||||||
 | 
						limit: number;
 | 
				
			||||||
 | 
						query: string | undefined;
 | 
				
			||||||
 | 
						sort: StaticDecode<typeof showSort>;
 | 
				
			||||||
 | 
						filter: SQL | undefined;
 | 
				
			||||||
 | 
						languages: string[];
 | 
				
			||||||
 | 
						preferOriginal: boolean | undefined;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
						const transQ = db
 | 
				
			||||||
 | 
							.selectDistinctOn([showTranslations.pk])
 | 
				
			||||||
 | 
							.from(showTranslations)
 | 
				
			||||||
 | 
							.orderBy(
 | 
				
			||||||
 | 
								showTranslations.pk,
 | 
				
			||||||
 | 
								sql`array_position(${sqlarr(languages)}, ${showTranslations.language})`,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							.as("t");
 | 
				
			||||||
 | 
						const { pk, poster, thumbnail, banner, logo, ...transCol } =
 | 
				
			||||||
 | 
							getColumns(transQ);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return await db
 | 
				
			||||||
 | 
							.select({
 | 
				
			||||||
 | 
								...getColumns(shows),
 | 
				
			||||||
 | 
								...transCol,
 | 
				
			||||||
 | 
								// movie columns (status is only a typescript hint)
 | 
				
			||||||
 | 
								status: sql<MovieStatus>`${shows.status}`,
 | 
				
			||||||
 | 
								airDate: shows.startAir,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								poster: sql<Image>`coalesce(${showTranslations.poster}, ${poster})`,
 | 
				
			||||||
 | 
								thumbnail: sql<Image>`coalesce(${showTranslations.thumbnail}, ${thumbnail})`,
 | 
				
			||||||
 | 
								banner: sql<Image>`coalesce(${showTranslations.banner}, ${banner})`,
 | 
				
			||||||
 | 
								logo: sql<Image>`coalesce(${showTranslations.logo}, ${logo})`,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							.from(shows)
 | 
				
			||||||
 | 
							.innerJoin(transQ, eq(shows.pk, transQ.pk))
 | 
				
			||||||
 | 
							.leftJoin(
 | 
				
			||||||
 | 
								showTranslations,
 | 
				
			||||||
 | 
								and(
 | 
				
			||||||
 | 
									eq(shows.pk, showTranslations.pk),
 | 
				
			||||||
 | 
									eq(showTranslations.language, shows.originalLanguage),
 | 
				
			||||||
 | 
									// TODO: check user's settings before fallbacking to false.
 | 
				
			||||||
 | 
									sql`coalesce(${preferOriginal ?? null}::boolean, false)`,
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							.where(
 | 
				
			||||||
 | 
								and(
 | 
				
			||||||
 | 
									filter,
 | 
				
			||||||
 | 
									query ? sql`${transQ.name} %> ${query}::text` : undefined,
 | 
				
			||||||
 | 
									keysetPaginate({ table: shows, after, sort }),
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							.orderBy(
 | 
				
			||||||
 | 
								...(query
 | 
				
			||||||
 | 
									? [sql`word_similarity(${query}::text, ${transQ.name})`]
 | 
				
			||||||
 | 
									: sortToSql(sort, shows)),
 | 
				
			||||||
 | 
								shows.pk,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							.limit(limit);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -57,7 +57,7 @@ export const Movie = t.Intersect([
 | 
				
			|||||||
	Resource(),
 | 
						Resource(),
 | 
				
			||||||
	MovieTranslation,
 | 
						MovieTranslation,
 | 
				
			||||||
	BaseMovie,
 | 
						BaseMovie,
 | 
				
			||||||
	t.Object({ isAvailable: t.Boolean() }),
 | 
						// t.Object({ isAvailable: t.Boolean() }),
 | 
				
			||||||
]);
 | 
					]);
 | 
				
			||||||
export type Movie = Prettify<typeof Movie.static>;
 | 
					export type Movie = Prettify<typeof Movie.static>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user