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 { entriesH } from "./controllers/entries";
|
||||
import { imagesH } from "./controllers/images";
|
||||
import { historyH } from "./controllers/profiles/history";
|
||||
import { watchlistH } from "./controllers/profiles/watchlist";
|
||||
import { seasonsH } from "./controllers/seasons";
|
||||
import { seed } from "./controllers/seed";
|
||||
@ -95,4 +96,5 @@ export const app = new Elysia({ prefix })
|
||||
},
|
||||
(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 { 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 { KError } from "~/models/error";
|
||||
import { SeedHistory } from "~/models/history";
|
||||
import {
|
||||
AcceptLanguage,
|
||||
Filter,
|
||||
Page,
|
||||
createPage,
|
||||
isUuid,
|
||||
processLanguages,
|
||||
} from "~/models/utils";
|
||||
import { desc } from "~/models/utils/descriptions";
|
||||
@ -18,8 +22,11 @@ import {
|
||||
entrySort,
|
||||
getEntries,
|
||||
} 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({
|
||||
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,
|
||||
} from "~/controllers/shows/logic";
|
||||
import { db } from "~/db";
|
||||
import { profiles, shows } from "~/db/schema";
|
||||
import { shows } from "~/db/schema";
|
||||
import { watchlist } from "~/db/schema/watchlist";
|
||||
import { conflictUpdateAllExcept, getColumns } from "~/db/utils";
|
||||
import { KError } from "~/models/error";
|
||||
@ -25,6 +25,7 @@ import {
|
||||
} from "~/models/utils";
|
||||
import { desc } from "~/models/utils/descriptions";
|
||||
import { MovieWatchStatus, SerieWatchStatus } from "~/models/watchlist";
|
||||
import { getOrCreateProfile } from "./profile";
|
||||
|
||||
async function setWatchStatus({
|
||||
show,
|
||||
@ -35,29 +36,13 @@ async function setWatchStatus({
|
||||
status: SerieWatchStatus;
|
||||
userId: string;
|
||||
}) {
|
||||
let [profile] = await db
|
||||
.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 profilePk = await getOrCreateProfile(userId);
|
||||
|
||||
const [ret] = await db
|
||||
.insert(watchlist)
|
||||
.values({
|
||||
...status,
|
||||
profilePk: profile.pk,
|
||||
profilePk: profilePk,
|
||||
showPk: show.pk,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { t } from "elysia";
|
||||
import type { Prettify } from "~/utils";
|
||||
import { bubbleImages, madeInAbyss, registerExamples } from "../examples";
|
||||
import { Progress } from "../history";
|
||||
import {
|
||||
DbMetadata,
|
||||
EpisodeId,
|
||||
@ -9,7 +10,6 @@ import {
|
||||
TranslationRecord,
|
||||
} from "../utils";
|
||||
import { EmbeddedVideo } from "../video";
|
||||
import { Progress } from "../watchlist";
|
||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||
|
||||
export const BaseEpisode = t.Intersect([
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { t } from "elysia";
|
||||
import { type Prettify, comment } from "~/utils";
|
||||
import { madeInAbyss, registerExamples } from "../examples";
|
||||
import { Progress } from "../history";
|
||||
import { DbMetadata, SeedImage } from "../utils";
|
||||
import { Resource } from "../utils/resource";
|
||||
import { Progress } from "../watchlist";
|
||||
import { BaseEntry } from "./base-entry";
|
||||
|
||||
export const ExtraType = t.UnionEnum([
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { t } from "elysia";
|
||||
import { type Prettify, comment } from "~/utils";
|
||||
import { bubbleImages, madeInAbyss, registerExamples } from "../examples";
|
||||
import { Progress } from "../history";
|
||||
import {
|
||||
DbMetadata,
|
||||
ExternalId,
|
||||
@ -10,7 +11,6 @@ import {
|
||||
TranslationRecord,
|
||||
} from "../utils";
|
||||
import { EmbeddedVideo } from "../video";
|
||||
import { Progress } from "../watchlist";
|
||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||
|
||||
export const BaseMovieEntry = t.Intersect(
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { t } from "elysia";
|
||||
import { type Prettify, comment } from "~/utils";
|
||||
import { bubbleImages, madeInAbyss, registerExamples } from "../examples";
|
||||
import { Progress } from "../history";
|
||||
import {
|
||||
DbMetadata,
|
||||
EpisodeId,
|
||||
@ -9,7 +10,6 @@ import {
|
||||
TranslationRecord,
|
||||
} from "../utils";
|
||||
import { EmbeddedVideo } from "../video";
|
||||
import { Progress } from "../watchlist";
|
||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||
|
||||
export const BaseSpecial = t.Intersect(
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { t } from "elysia";
|
||||
import { type Prettify, comment } from "~/utils";
|
||||
import { bubbleImages, registerExamples, youtubeExample } from "../examples";
|
||||
import { Progress } from "../history";
|
||||
import { DbMetadata, Resource } from "../utils";
|
||||
import { Progress } from "../watchlist";
|
||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||
|
||||
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 { 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([
|
||||
"completed",
|
||||
|
Loading…
x
Reference in New Issue
Block a user