mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add route to batch populate the history
This commit is contained in:
parent
fef9e844a1
commit
e2fa3af1e8
@ -2,6 +2,7 @@ import { Elysia, t } from "elysia";
|
|||||||
import { auth } from "./auth";
|
import { auth } from "./auth";
|
||||||
import { entriesH } from "./controllers/entries";
|
import { entriesH } from "./controllers/entries";
|
||||||
import { imagesH } from "./controllers/images";
|
import { imagesH } from "./controllers/images";
|
||||||
|
import { historyH } from "./controllers/profiles/history";
|
||||||
import { watchlistH } from "./controllers/profiles/watchlist";
|
import { watchlistH } from "./controllers/profiles/watchlist";
|
||||||
import { seasonsH } from "./controllers/seasons";
|
import { seasonsH } from "./controllers/seasons";
|
||||||
import { seed } from "./controllers/seed";
|
import { seed } from "./controllers/seed";
|
||||||
@ -95,4 +96,5 @@ export const app = new Elysia({ prefix })
|
|||||||
},
|
},
|
||||||
(app) => app.use(videosH).use(seed),
|
(app) => app.use(videosH).use(seed),
|
||||||
)
|
)
|
||||||
.use(watchlistH);
|
.use(watchlistH)
|
||||||
|
.use(historyH);
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
import { and, isNotNull, ne } from "drizzle-orm";
|
import { and, eq, isNotNull, ne, not, or, sql } from "drizzle-orm";
|
||||||
import Elysia, { t } from "elysia";
|
import Elysia, { t } from "elysia";
|
||||||
import { auth, getUserInfo } from "~/auth";
|
import { auth, getUserInfo } from "~/auth";
|
||||||
import { entries } from "~/db/schema";
|
import { db } from "~/db";
|
||||||
|
import { entries, history, videos } from "~/db/schema";
|
||||||
|
import { values } from "~/db/utils";
|
||||||
import { Entry } from "~/models/entry";
|
import { Entry } from "~/models/entry";
|
||||||
import { KError } from "~/models/error";
|
import { KError } from "~/models/error";
|
||||||
|
import { SeedHistory } from "~/models/history";
|
||||||
import {
|
import {
|
||||||
AcceptLanguage,
|
AcceptLanguage,
|
||||||
Filter,
|
Filter,
|
||||||
Page,
|
Page,
|
||||||
createPage,
|
createPage,
|
||||||
|
isUuid,
|
||||||
processLanguages,
|
processLanguages,
|
||||||
} from "~/models/utils";
|
} from "~/models/utils";
|
||||||
import { desc } from "~/models/utils/descriptions";
|
import { desc } from "~/models/utils/descriptions";
|
||||||
@ -18,8 +22,11 @@ import {
|
|||||||
entrySort,
|
entrySort,
|
||||||
getEntries,
|
getEntries,
|
||||||
} from "../entries";
|
} from "../entries";
|
||||||
|
import { getOrCreateProfile } from "./profile";
|
||||||
|
|
||||||
export const historyH = new Elysia({ tags: ["profiles"] }).use(auth).guard(
|
export const historyH = new Elysia({ tags: ["profiles"] })
|
||||||
|
.use(auth)
|
||||||
|
.guard(
|
||||||
{
|
{
|
||||||
query: t.Object({
|
query: t.Object({
|
||||||
sort: {
|
sort: {
|
||||||
@ -136,4 +143,59 @@ export const historyH = new Elysia({ tags: ["profiles"] }).use(auth).guard(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
.post(
|
||||||
|
"/profile/me/history",
|
||||||
|
async ({ body, jwt: { sub } }) => {
|
||||||
|
const profilePk = await getOrCreateProfile(sub);
|
||||||
|
|
||||||
|
const rows = await db
|
||||||
|
.insert(history)
|
||||||
|
.select(
|
||||||
|
db
|
||||||
|
.select({
|
||||||
|
profilePk: sql<number>`${profilePk}`,
|
||||||
|
entryPk: entries.pk,
|
||||||
|
videoPk: videos.pk,
|
||||||
|
percent: sql`hist.percent::integer`,
|
||||||
|
time: sql`hist.time::integer`,
|
||||||
|
playedDate: sql`hist.playedDate::timestamptz`,
|
||||||
|
})
|
||||||
|
.from(
|
||||||
|
values(
|
||||||
|
body.map((x) => ({ ...x, entryUseId: isUuid(x.entry) })),
|
||||||
|
).as("hist"),
|
||||||
|
)
|
||||||
|
.innerJoin(
|
||||||
|
entries,
|
||||||
|
or(
|
||||||
|
and(
|
||||||
|
sql`hist.entryUseId::boolean`,
|
||||||
|
eq(entries.id, sql`hist.entry::uuid`),
|
||||||
|
),
|
||||||
|
and(
|
||||||
|
not(sql`hist.entryUseId::boolean`),
|
||||||
|
eq(entries.id, sql`hist.entry`),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.innerJoin(videos, eq(videos.id, sql`hist.videoId::uuid`)),
|
||||||
|
)
|
||||||
|
.returning({ pk: history.pk });
|
||||||
|
return { status: 201, inserted: rows.length };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
detail: { description: "Bulk add entries/movies to your watch history." },
|
||||||
|
body: t.Array(SeedHistory),
|
||||||
|
permissions: ["core.read"],
|
||||||
|
response: {
|
||||||
|
201: t.Object({
|
||||||
|
status: t.Literal(201),
|
||||||
|
inserted: t.Integer({
|
||||||
|
description: "The number of history entry inserted",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
422: KError,
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
24
api/src/controllers/profiles/profile.ts
Normal file
24
api/src/controllers/profiles/profile.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { eq, sql } from "drizzle-orm";
|
||||||
|
import { db } from "~/db";
|
||||||
|
import { profiles } from "~/db/schema";
|
||||||
|
|
||||||
|
export async function getOrCreateProfile(userId: string) {
|
||||||
|
let [profile] = await db
|
||||||
|
.select({ pk: profiles.pk })
|
||||||
|
.from(profiles)
|
||||||
|
.where(eq(profiles.id, userId))
|
||||||
|
.limit(1);
|
||||||
|
if (profile) return profile.pk;
|
||||||
|
|
||||||
|
[profile] = await db
|
||||||
|
.insert(profiles)
|
||||||
|
.values({ id: userId })
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
// we can't do `onConflictDoNothing` because on race conditions
|
||||||
|
// we still want the profile to be returned.
|
||||||
|
target: [profiles.id],
|
||||||
|
set: { id: sql`excluded.id` },
|
||||||
|
})
|
||||||
|
.returning({ pk: profiles.pk });
|
||||||
|
return profile.pk;
|
||||||
|
}
|
@ -8,7 +8,7 @@ import {
|
|||||||
watchStatusQ,
|
watchStatusQ,
|
||||||
} from "~/controllers/shows/logic";
|
} from "~/controllers/shows/logic";
|
||||||
import { db } from "~/db";
|
import { db } from "~/db";
|
||||||
import { profiles, shows } from "~/db/schema";
|
import { shows } from "~/db/schema";
|
||||||
import { watchlist } from "~/db/schema/watchlist";
|
import { watchlist } from "~/db/schema/watchlist";
|
||||||
import { conflictUpdateAllExcept, getColumns } from "~/db/utils";
|
import { conflictUpdateAllExcept, getColumns } from "~/db/utils";
|
||||||
import { KError } from "~/models/error";
|
import { KError } from "~/models/error";
|
||||||
@ -25,6 +25,7 @@ import {
|
|||||||
} from "~/models/utils";
|
} from "~/models/utils";
|
||||||
import { desc } from "~/models/utils/descriptions";
|
import { desc } from "~/models/utils/descriptions";
|
||||||
import { MovieWatchStatus, SerieWatchStatus } from "~/models/watchlist";
|
import { MovieWatchStatus, SerieWatchStatus } from "~/models/watchlist";
|
||||||
|
import { getOrCreateProfile } from "./profile";
|
||||||
|
|
||||||
async function setWatchStatus({
|
async function setWatchStatus({
|
||||||
show,
|
show,
|
||||||
@ -35,29 +36,13 @@ async function setWatchStatus({
|
|||||||
status: SerieWatchStatus;
|
status: SerieWatchStatus;
|
||||||
userId: string;
|
userId: string;
|
||||||
}) {
|
}) {
|
||||||
let [profile] = await db
|
const profilePk = await getOrCreateProfile(userId);
|
||||||
.select({ pk: profiles.pk })
|
|
||||||
.from(profiles)
|
|
||||||
.where(eq(profiles.id, userId))
|
|
||||||
.limit(1);
|
|
||||||
if (!profile) {
|
|
||||||
[profile] = await db
|
|
||||||
.insert(profiles)
|
|
||||||
.values({ id: userId })
|
|
||||||
.onConflictDoUpdate({
|
|
||||||
// we can't do `onConflictDoNothing` because on race conditions
|
|
||||||
// we still want the profile to be returned.
|
|
||||||
target: [profiles.id],
|
|
||||||
set: { id: sql`excluded.id` },
|
|
||||||
})
|
|
||||||
.returning({ pk: profiles.pk });
|
|
||||||
}
|
|
||||||
|
|
||||||
const [ret] = await db
|
const [ret] = await db
|
||||||
.insert(watchlist)
|
.insert(watchlist)
|
||||||
.values({
|
.values({
|
||||||
...status,
|
...status,
|
||||||
profilePk: profile.pk,
|
profilePk: profilePk,
|
||||||
showPk: show.pk,
|
showPk: show.pk,
|
||||||
})
|
})
|
||||||
.onConflictDoUpdate({
|
.onConflictDoUpdate({
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { t } from "elysia";
|
import { t } from "elysia";
|
||||||
import type { Prettify } from "~/utils";
|
import type { Prettify } from "~/utils";
|
||||||
import { bubbleImages, madeInAbyss, registerExamples } from "../examples";
|
import { bubbleImages, madeInAbyss, registerExamples } from "../examples";
|
||||||
|
import { Progress } from "../history";
|
||||||
import {
|
import {
|
||||||
DbMetadata,
|
DbMetadata,
|
||||||
EpisodeId,
|
EpisodeId,
|
||||||
@ -9,7 +10,6 @@ import {
|
|||||||
TranslationRecord,
|
TranslationRecord,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { EmbeddedVideo } from "../video";
|
import { EmbeddedVideo } from "../video";
|
||||||
import { Progress } from "../watchlist";
|
|
||||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||||
|
|
||||||
export const BaseEpisode = t.Intersect([
|
export const BaseEpisode = t.Intersect([
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { t } from "elysia";
|
import { t } from "elysia";
|
||||||
import { type Prettify, comment } from "~/utils";
|
import { type Prettify, comment } from "~/utils";
|
||||||
import { madeInAbyss, registerExamples } from "../examples";
|
import { madeInAbyss, registerExamples } from "../examples";
|
||||||
|
import { Progress } from "../history";
|
||||||
import { DbMetadata, SeedImage } from "../utils";
|
import { DbMetadata, SeedImage } from "../utils";
|
||||||
import { Resource } from "../utils/resource";
|
import { Resource } from "../utils/resource";
|
||||||
import { Progress } from "../watchlist";
|
|
||||||
import { BaseEntry } from "./base-entry";
|
import { BaseEntry } from "./base-entry";
|
||||||
|
|
||||||
export const ExtraType = t.UnionEnum([
|
export const ExtraType = t.UnionEnum([
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { t } from "elysia";
|
import { t } from "elysia";
|
||||||
import { type Prettify, comment } from "~/utils";
|
import { type Prettify, comment } from "~/utils";
|
||||||
import { bubbleImages, madeInAbyss, registerExamples } from "../examples";
|
import { bubbleImages, madeInAbyss, registerExamples } from "../examples";
|
||||||
|
import { Progress } from "../history";
|
||||||
import {
|
import {
|
||||||
DbMetadata,
|
DbMetadata,
|
||||||
ExternalId,
|
ExternalId,
|
||||||
@ -10,7 +11,6 @@ import {
|
|||||||
TranslationRecord,
|
TranslationRecord,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { EmbeddedVideo } from "../video";
|
import { EmbeddedVideo } from "../video";
|
||||||
import { Progress } from "../watchlist";
|
|
||||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||||
|
|
||||||
export const BaseMovieEntry = t.Intersect(
|
export const BaseMovieEntry = t.Intersect(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { t } from "elysia";
|
import { t } from "elysia";
|
||||||
import { type Prettify, comment } from "~/utils";
|
import { type Prettify, comment } from "~/utils";
|
||||||
import { bubbleImages, madeInAbyss, registerExamples } from "../examples";
|
import { bubbleImages, madeInAbyss, registerExamples } from "../examples";
|
||||||
|
import { Progress } from "../history";
|
||||||
import {
|
import {
|
||||||
DbMetadata,
|
DbMetadata,
|
||||||
EpisodeId,
|
EpisodeId,
|
||||||
@ -9,7 +10,6 @@ import {
|
|||||||
TranslationRecord,
|
TranslationRecord,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { EmbeddedVideo } from "../video";
|
import { EmbeddedVideo } from "../video";
|
||||||
import { Progress } from "../watchlist";
|
|
||||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||||
|
|
||||||
export const BaseSpecial = t.Intersect(
|
export const BaseSpecial = t.Intersect(
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { t } from "elysia";
|
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 { Progress } from "../history";
|
||||||
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(
|
||||||
|
40
api/src/models/history.ts
Normal file
40
api/src/models/history.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { t } from "elysia";
|
||||||
|
import { comment } from "~/utils";
|
||||||
|
|
||||||
|
export const Progress = t.Object({
|
||||||
|
percent: t.Integer({ minimum: 0, maximum: 100 }),
|
||||||
|
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.
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
playedDate: t.Nullable(t.String({ format: "date-time" })),
|
||||||
|
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 const SeedHistory = t.Intersect([
|
||||||
|
t.Object({
|
||||||
|
entry: t.String({
|
||||||
|
description: "Id or slug of the entry/movie you watched",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
Progress,
|
||||||
|
]);
|
||||||
|
export type SeedHistory = typeof SeedHistory.static;
|
@ -1,33 +1,4 @@
|
|||||||
import { t } from "elysia";
|
import { t } from "elysia";
|
||||||
import { comment } from "~/utils";
|
|
||||||
|
|
||||||
export const Progress = t.Object({
|
|
||||||
percent: t.Integer({ minimum: 0, maximum: 100 }),
|
|
||||||
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.
|
|
||||||
`,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
playedDate: t.Nullable(t.String({ format: "date-time" })),
|
|
||||||
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 const WatchlistStatus = t.UnionEnum([
|
export const WatchlistStatus = t.UnionEnum([
|
||||||
"completed",
|
"completed",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user