Add progress status in every entry

This commit is contained in:
Zoe Roux 2025-03-11 15:20:33 +01:00
parent 781a6a8196
commit 6ecaec2dee
No known key found for this signature in database
8 changed files with 79 additions and 20 deletions

View File

@ -1,10 +1,11 @@
import { type SQL, and, eq, isNotNull, ne, sql } from "drizzle-orm";
import { type SQL, and, desc, eq, isNotNull, ne, sql } from "drizzle-orm";
import { Elysia, t } from "elysia";
import { db } from "~/db";
import {
entries,
entryTranslations,
entryVideoJoin,
history,
shows,
videos,
} from "~/db/schema";
@ -39,7 +40,7 @@ import {
processLanguages,
sortToSql,
} from "~/models/utils";
import { desc } from "~/models/utils/descriptions";
import { desc as description } from "~/models/utils/descriptions";
import type { EmbeddedVideo } from "~/models/video";
const entryFilters: FilterDef = {
@ -149,6 +150,18 @@ async function getEntries({
.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)
.leftJoin(videos, eq(history.videoPk, videos.pk))
.orderBy(history.entryPk, desc(history.playedDate))
.as("progress");
const {
kind,
externalId,
@ -163,6 +176,9 @@ async function getEntries({
...entryCol,
...transCol,
videos: videosQ.videos,
progress: {
...getColumns(progressQ),
},
// specials don't have an `episodeNumber` but a `number` field.
number: episodeNumber,
@ -181,6 +197,7 @@ async function getEntries({
.from(entries)
.innerJoin(transQ, eq(entries.pk, transQ.pk))
.leftJoinLateral(videosQ, sql`true`)
.leftJoin(progressQ, eq(entries.pk, progressQ.entryPk))
.where(
and(
filter,
@ -265,14 +282,14 @@ export const entriesH = new Elysia({ tags: ["series"] })
query: t.Object({
sort: entrySort,
filter: t.Optional(Filter({ def: entryFilters })),
query: t.Optional(t.String({ description: desc.query })),
query: t.Optional(t.String({ description: description.query })),
limit: t.Integer({
minimum: 1,
maximum: 250,
default: 50,
description: "Max page size.",
}),
after: t.Optional(t.String({ description: desc.after })),
after: t.Optional(t.String({ description: description.after })),
}),
headers: t.Object(
{
@ -342,14 +359,14 @@ export const entriesH = new Elysia({ tags: ["series"] })
query: t.Object({
sort: extraSort,
filter: t.Optional(Filter({ def: extraFilters })),
query: t.Optional(t.String({ description: desc.query })),
query: t.Optional(t.String({ description: description.query })),
limit: t.Integer({
minimum: 1,
maximum: 250,
default: 50,
description: "Max page size.",
}),
after: t.Optional(t.String({ description: desc.after })),
after: t.Optional(t.String({ description: description.after })),
}),
response: {
200: Page(Extra),
@ -383,14 +400,14 @@ export const entriesH = new Elysia({ tags: ["series"] })
query: t.Object({
sort: extraSort,
filter: t.Optional(Filter({ def: unknownFilters })),
query: t.Optional(t.String({ description: desc.query })),
query: t.Optional(t.String({ description: description.query })),
limit: t.Integer({
minimum: 1,
maximum: 250,
default: 50,
description: "Max page size.",
}),
after: t.Optional(t.String({ description: desc.after })),
after: t.Optional(t.String({ description: description.after })),
}),
response: {
200: Page(UnknownEntry),
@ -423,14 +440,14 @@ export const entriesH = new Elysia({ tags: ["series"] })
detail: { description: "Get new movies/episodes added recently." },
query: t.Object({
filter: t.Optional(Filter({ def: entryFilters })),
query: t.Optional(t.String({ description: desc.query })),
query: t.Optional(t.String({ description: description.query })),
limit: t.Integer({
minimum: 1,
maximum: 250,
default: 50,
description: "Max page size.",
}),
after: t.Optional(t.String({ description: desc.after })),
after: t.Optional(t.String({ description: description.after })),
}),
response: {
200: Page(Entry),

View File

@ -1,5 +1,5 @@
import { index, integer, jsonb, timestamp } from "drizzle-orm/pg-core";
import type { Progress } from "~/models/watchlist";
import { sql } from "drizzle-orm";
import { check, index, integer, jsonb, timestamp } from "drizzle-orm/pg-core";
import { entries } from "./entries";
import { profiles } from "./profiles";
import { schema } from "./utils";
@ -18,8 +18,13 @@ export const history = schema.table(
videoPk: integer()
.notNull()
.references(() => videos.pk, { onDelete: "set null" }),
progress: jsonb().$type<Progress>(),
percent: integer().notNull().default(0),
time: integer(),
playedDate: timestamp({ mode: "string" }).notNull().defaultNow(),
},
(t) => [index("history_play_date").on(t.playedDate.desc())],
(t) => [
index("history_play_date").on(t.playedDate.desc()),
check("percent_valid", sql`${t.percent} between 0 and 100`),
],
);

View File

@ -10,6 +10,7 @@ import {
} from "../utils";
import { EmbeddedVideo } from "../video";
import { BaseEntry, EntryTranslation } from "./base-entry";
import { Progress } from "../watchlist";
export const BaseEpisode = t.Intersect([
t.Object({
@ -27,7 +28,8 @@ export const Episode = t.Intersect([
EntryTranslation(),
BaseEpisode,
t.Object({
videos: t.Optional(t.Array(EmbeddedVideo)),
videos: t.Array(EmbeddedVideo),
progress: Progress,
}),
DbMetadata,
]);

View File

@ -4,6 +4,7 @@ import { madeInAbyss, registerExamples } from "../examples";
import { DbMetadata, SeedImage } from "../utils";
import { Resource } from "../utils/resource";
import { BaseEntry } from "./base-entry";
import { Progress } from "../watchlist";
export const ExtraType = t.UnionEnum([
"other",
@ -31,7 +32,14 @@ export const BaseExtra = t.Intersect(
},
);
export const Extra = t.Intersect([Resource(), BaseExtra, DbMetadata]);
export const Extra = t.Intersect([
Resource(),
BaseExtra,
t.Object({
progress: t.Omit(Progress, ["videoId"]),
}),
DbMetadata,
]);
export type Extra = Prettify<typeof Extra.static>;
export const SeedExtra = t.Intersect([

View File

@ -11,6 +11,7 @@ import {
} from "../utils";
import { EmbeddedVideo } from "../video";
import { BaseEntry, EntryTranslation } from "./base-entry";
import { Progress } from "../watchlist";
export const BaseMovieEntry = t.Intersect(
[
@ -46,6 +47,7 @@ export const MovieEntry = t.Intersect([
BaseMovieEntry,
t.Object({
videos: t.Optional(t.Array(EmbeddedVideo)),
progress: Progress,
}),
DbMetadata,
]);

View File

@ -10,6 +10,7 @@ import {
} from "../utils";
import { EmbeddedVideo } from "../video";
import { BaseEntry, EntryTranslation } from "./base-entry";
import { Progress } from "../watchlist";
export const BaseSpecial = t.Intersect(
[
@ -38,6 +39,7 @@ export const Special = t.Intersect([
BaseSpecial,
t.Object({
videos: t.Optional(t.Array(EmbeddedVideo)),
progress: Progress,
}),
DbMetadata,
]);

View File

@ -2,6 +2,7 @@ import { t } from "elysia";
import { type Prettify, comment } from "~/utils";
import { bubbleImages, registerExamples, youtubeExample } from "../examples";
import { DbMetadata, Resource } from "../utils";
import { Progress } from "../watchlist";
import { BaseEntry, EntryTranslation } from "./base-entry";
export const BaseUnknownEntry = t.Intersect(
@ -27,6 +28,9 @@ export const UnknownEntry = t.Intersect([
Resource(),
UnknownEntryTranslation,
BaseUnknownEntry,
t.Object({
progress: t.Omit(Progress, ["videoId"]),
}),
DbMetadata,
]);
export type UnknownEntry = Prettify<typeof UnknownEntry.static>;

View File

@ -1,10 +1,29 @@
import { t } from "elysia";
import { comment } from "~/utils";
export const Progress = t.Object({
percent: t.Integer({ minimum: 0, maximum: 100 }),
time: t.Number({
minimum: 0,
description: "When this episode was stopped (in seconds since the start",
}),
time: t.Nullable(
t.Integer({
minimum: 0,
description: comment`
When this episode was stopped (in seconds since the start).
This value is null if the entry was never watched or is finished.
`,
}),
),
videoId: t.Nullable(
t.String({
format: "uuid",
description: comment`
Id of the video the user watched.
This can be used to resume playback in the correct video file
without asking the user what video to play.
This will be null if the user did not watch the entry or
if the video was deleted since.
`,
}),
),
});
export type Progress = typeof Progress.static;