diff --git a/api/src/controllers/movies.ts b/api/src/controllers/movies.ts index eaf21049..2cad2f1f 100644 --- a/api/src/controllers/movies.ts +++ b/api/src/controllers/movies.ts @@ -16,6 +16,7 @@ import { MovieTranslation, } from "~/models/movie"; import { + AcceptLanguage, Filter, type FilterDef, Genre, @@ -28,8 +29,8 @@ import { processLanguages, sortToSql, } from "~/models/utils"; -import { comment } from "~/utils"; import { db } from "../db"; +import { desc } from "~/models/utils/descriptions"; const movieFilters: FilterDef = { genres: { @@ -180,13 +181,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) }), query: t.Object({ preferOriginal: t.Optional( - t.Boolean({ - description: comment` - Prefer images in the original's language. If true, will return untranslated images instead of the translated ones. - - If unspecified, kyoo will look at the current user's settings to decide what to do. - `, - }), + t.Boolean({ description: desc.preferOriginal }), ), with: t.Array(t.UnionEnum(["translations", "videos"]), { default: [], @@ -194,14 +189,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) }), }), headers: t.Object({ - "accept-language": t.String({ - default: "*", - example: "en-us, ja;q=0.5", - description: comment` - List of languages you want the data in. - This follows the [Accept-Language offical specification](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language). - `, - }), + "accept-language": AcceptLanguage(), }), response: { 200: { ...FullMovie, description: "Found" }, @@ -209,14 +197,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) ...KError, description: "No movie found with the given id or slug.", }, - 422: { - ...KError, - description: comment` - The Accept-Language header can't be satisfied (all languages listed are - unavailable.) Try with another languages or add * to the list of languages - to fallback to any language. - `, - }, + 422: KError, }, }, ) @@ -339,58 +320,26 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) default: ["slug"], }), filter: t.Optional(Filter({ def: movieFilters })), - query: t.Optional( - t.String({ - description: comment` - Search query. - Searching automatically sort via relevance before the other sort parameters. - `, - }), - ), + 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: comment` - Id of the cursor in the pagination. - You can ignore this and only use the prev/next field in the response. - `, - }), - ), + after: t.Optional(t.String({ description: desc.after })), preferOriginal: t.Optional( t.Boolean({ - description: comment` - Prefer images in the original's language. If true, will return untranslated images instead of the translated ones. - - If unspecified, kyoo will look at the current user's settings to decide what to do. - `, + description: desc.preferOriginal, }), ), }), headers: t.Object({ - "accept-language": t.String({ - default: "*", - example: "en-us, ja;q=0.5", - description: comment` - List of languages you want the data in. - This follows the [Accept-Language offical specification](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language). - - In this request, * is always implied (if no language could satisfy the request, kyoo will use any language available.) - `, - }), + "accept-language": AcceptLanguage({ autoFallback: true }), }), response: { - 200: Page(Movie, { - description: "Paginated list of movies that match filters.", - }), - 422: { - ...KError, - description: "Invalid query parameters.", - }, + 200: Page(Movie), + 422: KError, }, }, ); diff --git a/api/src/models/error.ts b/api/src/models/error.ts index 9d2229d7..0d746c7c 100644 --- a/api/src/models/error.ts +++ b/api/src/models/error.ts @@ -7,6 +7,7 @@ export const KError = t.Object( details: t.Optional(t.Any()), }, { + description: "Invalid parameters.", examples: [{ status: 404, message: "Movie not found" }], }, ); diff --git a/api/src/models/utils/descriptions.ts b/api/src/models/utils/descriptions.ts new file mode 100644 index 00000000..0e683805 --- /dev/null +++ b/api/src/models/utils/descriptions.ts @@ -0,0 +1,19 @@ +import { comment } from "~/utils"; + +export const desc = { + preferOriginal: comment` + Prefer images in the original's language. If true, will return untranslated images instead of the translated ones. + + If unspecified, kyoo will look at the current user's settings to decide what to do. + `, + + after: comment` + Id of the cursor in the pagination. + You can ignore this and only use the prev/next field in the response. + `, + + query: comment` + Search query. + Searching automatically sort via relevance before the other sort parameters. + `, +}; diff --git a/api/src/models/utils/keyset-paginate.ts b/api/src/models/utils/keyset-paginate.ts index a7dd024e..76fd33bb 100644 --- a/api/src/models/utils/keyset-paginate.ts +++ b/api/src/models/utils/keyset-paginate.ts @@ -77,7 +77,13 @@ export const keysetPaginate = < return where; }; -export const generateAfter = (cursor: any, sort: Sort) => { +export const generateAfter = < + const ST extends NonEmptyArray, + const Remap extends Partial> = never, +>( + cursor: any, + sort: Sort, +) => { const ret = [ ...sort.sort.map((by) => cursor[by.remmapedKey ?? by.key]), cursor.pk, diff --git a/api/src/models/utils/language.ts b/api/src/models/utils/language.ts index d2b4e1fc..88085e57 100644 --- a/api/src/models/utils/language.ts +++ b/api/src/models/utils/language.ts @@ -51,7 +51,7 @@ export const TranslationRecord = ( if (!(locale.language in translations)) translations[locale.language] = translations[lang]; // normalize locale names (caps, old values etc) - // we need to do this here because the record's key (Language)'s transform is not runned. + // we need to do this here because the record's key (Language)'s transform is not run. // this is a limitation of typebox if (lang !== locale.baseName) { translations[locale.baseName] = translations[lang]; @@ -80,3 +80,21 @@ export const processLanguages = (languages?: string) => { return [lang]; }); }; + +export const AcceptLanguage = ({ + autoFallback = false, +}: { autoFallback?: boolean } = {}) => + t.String({ + default: "*", + example: "en-us, ja;q=0.5", + description: + comment` + List of languages you want the data in. + This follows the [Accept-Language offical specification](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language). + ` + autoFallback + ? comment` + + In this request, * is always implied (if no language could satisfy the request, kyoo will use any language available.) + ` + : "", + }); diff --git a/api/src/models/utils/page.ts b/api/src/models/utils/page.ts index c5e9eec8..96986039 100644 --- a/api/src/models/utils/page.ts +++ b/api/src/models/utils/page.ts @@ -1,7 +1,7 @@ import type { ObjectOptions } from "@sinclair/typebox"; import { type TSchema, t } from "elysia"; import { generateAfter } from "./keyset-paginate"; -import type { Sort } from "./sort"; +import type { NonEmptyArray, Sort } from "./sort"; export const Page = (schema: T, options?: ObjectOptions) => t.Object( @@ -10,12 +10,19 @@ export const Page = (schema: T, options?: ObjectOptions) => this: t.String({ format: "uri" }), next: t.Nullable(t.String({ format: "uri" })), }, - options, + { + description: `Paginated list of ${schema.title} that match filters.`, + ...options, + }, ); -export const createPage = ( +export const createPage = < + T, + const ST extends NonEmptyArray, + const Remap extends Partial> = never, +>( items: T[], - { url, sort, limit }: { url: string; sort: Sort; limit: number }, + { url, sort, limit }: { url: string; sort: Sort; limit: number }, ) => { let next: string | null = null; const uri = new URL(url); diff --git a/api/src/models/utils/sort.ts b/api/src/models/utils/sort.ts index c3b5f5f6..d9a4c177 100644 --- a/api/src/models/utils/sort.ts +++ b/api/src/models/utils/sort.ts @@ -18,7 +18,7 @@ export type NonEmptyArray = [T, ...T[]]; export const Sort = < const T extends NonEmptyArray, - const Remap extends Partial>, + const Remap extends Partial> = never, >( values: T, {