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 { db } from "~/db";
 | 
			
		||||
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 { bubble } from "~/models/examples";
 | 
			
		||||
import {
 | 
			
		||||
	FullMovie,
 | 
			
		||||
	Movie,
 | 
			
		||||
	MovieStatus,
 | 
			
		||||
	type MovieStatus,
 | 
			
		||||
	MovieTranslation,
 | 
			
		||||
} from "~/models/movie";
 | 
			
		||||
import {
 | 
			
		||||
	AcceptLanguage,
 | 
			
		||||
	Filter,
 | 
			
		||||
	type FilterDef,
 | 
			
		||||
	Genre,
 | 
			
		||||
	type Image,
 | 
			
		||||
	Page,
 | 
			
		||||
	Sort,
 | 
			
		||||
	createPage,
 | 
			
		||||
	isUuid,
 | 
			
		||||
	keysetPaginate,
 | 
			
		||||
	processLanguages,
 | 
			
		||||
	sortToSql,
 | 
			
		||||
} from "~/models/utils";
 | 
			
		||||
import { desc } from "~/models/utils/descriptions";
 | 
			
		||||
 | 
			
		||||
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,
 | 
			
		||||
	},
 | 
			
		||||
};
 | 
			
		||||
import { getShows, showFilters, showSort } from "./shows";
 | 
			
		||||
 | 
			
		||||
export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
 | 
			
		||||
	.model({
 | 
			
		||||
@ -236,85 +212,22 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
 | 
			
		||||
			request: { url },
 | 
			
		||||
		}) => {
 | 
			
		||||
			const langs = processLanguages(languages);
 | 
			
		||||
 | 
			
		||||
			// we keep the pk for after handling. it will be removed by elysia's validators after.
 | 
			
		||||
			const { kind, startAir, endAir, ...moviesCol } = getColumns(shows);
 | 
			
		||||
 | 
			
		||||
			const transQ = db
 | 
			
		||||
				.selectDistinctOn([showTranslations.pk])
 | 
			
		||||
				.from(showTranslations)
 | 
			
		||||
				.orderBy(
 | 
			
		||||
					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);
 | 
			
		||||
 | 
			
		||||
			const items = await getShows({
 | 
			
		||||
				limit,
 | 
			
		||||
				after,
 | 
			
		||||
				query,
 | 
			
		||||
				sort,
 | 
			
		||||
				filter: and(eq(shows.kind, "movie"), filter),
 | 
			
		||||
				languages: langs,
 | 
			
		||||
				preferOriginal,
 | 
			
		||||
			});
 | 
			
		||||
			return createPage(items, { url, sort, limit });
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			detail: { description: "Get all movies" },
 | 
			
		||||
			query: t.Object({
 | 
			
		||||
				sort: Sort(["slug", "rating", "airDate", "createdAt", "nextRefresh"], {
 | 
			
		||||
					remap: { airDate: "startAir" },
 | 
			
		||||
					default: ["slug"],
 | 
			
		||||
				}),
 | 
			
		||||
				filter: t.Optional(Filter({ def: movieFilters })),
 | 
			
		||||
				sort: showSort,
 | 
			
		||||
				filter: t.Optional(Filter({ def: showFilters })),
 | 
			
		||||
				query: t.Optional(t.String({ description: desc.query })),
 | 
			
		||||
				limit: t.Integer({
 | 
			
		||||
					minimum: 1,
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,67 @@
 | 
			
		||||
import { and, eq } from "drizzle-orm";
 | 
			
		||||
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({
 | 
			
		||||
		serie: Serie,
 | 
			
		||||
		error: t.Object({}),
 | 
			
		||||
		"serie-translation": SerieTranslation,
 | 
			
		||||
	})
 | 
			
		||||
	.get("/:id", () => "hello" as unknown as Serie, {
 | 
			
		||||
		response: { 200: "serie" },
 | 
			
		||||
	});
 | 
			
		||||
	.get(
 | 
			
		||||
		"",
 | 
			
		||||
		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(),
 | 
			
		||||
	MovieTranslation,
 | 
			
		||||
	BaseMovie,
 | 
			
		||||
	t.Object({ isAvailable: t.Boolean() }),
 | 
			
		||||
	// t.Object({ isAvailable: t.Boolean() }),
 | 
			
		||||
]);
 | 
			
		||||
export type Movie = Prettify<typeof Movie.static>;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user