mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
291 lines
7.1 KiB
TypeScript
291 lines
7.1 KiB
TypeScript
import { type SQL, and, desc, eq, exists, ne, sql } from "drizzle-orm";
|
|
import { db } from "~/db";
|
|
import {
|
|
entries,
|
|
entryTranslations,
|
|
entryVideoJoin,
|
|
history,
|
|
showStudioJoin,
|
|
showTranslations,
|
|
shows,
|
|
studioTranslations,
|
|
studios,
|
|
videos,
|
|
} from "~/db/schema";
|
|
import {
|
|
coalesce,
|
|
getColumns,
|
|
jsonbAgg,
|
|
jsonbBuildObject,
|
|
jsonbObjectAgg,
|
|
sqlarr,
|
|
} from "~/db/utils";
|
|
import type { Entry } from "~/models/entry";
|
|
import type { MovieStatus } from "~/models/movie";
|
|
import { SerieStatus, type SerieTranslation } from "~/models/serie";
|
|
import type { Studio } from "~/models/studio";
|
|
import {
|
|
type FilterDef,
|
|
Genre,
|
|
type Image,
|
|
Sort,
|
|
buildRelations,
|
|
keysetPaginate,
|
|
sortToSql,
|
|
} from "~/models/utils";
|
|
import type { EmbeddedVideo } from "~/models/video";
|
|
|
|
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: sql`${shows.original}->'language'`,
|
|
type: "string",
|
|
},
|
|
tags: {
|
|
column: sql.raw(`t.${showTranslations.tags.name}`),
|
|
type: "string",
|
|
isArray: true,
|
|
},
|
|
};
|
|
export const showSort = Sort(
|
|
{
|
|
slug: shows.slug,
|
|
rating: shows.rating,
|
|
airDate: shows.startAir,
|
|
startAir: shows.startAir,
|
|
endAir: shows.endAir,
|
|
createdAt: shows.createdAt,
|
|
nextRefresh: shows.nextRefresh,
|
|
},
|
|
{
|
|
default: ["slug"],
|
|
tablePk: shows.pk,
|
|
},
|
|
);
|
|
|
|
const showRelations = {
|
|
translations: () => {
|
|
const { pk, language, ...trans } = getColumns(showTranslations);
|
|
return db
|
|
.select({
|
|
json: jsonbObjectAgg(
|
|
language,
|
|
jsonbBuildObject<SerieTranslation>(trans),
|
|
).as("json"),
|
|
})
|
|
.from(showTranslations)
|
|
.where(eq(showTranslations.pk, shows.pk))
|
|
.as("translations");
|
|
},
|
|
studios: ({ languages }: { languages: string[] }) => {
|
|
const { pk: _, ...studioCol } = getColumns(studios);
|
|
const studioTransQ = db
|
|
.selectDistinctOn([studioTranslations.pk])
|
|
.from(studioTranslations)
|
|
.orderBy(
|
|
studioTranslations.pk,
|
|
sql`array_position(${sqlarr(languages)}, ${studioTranslations.language})`,
|
|
)
|
|
.as("t");
|
|
const { pk, language, ...studioTrans } = getColumns(studioTransQ);
|
|
|
|
return db
|
|
.select({
|
|
json: coalesce(
|
|
jsonbAgg(jsonbBuildObject<Studio>({ ...studioTrans, ...studioCol })),
|
|
sql`'[]'::jsonb`,
|
|
).as("json"),
|
|
})
|
|
.from(studios)
|
|
.leftJoin(studioTransQ, eq(studios.pk, studioTransQ.pk))
|
|
.where(
|
|
exists(
|
|
db
|
|
.select()
|
|
.from(showStudioJoin)
|
|
.where(
|
|
and(
|
|
eq(showStudioJoin.studioPk, studios.pk),
|
|
eq(showStudioJoin.showPk, shows.pk),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
.as("studios");
|
|
},
|
|
// only available for movies
|
|
videos: () => {
|
|
const { guess, createdAt, updatedAt, ...videosCol } = getColumns(videos);
|
|
return db
|
|
.select({
|
|
videos: coalesce(
|
|
jsonbAgg(
|
|
jsonbBuildObject<EmbeddedVideo>({
|
|
slug: entryVideoJoin.slug,
|
|
...videosCol,
|
|
}),
|
|
),
|
|
sql`'[]'::jsonb`,
|
|
).as("videos"),
|
|
})
|
|
.from(entryVideoJoin)
|
|
.where(eq(entryVideoJoin.entryPk, entries.pk))
|
|
.leftJoin(entries, eq(entries.showPk, shows.pk))
|
|
.leftJoin(videos, eq(videos.pk, entryVideoJoin.videoPk))
|
|
.as("videos");
|
|
},
|
|
firstEntry: ({
|
|
languages,
|
|
userId,
|
|
}: { languages: string[]; userId: number }) => {
|
|
const transQ = db
|
|
.selectDistinctOn([entryTranslations.pk])
|
|
.from(entryTranslations)
|
|
.orderBy(
|
|
entryTranslations.pk,
|
|
sql`array_position(${sqlarr(languages)}, ${entryTranslations.language})`,
|
|
)
|
|
.as("t");
|
|
const { pk, ...transCol } = getColumns(transQ);
|
|
|
|
const { guess, createdAt, updatedAt, ...videosCol } = getColumns(videos);
|
|
const videosQ = db
|
|
.select({
|
|
videos: coalesce(
|
|
jsonbAgg(
|
|
jsonbBuildObject<EmbeddedVideo>({
|
|
slug: entryVideoJoin.slug,
|
|
...videosCol,
|
|
}),
|
|
),
|
|
sql`'[]'::jsonb`,
|
|
).as("videos"),
|
|
})
|
|
.from(entryVideoJoin)
|
|
.where(eq(entryVideoJoin.entryPk, entries.pk))
|
|
.leftJoin(videos, eq(videos.pk, entryVideoJoin.videoPk))
|
|
.as("videos");
|
|
|
|
const progressQ = db
|
|
.selectDistinctOn([history.entryPk], {
|
|
percent: history.percent,
|
|
time: history.time,
|
|
entryPk: history.entryPk,
|
|
videoId: videos.id,
|
|
})
|
|
.from(history)
|
|
.where(eq(history.profilePk, userId))
|
|
.leftJoin(videos, eq(history.videoPk, videos.pk))
|
|
.orderBy(history.entryPk, desc(history.playedDate))
|
|
.as("progress");
|
|
|
|
return db
|
|
.select({
|
|
firstEntry: jsonbBuildObject<Entry>({
|
|
...getColumns(entries),
|
|
...transCol,
|
|
number: entries.episodeNumber,
|
|
videos: videosQ.videos,
|
|
progress: getColumns(progressQ),
|
|
}).as("firstEntry"),
|
|
})
|
|
.from(entries)
|
|
.innerJoin(transQ, eq(entries.pk, transQ.pk))
|
|
.leftJoin(progressQ, eq(entries.pk, progressQ.entryPk))
|
|
.leftJoinLateral(videosQ, sql`true`)
|
|
.where(and(eq(entries.showPk, shows.pk), ne(entries.kind, "extra")))
|
|
.orderBy(entries.order)
|
|
.limit(1)
|
|
.as("firstEntry");
|
|
},
|
|
};
|
|
|
|
export async function getShows({
|
|
after,
|
|
limit,
|
|
query,
|
|
sort,
|
|
filter,
|
|
languages,
|
|
fallbackLanguage = true,
|
|
preferOriginal = false,
|
|
relations = [],
|
|
userId,
|
|
}: {
|
|
after?: string;
|
|
limit: number;
|
|
query?: string;
|
|
sort?: Sort;
|
|
filter?: SQL;
|
|
languages: string[];
|
|
fallbackLanguage?: boolean;
|
|
preferOriginal?: boolean;
|
|
relations?: (keyof typeof showRelations)[];
|
|
userId: number;
|
|
}) {
|
|
const transQ = db
|
|
.selectDistinctOn([showTranslations.pk])
|
|
.from(showTranslations)
|
|
.where(
|
|
!fallbackLanguage
|
|
? eq(showTranslations.language, sql`any(${sqlarr(languages)})`)
|
|
: undefined,
|
|
)
|
|
.orderBy(
|
|
showTranslations.pk,
|
|
sql`array_position(${sqlarr(languages)}, ${showTranslations.language})`,
|
|
)
|
|
.as("t");
|
|
|
|
return await db
|
|
.select({
|
|
...getColumns(shows),
|
|
...getColumns(transQ),
|
|
|
|
// movie columns (status is only a typescript hint)
|
|
status: sql<MovieStatus>`${shows.status}`,
|
|
airDate: shows.startAir,
|
|
kind: sql<any>`${shows.kind}`,
|
|
isAvailable: sql<boolean>`${shows.availableCount} != 0`,
|
|
|
|
...(preferOriginal && {
|
|
poster: sql<Image>`coalesce(nullif(${shows.original}->'poster', 'null'::jsonb), ${transQ.poster})`,
|
|
thumbnail: sql<Image>`coalesce(nullif(${shows.original}->'thumbnail', 'null'::jsonb), ${transQ.thumbnail})`,
|
|
banner: sql<Image>`coalesce(nullif(${shows.original}->'banner', 'null'::jsonb), ${transQ.banner})`,
|
|
logo: sql<Image>`coalesce(nullif(${shows.original}->'logo', 'null'::jsonb), ${transQ.logo})`,
|
|
}),
|
|
|
|
...buildRelations(relations, showRelations, { languages, userId }),
|
|
})
|
|
.from(shows)
|
|
[fallbackLanguage ? "innerJoin" : ("leftJoin" as "innerJoin")](
|
|
transQ,
|
|
eq(shows.pk, transQ.pk),
|
|
)
|
|
.where(
|
|
and(
|
|
filter,
|
|
query ? sql`${transQ.name} %> ${query}::text` : undefined,
|
|
keysetPaginate({ after, sort }),
|
|
),
|
|
)
|
|
.orderBy(
|
|
...(query
|
|
? [sql`word_similarity(${query}::text, ${transQ.name})`]
|
|
: sortToSql(sort)),
|
|
shows.pk,
|
|
)
|
|
.limit(limit);
|
|
}
|