Create history api

This commit is contained in:
Zoe Roux 2025-04-07 18:22:24 +02:00
parent df7d109c34
commit 4ba7750012
No known key found for this signature in database
4 changed files with 120 additions and 43 deletions

View File

@ -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"] })

View 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),
},
},
),
);

View File

@ -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))

View File

@ -12,6 +12,7 @@ export const Progress = t.Object({
`,
}),
),
playedDate: t.Nullable(t.String({ format: "date-time" })),
videoId: t.Nullable(
t.String({
format: "uuid",