mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-31 20:24:27 -04:00
Add progress status in every entry
This commit is contained in:
parent
781a6a8196
commit
6ecaec2dee
@ -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 { Elysia, t } from "elysia";
|
||||||
import { db } from "~/db";
|
import { db } from "~/db";
|
||||||
import {
|
import {
|
||||||
entries,
|
entries,
|
||||||
entryTranslations,
|
entryTranslations,
|
||||||
entryVideoJoin,
|
entryVideoJoin,
|
||||||
|
history,
|
||||||
shows,
|
shows,
|
||||||
videos,
|
videos,
|
||||||
} from "~/db/schema";
|
} from "~/db/schema";
|
||||||
@ -39,7 +40,7 @@ import {
|
|||||||
processLanguages,
|
processLanguages,
|
||||||
sortToSql,
|
sortToSql,
|
||||||
} from "~/models/utils";
|
} from "~/models/utils";
|
||||||
import { desc } 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 = {
|
const entryFilters: FilterDef = {
|
||||||
@ -149,6 +150,18 @@ async function getEntries({
|
|||||||
.leftJoin(videos, eq(videos.pk, entryVideoJoin.videoPk))
|
.leftJoin(videos, eq(videos.pk, entryVideoJoin.videoPk))
|
||||||
.as("videos");
|
.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 {
|
const {
|
||||||
kind,
|
kind,
|
||||||
externalId,
|
externalId,
|
||||||
@ -163,6 +176,9 @@ async function getEntries({
|
|||||||
...entryCol,
|
...entryCol,
|
||||||
...transCol,
|
...transCol,
|
||||||
videos: videosQ.videos,
|
videos: videosQ.videos,
|
||||||
|
progress: {
|
||||||
|
...getColumns(progressQ),
|
||||||
|
},
|
||||||
// specials don't have an `episodeNumber` but a `number` field.
|
// specials don't have an `episodeNumber` but a `number` field.
|
||||||
number: episodeNumber,
|
number: episodeNumber,
|
||||||
|
|
||||||
@ -181,6 +197,7 @@ async function getEntries({
|
|||||||
.from(entries)
|
.from(entries)
|
||||||
.innerJoin(transQ, eq(entries.pk, transQ.pk))
|
.innerJoin(transQ, eq(entries.pk, transQ.pk))
|
||||||
.leftJoinLateral(videosQ, sql`true`)
|
.leftJoinLateral(videosQ, sql`true`)
|
||||||
|
.leftJoin(progressQ, eq(entries.pk, progressQ.entryPk))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
filter,
|
filter,
|
||||||
@ -265,14 +282,14 @@ export const entriesH = new Elysia({ tags: ["series"] })
|
|||||||
query: t.Object({
|
query: t.Object({
|
||||||
sort: entrySort,
|
sort: entrySort,
|
||||||
filter: t.Optional(Filter({ def: entryFilters })),
|
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({
|
limit: t.Integer({
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
maximum: 250,
|
maximum: 250,
|
||||||
default: 50,
|
default: 50,
|
||||||
description: "Max page size.",
|
description: "Max page size.",
|
||||||
}),
|
}),
|
||||||
after: t.Optional(t.String({ description: desc.after })),
|
after: t.Optional(t.String({ description: description.after })),
|
||||||
}),
|
}),
|
||||||
headers: t.Object(
|
headers: t.Object(
|
||||||
{
|
{
|
||||||
@ -342,14 +359,14 @@ export const entriesH = new Elysia({ tags: ["series"] })
|
|||||||
query: t.Object({
|
query: t.Object({
|
||||||
sort: extraSort,
|
sort: extraSort,
|
||||||
filter: t.Optional(Filter({ def: extraFilters })),
|
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({
|
limit: t.Integer({
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
maximum: 250,
|
maximum: 250,
|
||||||
default: 50,
|
default: 50,
|
||||||
description: "Max page size.",
|
description: "Max page size.",
|
||||||
}),
|
}),
|
||||||
after: t.Optional(t.String({ description: desc.after })),
|
after: t.Optional(t.String({ description: description.after })),
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: Page(Extra),
|
200: Page(Extra),
|
||||||
@ -383,14 +400,14 @@ export const entriesH = new Elysia({ tags: ["series"] })
|
|||||||
query: t.Object({
|
query: t.Object({
|
||||||
sort: extraSort,
|
sort: extraSort,
|
||||||
filter: t.Optional(Filter({ def: unknownFilters })),
|
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({
|
limit: t.Integer({
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
maximum: 250,
|
maximum: 250,
|
||||||
default: 50,
|
default: 50,
|
||||||
description: "Max page size.",
|
description: "Max page size.",
|
||||||
}),
|
}),
|
||||||
after: t.Optional(t.String({ description: desc.after })),
|
after: t.Optional(t.String({ description: description.after })),
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: Page(UnknownEntry),
|
200: Page(UnknownEntry),
|
||||||
@ -423,14 +440,14 @@ export const entriesH = new Elysia({ tags: ["series"] })
|
|||||||
detail: { description: "Get new movies/episodes added recently." },
|
detail: { description: "Get new movies/episodes added recently." },
|
||||||
query: t.Object({
|
query: t.Object({
|
||||||
filter: t.Optional(Filter({ def: entryFilters })),
|
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({
|
limit: t.Integer({
|
||||||
minimum: 1,
|
minimum: 1,
|
||||||
maximum: 250,
|
maximum: 250,
|
||||||
default: 50,
|
default: 50,
|
||||||
description: "Max page size.",
|
description: "Max page size.",
|
||||||
}),
|
}),
|
||||||
after: t.Optional(t.String({ description: desc.after })),
|
after: t.Optional(t.String({ description: description.after })),
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: Page(Entry),
|
200: Page(Entry),
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { index, integer, jsonb, timestamp } from "drizzle-orm/pg-core";
|
import { sql } from "drizzle-orm";
|
||||||
import type { Progress } from "~/models/watchlist";
|
import { check, index, integer, jsonb, timestamp } from "drizzle-orm/pg-core";
|
||||||
import { entries } from "./entries";
|
import { entries } from "./entries";
|
||||||
import { profiles } from "./profiles";
|
import { profiles } from "./profiles";
|
||||||
import { schema } from "./utils";
|
import { schema } from "./utils";
|
||||||
@ -18,8 +18,13 @@ export const history = schema.table(
|
|||||||
videoPk: integer()
|
videoPk: integer()
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => videos.pk, { onDelete: "set null" }),
|
.references(() => videos.pk, { onDelete: "set null" }),
|
||||||
progress: jsonb().$type<Progress>(),
|
percent: integer().notNull().default(0),
|
||||||
|
time: integer(),
|
||||||
playedDate: timestamp({ mode: "string" }).notNull().defaultNow(),
|
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`),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { EmbeddedVideo } from "../video";
|
import { EmbeddedVideo } from "../video";
|
||||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||||
|
import { Progress } from "../watchlist";
|
||||||
|
|
||||||
export const BaseEpisode = t.Intersect([
|
export const BaseEpisode = t.Intersect([
|
||||||
t.Object({
|
t.Object({
|
||||||
@ -27,7 +28,8 @@ export const Episode = t.Intersect([
|
|||||||
EntryTranslation(),
|
EntryTranslation(),
|
||||||
BaseEpisode,
|
BaseEpisode,
|
||||||
t.Object({
|
t.Object({
|
||||||
videos: t.Optional(t.Array(EmbeddedVideo)),
|
videos: t.Array(EmbeddedVideo),
|
||||||
|
progress: Progress,
|
||||||
}),
|
}),
|
||||||
DbMetadata,
|
DbMetadata,
|
||||||
]);
|
]);
|
||||||
|
@ -4,6 +4,7 @@ import { madeInAbyss, registerExamples } from "../examples";
|
|||||||
import { DbMetadata, SeedImage } from "../utils";
|
import { DbMetadata, SeedImage } from "../utils";
|
||||||
import { Resource } from "../utils/resource";
|
import { Resource } from "../utils/resource";
|
||||||
import { BaseEntry } from "./base-entry";
|
import { BaseEntry } from "./base-entry";
|
||||||
|
import { Progress } from "../watchlist";
|
||||||
|
|
||||||
export const ExtraType = t.UnionEnum([
|
export const ExtraType = t.UnionEnum([
|
||||||
"other",
|
"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 type Extra = Prettify<typeof Extra.static>;
|
||||||
|
|
||||||
export const SeedExtra = t.Intersect([
|
export const SeedExtra = t.Intersect([
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { EmbeddedVideo } from "../video";
|
import { EmbeddedVideo } from "../video";
|
||||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||||
|
import { Progress } from "../watchlist";
|
||||||
|
|
||||||
export const BaseMovieEntry = t.Intersect(
|
export const BaseMovieEntry = t.Intersect(
|
||||||
[
|
[
|
||||||
@ -46,6 +47,7 @@ export const MovieEntry = t.Intersect([
|
|||||||
BaseMovieEntry,
|
BaseMovieEntry,
|
||||||
t.Object({
|
t.Object({
|
||||||
videos: t.Optional(t.Array(EmbeddedVideo)),
|
videos: t.Optional(t.Array(EmbeddedVideo)),
|
||||||
|
progress: Progress,
|
||||||
}),
|
}),
|
||||||
DbMetadata,
|
DbMetadata,
|
||||||
]);
|
]);
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { EmbeddedVideo } from "../video";
|
import { EmbeddedVideo } from "../video";
|
||||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||||
|
import { Progress } from "../watchlist";
|
||||||
|
|
||||||
export const BaseSpecial = t.Intersect(
|
export const BaseSpecial = t.Intersect(
|
||||||
[
|
[
|
||||||
@ -38,6 +39,7 @@ export const Special = t.Intersect([
|
|||||||
BaseSpecial,
|
BaseSpecial,
|
||||||
t.Object({
|
t.Object({
|
||||||
videos: t.Optional(t.Array(EmbeddedVideo)),
|
videos: t.Optional(t.Array(EmbeddedVideo)),
|
||||||
|
progress: Progress,
|
||||||
}),
|
}),
|
||||||
DbMetadata,
|
DbMetadata,
|
||||||
]);
|
]);
|
||||||
|
@ -2,6 +2,7 @@ import { t } from "elysia";
|
|||||||
import { type Prettify, comment } from "~/utils";
|
import { type Prettify, comment } from "~/utils";
|
||||||
import { bubbleImages, registerExamples, youtubeExample } from "../examples";
|
import { bubbleImages, registerExamples, youtubeExample } from "../examples";
|
||||||
import { DbMetadata, Resource } from "../utils";
|
import { DbMetadata, Resource } from "../utils";
|
||||||
|
import { Progress } from "../watchlist";
|
||||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||||
|
|
||||||
export const BaseUnknownEntry = t.Intersect(
|
export const BaseUnknownEntry = t.Intersect(
|
||||||
@ -27,6 +28,9 @@ export const UnknownEntry = t.Intersect([
|
|||||||
Resource(),
|
Resource(),
|
||||||
UnknownEntryTranslation,
|
UnknownEntryTranslation,
|
||||||
BaseUnknownEntry,
|
BaseUnknownEntry,
|
||||||
|
t.Object({
|
||||||
|
progress: t.Omit(Progress, ["videoId"]),
|
||||||
|
}),
|
||||||
DbMetadata,
|
DbMetadata,
|
||||||
]);
|
]);
|
||||||
export type UnknownEntry = Prettify<typeof UnknownEntry.static>;
|
export type UnknownEntry = Prettify<typeof UnknownEntry.static>;
|
||||||
|
@ -1,10 +1,29 @@
|
|||||||
import { t } from "elysia";
|
import { t } from "elysia";
|
||||||
|
import { comment } from "~/utils";
|
||||||
|
|
||||||
export const Progress = t.Object({
|
export const Progress = t.Object({
|
||||||
percent: t.Integer({ minimum: 0, maximum: 100 }),
|
percent: t.Integer({ minimum: 0, maximum: 100 }),
|
||||||
time: t.Number({
|
time: t.Nullable(
|
||||||
minimum: 0,
|
t.Integer({
|
||||||
description: "When this episode was stopped (in seconds since the start",
|
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;
|
export type Progress = typeof Progress.static;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user