mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-31 12:14:46 -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 { desc as description } from "~/models/utils/descriptions";
|
||||||
import type { EmbeddedVideo } from "~/models/video";
|
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: {
|
kind: {
|
||||||
column: entries.kind,
|
column: entries.kind,
|
||||||
type: "enum",
|
type: "enum",
|
||||||
@ -57,18 +72,21 @@ const entryFilters: FilterDef = {
|
|||||||
order: { column: entries.order, type: "float" },
|
order: { column: entries.order, type: "float" },
|
||||||
runtime: { column: entries.runtime, type: "float" },
|
runtime: { column: entries.runtime, type: "float" },
|
||||||
airDate: { column: entries.airDate, type: "date" },
|
airDate: { column: entries.airDate, type: "date" },
|
||||||
|
playedDate: { column: entryProgressQ.playedDate, type: "date" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const extraFilters: FilterDef = {
|
const extraFilters: FilterDef = {
|
||||||
kind: { column: entries.extraKind, type: "enum", values: ExtraType.enum },
|
kind: { column: entries.extraKind, type: "enum", values: ExtraType.enum },
|
||||||
runtime: { column: entries.runtime, type: "float" },
|
runtime: { column: entries.runtime, type: "float" },
|
||||||
|
playedDate: { column: entryProgressQ.playedDate, type: "date" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const unknownFilters: FilterDef = {
|
const unknownFilters: FilterDef = {
|
||||||
runtime: { column: entries.runtime, type: "float" },
|
runtime: { column: entries.runtime, type: "float" },
|
||||||
|
playedDate: { column: entryProgressQ.playedDate, type: "date" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const entrySort = Sort(
|
export const entrySort = Sort(
|
||||||
{
|
{
|
||||||
order: entries.order,
|
order: entries.order,
|
||||||
seasonNumber: entries.seasonNumber,
|
seasonNumber: entries.seasonNumber,
|
||||||
@ -76,6 +94,7 @@ const entrySort = Sort(
|
|||||||
number: entries.episodeNumber,
|
number: entries.episodeNumber,
|
||||||
airDate: entries.airDate,
|
airDate: entries.airDate,
|
||||||
nextRefresh: entries.nextRefresh,
|
nextRefresh: entries.nextRefresh,
|
||||||
|
playedDate: entryProgressQ.playedDate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: ["order"],
|
default: ["order"],
|
||||||
@ -89,6 +108,7 @@ const extraSort = Sort(
|
|||||||
name: entryTranslations.name,
|
name: entryTranslations.name,
|
||||||
runtime: entries.runtime,
|
runtime: entries.runtime,
|
||||||
createdAt: entries.createdAt,
|
createdAt: entries.createdAt,
|
||||||
|
playedDate: entryProgressQ.playedDate,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: ["slug"],
|
default: ["slug"],
|
||||||
@ -126,36 +146,19 @@ export const entryVideosQ = db
|
|||||||
.leftJoin(videos, eq(videos.pk, entryVideoJoin.videoPk))
|
.leftJoin(videos, eq(videos.pk, entryVideoJoin.videoPk))
|
||||||
.as("videos");
|
.as("videos");
|
||||||
|
|
||||||
export const getEntryProgressQ = (userId: string) =>
|
export const mapProgress = ({ aliased }: { aliased: boolean }) => {
|
||||||
db
|
const { time, percent, playedDate, videoId } = getColumns(entryProgressQ);
|
||||||
.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);
|
|
||||||
const ret = {
|
const ret = {
|
||||||
time: coalesce(time, sql`0`),
|
time: coalesce(time, sql`0`),
|
||||||
percent: coalesce(percent, sql`0`),
|
percent: coalesce(percent, sql`0`),
|
||||||
|
playedDate: sql`${playedDate}`,
|
||||||
videoId: sql`${videoId}`,
|
videoId: sql`${videoId}`,
|
||||||
};
|
};
|
||||||
if (!aliased) return ret;
|
if (!aliased) return ret;
|
||||||
return Object.fromEntries(Object.entries(ret).map(([k, v]) => [k, v.as(k)]));
|
return Object.fromEntries(Object.entries(ret).map(([k, v]) => [k, v.as(k)]));
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getEntries({
|
export async function getEntries({
|
||||||
after,
|
after,
|
||||||
limit,
|
limit,
|
||||||
query,
|
query,
|
||||||
@ -182,8 +185,6 @@ async function getEntries({
|
|||||||
.as("t");
|
.as("t");
|
||||||
const { pk, name, ...transCol } = getColumns(transQ);
|
const { pk, name, ...transCol } = getColumns(transQ);
|
||||||
|
|
||||||
const entryProgressQ = getEntryProgressQ(userId);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
kind,
|
kind,
|
||||||
externalId,
|
externalId,
|
||||||
@ -198,7 +199,7 @@ async function getEntries({
|
|||||||
...entryCol,
|
...entryCol,
|
||||||
...transCol,
|
...transCol,
|
||||||
videos: entryVideosQ.videos,
|
videos: entryVideosQ.videos,
|
||||||
progress: mapProgress(entryProgressQ, { aliased: true }),
|
progress: mapProgress({ aliased: true }),
|
||||||
// specials don't have an `episodeNumber` but a `number` field.
|
// specials don't have an `episodeNumber` but a `number` field.
|
||||||
number: episodeNumber,
|
number: episodeNumber,
|
||||||
|
|
||||||
@ -231,7 +232,8 @@ async function getEntries({
|
|||||||
: sortToSql(sort)),
|
: sortToSql(sort)),
|
||||||
entries.pk,
|
entries.pk,
|
||||||
)
|
)
|
||||||
.limit(limit);
|
.limit(limit)
|
||||||
|
.execute({ userId });
|
||||||
}
|
}
|
||||||
|
|
||||||
export const entriesH = new Elysia({ tags: ["series"] })
|
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";
|
} from "~/models/utils";
|
||||||
import type { EmbeddedVideo } from "~/models/video";
|
import type { EmbeddedVideo } from "~/models/video";
|
||||||
import { WatchlistStatus } from "~/models/watchlist";
|
import { WatchlistStatus } from "~/models/watchlist";
|
||||||
import { entryVideosQ, getEntryProgressQ, mapProgress } from "../entries";
|
import { entryProgressQ, entryVideosQ, mapProgress } from "../entries";
|
||||||
|
|
||||||
export const watchStatusQ = db
|
export const watchStatusQ = db
|
||||||
.select({
|
.select({
|
||||||
@ -75,6 +75,7 @@ export const showFilters: FilterDef = {
|
|||||||
type: "enum",
|
type: "enum",
|
||||||
values: WatchlistStatus.enum,
|
values: WatchlistStatus.enum,
|
||||||
},
|
},
|
||||||
|
score: { column: watchStatusQ.score, type: "int" },
|
||||||
};
|
};
|
||||||
export const showSort = Sort(
|
export const showSort = Sort(
|
||||||
{
|
{
|
||||||
@ -86,6 +87,7 @@ export const showSort = Sort(
|
|||||||
createdAt: shows.createdAt,
|
createdAt: shows.createdAt,
|
||||||
nextRefresh: shows.nextRefresh,
|
nextRefresh: shows.nextRefresh,
|
||||||
watchStatus: watchStatusQ.status,
|
watchStatus: watchStatusQ.status,
|
||||||
|
score: watchStatusQ.score,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: ["slug"],
|
default: ["slug"],
|
||||||
@ -164,10 +166,7 @@ const showRelations = {
|
|||||||
.leftJoin(videos, eq(videos.pk, entryVideoJoin.videoPk))
|
.leftJoin(videos, eq(videos.pk, entryVideoJoin.videoPk))
|
||||||
.as("videos");
|
.as("videos");
|
||||||
},
|
},
|
||||||
firstEntry: ({
|
firstEntry: ({ languages }: { languages: string[] }) => {
|
||||||
languages,
|
|
||||||
userId,
|
|
||||||
}: { languages: string[]; userId: string }) => {
|
|
||||||
const transQ = db
|
const transQ = db
|
||||||
.selectDistinctOn([entryTranslations.pk])
|
.selectDistinctOn([entryTranslations.pk])
|
||||||
.from(entryTranslations)
|
.from(entryTranslations)
|
||||||
@ -178,8 +177,6 @@ const showRelations = {
|
|||||||
.as("t");
|
.as("t");
|
||||||
const { pk, ...transCol } = getColumns(transQ);
|
const { pk, ...transCol } = getColumns(transQ);
|
||||||
|
|
||||||
const progressQ = getEntryProgressQ(userId);
|
|
||||||
|
|
||||||
return db
|
return db
|
||||||
.select({
|
.select({
|
||||||
firstEntry: jsonbBuildObject<Entry>({
|
firstEntry: jsonbBuildObject<Entry>({
|
||||||
@ -187,12 +184,12 @@ const showRelations = {
|
|||||||
...transCol,
|
...transCol,
|
||||||
number: entries.episodeNumber,
|
number: entries.episodeNumber,
|
||||||
videos: entryVideosQ.videos,
|
videos: entryVideosQ.videos,
|
||||||
progress: mapProgress(progressQ),
|
progress: mapProgress({ aliased: false }),
|
||||||
}).as("firstEntry"),
|
}).as("firstEntry"),
|
||||||
})
|
})
|
||||||
.from(entries)
|
.from(entries)
|
||||||
.innerJoin(transQ, eq(entries.pk, transQ.pk))
|
.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`)
|
.leftJoinLateral(entryVideosQ, sql`true`)
|
||||||
.where(and(eq(entries.showPk, shows.pk), ne(entries.kind, "extra")))
|
.where(and(eq(entries.showPk, shows.pk), ne(entries.kind, "extra")))
|
||||||
.orderBy(entries.order)
|
.orderBy(entries.order)
|
||||||
@ -201,10 +198,8 @@ const showRelations = {
|
|||||||
},
|
},
|
||||||
nextEntry: ({
|
nextEntry: ({
|
||||||
languages,
|
languages,
|
||||||
userId,
|
|
||||||
}: {
|
}: {
|
||||||
languages: string[];
|
languages: string[];
|
||||||
userId: string;
|
|
||||||
}) => {
|
}) => {
|
||||||
const transQ = db
|
const transQ = db
|
||||||
.selectDistinctOn([entryTranslations.pk])
|
.selectDistinctOn([entryTranslations.pk])
|
||||||
@ -216,8 +211,6 @@ const showRelations = {
|
|||||||
.as("t");
|
.as("t");
|
||||||
const { pk, ...transCol } = getColumns(transQ);
|
const { pk, ...transCol } = getColumns(transQ);
|
||||||
|
|
||||||
const progressQ = getEntryProgressQ(userId);
|
|
||||||
|
|
||||||
return db
|
return db
|
||||||
.select({
|
.select({
|
||||||
nextEntry: jsonbBuildObject<Entry>({
|
nextEntry: jsonbBuildObject<Entry>({
|
||||||
@ -225,12 +218,12 @@ const showRelations = {
|
|||||||
...transCol,
|
...transCol,
|
||||||
number: entries.episodeNumber,
|
number: entries.episodeNumber,
|
||||||
videos: entryVideosQ.videos,
|
videos: entryVideosQ.videos,
|
||||||
progress: mapProgress(progressQ),
|
progress: mapProgress({ aliased: false }),
|
||||||
}).as("nextEntry"),
|
}).as("nextEntry"),
|
||||||
})
|
})
|
||||||
.from(entries)
|
.from(entries)
|
||||||
.innerJoin(transQ, eq(entries.pk, transQ.pk))
|
.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`)
|
.leftJoinLateral(entryVideosQ, sql`true`)
|
||||||
.where(eq(watchStatusQ.nextEntry, entries.pk))
|
.where(eq(watchStatusQ.nextEntry, entries.pk))
|
||||||
.as("nextEntry");
|
.as("nextEntry");
|
||||||
@ -294,7 +287,7 @@ export async function getShows({
|
|||||||
|
|
||||||
watchStatus: getColumns(watchStatusQ),
|
watchStatus: getColumns(watchStatusQ),
|
||||||
|
|
||||||
...buildRelations(relations, showRelations, { languages, userId }),
|
...buildRelations(relations, showRelations, { languages }),
|
||||||
})
|
})
|
||||||
.from(shows)
|
.from(shows)
|
||||||
.leftJoin(watchStatusQ, eq(shows.pk, watchStatusQ.showPk))
|
.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(
|
videoId: t.Nullable(
|
||||||
t.String({
|
t.String({
|
||||||
format: "uuid",
|
format: "uuid",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user