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, isUuid, 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; 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`${shows.status}`, airDate: shows.startAir, poster: sql`coalesce(${showTranslations.poster}, ${poster})`, thumbnail: sql`coalesce(${showTranslations.thumbnail}, ${thumbnail})`, banner: sql`coalesce(${showTranslations.banner}, ${banner})`, logo: sql`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); } 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`${shows.startAir}`.as("airDate"), status: sql`${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`(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 }; }