mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Create history api
This commit is contained in:
parent
df7d109c34
commit
4ba7750012
@ -45,7 +45,22 @@ import {
|
||||
import { desc as description } from "~/models/utils/descriptions";
|
||||
import type { EmbeddedVideo } from "~/models/video";
|
||||
|
||||
const entryFilters: FilterDef = {
|
||||
export const entryProgressQ = db
|
||||
.selectDistinctOn([history.entryPk], {
|
||||
percent: history.percent,
|
||||
time: history.time,
|
||||
entryPk: history.entryPk,
|
||||
playedDate: history.playedDate,
|
||||
videoId: videos.id,
|
||||
})
|
||||
.from(history)
|
||||
.leftJoin(videos, eq(history.videoPk, videos.pk))
|
||||
.leftJoin(profiles, eq(history.profilePk, profiles.pk))
|
||||
.where(eq(profiles.id, sql.placeholder("userId")))
|
||||
.orderBy(history.entryPk, desc(history.playedDate))
|
||||
.as("progress");
|
||||
|
||||
export const entryFilters: FilterDef = {
|
||||
kind: {
|
||||
column: entries.kind,
|
||||
type: "enum",
|
||||
@ -57,18 +72,21 @@ const entryFilters: FilterDef = {
|
||||
order: { column: entries.order, type: "float" },
|
||||
runtime: { column: entries.runtime, type: "float" },
|
||||
airDate: { column: entries.airDate, type: "date" },
|
||||
playedDate: { column: entryProgressQ.playedDate, type: "date" },
|
||||
};
|
||||
|
||||
const extraFilters: FilterDef = {
|
||||
kind: { column: entries.extraKind, type: "enum", values: ExtraType.enum },
|
||||
runtime: { column: entries.runtime, type: "float" },
|
||||
playedDate: { column: entryProgressQ.playedDate, type: "date" },
|
||||
};
|
||||
|
||||
const unknownFilters: FilterDef = {
|
||||
runtime: { column: entries.runtime, type: "float" },
|
||||
playedDate: { column: entryProgressQ.playedDate, type: "date" },
|
||||
};
|
||||
|
||||
const entrySort = Sort(
|
||||
export const entrySort = Sort(
|
||||
{
|
||||
order: entries.order,
|
||||
seasonNumber: entries.seasonNumber,
|
||||
@ -76,6 +94,7 @@ const entrySort = Sort(
|
||||
number: entries.episodeNumber,
|
||||
airDate: entries.airDate,
|
||||
nextRefresh: entries.nextRefresh,
|
||||
playedDate: entryProgressQ.playedDate,
|
||||
},
|
||||
{
|
||||
default: ["order"],
|
||||
@ -89,6 +108,7 @@ const extraSort = Sort(
|
||||
name: entryTranslations.name,
|
||||
runtime: entries.runtime,
|
||||
createdAt: entries.createdAt,
|
||||
playedDate: entryProgressQ.playedDate,
|
||||
},
|
||||
{
|
||||
default: ["slug"],
|
||||
@ -126,36 +146,19 @@ export const entryVideosQ = db
|
||||
.leftJoin(videos, eq(videos.pk, entryVideoJoin.videoPk))
|
||||
.as("videos");
|
||||
|
||||
export const getEntryProgressQ = (userId: string) =>
|
||||
db
|
||||
.selectDistinctOn([history.entryPk], {
|
||||
percent: history.percent,
|
||||
time: history.time,
|
||||
entryPk: history.entryPk,
|
||||
videoId: videos.id,
|
||||
})
|
||||
.from(history)
|
||||
.leftJoin(videos, eq(history.videoPk, videos.pk))
|
||||
.leftJoin(profiles, eq(history.profilePk, profiles.pk))
|
||||
.where(eq(profiles.id, userId))
|
||||
.orderBy(history.entryPk, desc(history.playedDate))
|
||||
.as("progress");
|
||||
|
||||
export const mapProgress = (
|
||||
progressQ: ReturnType<typeof getEntryProgressQ>,
|
||||
{ aliased }: { aliased: boolean } = { aliased: false },
|
||||
) => {
|
||||
const { time, percent, videoId } = getColumns(progressQ);
|
||||
export const mapProgress = ({ aliased }: { aliased: boolean }) => {
|
||||
const { time, percent, playedDate, videoId } = getColumns(entryProgressQ);
|
||||
const ret = {
|
||||
time: coalesce(time, sql`0`),
|
||||
percent: coalesce(percent, sql`0`),
|
||||
playedDate: sql`${playedDate}`,
|
||||
videoId: sql`${videoId}`,
|
||||
};
|
||||
if (!aliased) return ret;
|
||||
return Object.fromEntries(Object.entries(ret).map(([k, v]) => [k, v.as(k)]));
|
||||
};
|
||||
|
||||
async function getEntries({
|
||||
export async function getEntries({
|
||||
after,
|
||||
limit,
|
||||
query,
|
||||
@ -182,8 +185,6 @@ async function getEntries({
|
||||
.as("t");
|
||||
const { pk, name, ...transCol } = getColumns(transQ);
|
||||
|
||||
const entryProgressQ = getEntryProgressQ(userId);
|
||||
|
||||
const {
|
||||
kind,
|
||||
externalId,
|
||||
@ -198,7 +199,7 @@ async function getEntries({
|
||||
...entryCol,
|
||||
...transCol,
|
||||
videos: entryVideosQ.videos,
|
||||
progress: mapProgress(entryProgressQ, { aliased: true }),
|
||||
progress: mapProgress({ aliased: true }),
|
||||
// specials don't have an `episodeNumber` but a `number` field.
|
||||
number: episodeNumber,
|
||||
|
||||
@ -231,7 +232,8 @@ async function getEntries({
|
||||
: sortToSql(sort)),
|
||||
entries.pk,
|
||||
)
|
||||
.limit(limit);
|
||||
.limit(limit)
|
||||
.execute({ userId });
|
||||
}
|
||||
|
||||
export const entriesH = new Elysia({ tags: ["series"] })
|
||||
|
81
api/src/controllers/profiles/history.ts
Normal file
81
api/src/controllers/profiles/history.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { and, eq, isNotNull, ne } from "drizzle-orm";
|
||||
import Elysia, { t } from "elysia";
|
||||
import { auth } from "~/auth";
|
||||
import { entries } from "~/db/schema";
|
||||
import { Entry } from "~/models/entry";
|
||||
import {
|
||||
AcceptLanguage,
|
||||
Filter,
|
||||
Page,
|
||||
createPage,
|
||||
processLanguages,
|
||||
} from "~/models/utils";
|
||||
import { desc } from "~/models/utils/descriptions";
|
||||
import {
|
||||
entryFilters,
|
||||
entryProgressQ,
|
||||
entrySort,
|
||||
getEntries,
|
||||
} from "../entries";
|
||||
|
||||
export const historyH = new Elysia({ tags: ["profiles"] }).use(auth).guard(
|
||||
{
|
||||
query: t.Object({
|
||||
sort: {
|
||||
...entrySort,
|
||||
default: ["-playedDate"],
|
||||
},
|
||||
filter: t.Optional(Filter({ def: entryFilters })),
|
||||
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 })),
|
||||
}),
|
||||
},
|
||||
(app) =>
|
||||
app.get(
|
||||
"/profiles/me/history",
|
||||
async ({
|
||||
query: { sort, filter, query, limit, after },
|
||||
headers: { "accept-language": languages },
|
||||
request: { url },
|
||||
jwt: { sub },
|
||||
}) => {
|
||||
const langs = processLanguages(languages);
|
||||
const items = (await getEntries({
|
||||
limit,
|
||||
after,
|
||||
query,
|
||||
sort,
|
||||
filter: and(
|
||||
isNotNull(entryProgressQ.playedDate),
|
||||
ne(entries.kind, "extra"),
|
||||
ne(entries.kind, "unknown"),
|
||||
filter,
|
||||
),
|
||||
languages: langs,
|
||||
userId: sub,
|
||||
})) as Entry[];
|
||||
|
||||
return createPage(items, { url, sort, limit });
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
description: "List your watch history (episodes/movies seen)",
|
||||
},
|
||||
headers: t.Object(
|
||||
{
|
||||
"accept-language": AcceptLanguage({ autoFallback: true }),
|
||||
},
|
||||
{ additionalProperties: true },
|
||||
),
|
||||
response: {
|
||||
200: Page(Entry),
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
@ -36,7 +36,7 @@ import {
|
||||
} from "~/models/utils";
|
||||
import type { EmbeddedVideo } from "~/models/video";
|
||||
import { WatchlistStatus } from "~/models/watchlist";
|
||||
import { entryVideosQ, getEntryProgressQ, mapProgress } from "../entries";
|
||||
import { entryProgressQ, entryVideosQ, mapProgress } from "../entries";
|
||||
|
||||
export const watchStatusQ = db
|
||||
.select({
|
||||
@ -75,6 +75,7 @@ export const showFilters: FilterDef = {
|
||||
type: "enum",
|
||||
values: WatchlistStatus.enum,
|
||||
},
|
||||
score: { column: watchStatusQ.score, type: "int" },
|
||||
};
|
||||
export const showSort = Sort(
|
||||
{
|
||||
@ -86,6 +87,7 @@ export const showSort = Sort(
|
||||
createdAt: shows.createdAt,
|
||||
nextRefresh: shows.nextRefresh,
|
||||
watchStatus: watchStatusQ.status,
|
||||
score: watchStatusQ.score,
|
||||
},
|
||||
{
|
||||
default: ["slug"],
|
||||
@ -164,10 +166,7 @@ const showRelations = {
|
||||
.leftJoin(videos, eq(videos.pk, entryVideoJoin.videoPk))
|
||||
.as("videos");
|
||||
},
|
||||
firstEntry: ({
|
||||
languages,
|
||||
userId,
|
||||
}: { languages: string[]; userId: string }) => {
|
||||
firstEntry: ({ languages }: { languages: string[] }) => {
|
||||
const transQ = db
|
||||
.selectDistinctOn([entryTranslations.pk])
|
||||
.from(entryTranslations)
|
||||
@ -178,8 +177,6 @@ const showRelations = {
|
||||
.as("t");
|
||||
const { pk, ...transCol } = getColumns(transQ);
|
||||
|
||||
const progressQ = getEntryProgressQ(userId);
|
||||
|
||||
return db
|
||||
.select({
|
||||
firstEntry: jsonbBuildObject<Entry>({
|
||||
@ -187,12 +184,12 @@ const showRelations = {
|
||||
...transCol,
|
||||
number: entries.episodeNumber,
|
||||
videos: entryVideosQ.videos,
|
||||
progress: mapProgress(progressQ),
|
||||
progress: mapProgress({ aliased: false }),
|
||||
}).as("firstEntry"),
|
||||
})
|
||||
.from(entries)
|
||||
.innerJoin(transQ, eq(entries.pk, transQ.pk))
|
||||
.leftJoin(progressQ, eq(entries.pk, progressQ.entryPk))
|
||||
.leftJoin(entryProgressQ, eq(entries.pk, entryProgressQ.entryPk))
|
||||
.leftJoinLateral(entryVideosQ, sql`true`)
|
||||
.where(and(eq(entries.showPk, shows.pk), ne(entries.kind, "extra")))
|
||||
.orderBy(entries.order)
|
||||
@ -201,10 +198,8 @@ const showRelations = {
|
||||
},
|
||||
nextEntry: ({
|
||||
languages,
|
||||
userId,
|
||||
}: {
|
||||
languages: string[];
|
||||
userId: string;
|
||||
}) => {
|
||||
const transQ = db
|
||||
.selectDistinctOn([entryTranslations.pk])
|
||||
@ -216,8 +211,6 @@ const showRelations = {
|
||||
.as("t");
|
||||
const { pk, ...transCol } = getColumns(transQ);
|
||||
|
||||
const progressQ = getEntryProgressQ(userId);
|
||||
|
||||
return db
|
||||
.select({
|
||||
nextEntry: jsonbBuildObject<Entry>({
|
||||
@ -225,12 +218,12 @@ const showRelations = {
|
||||
...transCol,
|
||||
number: entries.episodeNumber,
|
||||
videos: entryVideosQ.videos,
|
||||
progress: mapProgress(progressQ),
|
||||
progress: mapProgress({ aliased: false }),
|
||||
}).as("nextEntry"),
|
||||
})
|
||||
.from(entries)
|
||||
.innerJoin(transQ, eq(entries.pk, transQ.pk))
|
||||
.leftJoin(progressQ, eq(entries.pk, progressQ.entryPk))
|
||||
.leftJoin(entryProgressQ, eq(entries.pk, entryProgressQ.entryPk))
|
||||
.leftJoinLateral(entryVideosQ, sql`true`)
|
||||
.where(eq(watchStatusQ.nextEntry, entries.pk))
|
||||
.as("nextEntry");
|
||||
@ -294,7 +287,7 @@ export async function getShows({
|
||||
|
||||
watchStatus: getColumns(watchStatusQ),
|
||||
|
||||
...buildRelations(relations, showRelations, { languages, userId }),
|
||||
...buildRelations(relations, showRelations, { languages }),
|
||||
})
|
||||
.from(shows)
|
||||
.leftJoin(watchStatusQ, eq(shows.pk, watchStatusQ.showPk))
|
||||
|
@ -12,6 +12,7 @@ export const Progress = t.Object({
|
||||
`,
|
||||
}),
|
||||
),
|
||||
playedDate: t.Nullable(t.String({ format: "date-time" })),
|
||||
videoId: t.Nullable(
|
||||
t.String({
|
||||
format: "uuid",
|
||||
|
Loading…
x
Reference in New Issue
Block a user