mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-12-24 13:57:27 -05:00
Add back entry_pk in history
This commit is contained in:
parent
333dc46ebf
commit
ab5da0d5c6
@ -1,3 +0,0 @@
|
||||
ALTER TABLE "kyoo"."history" DROP CONSTRAINT "history_entry_pk_entries_pk_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "kyoo"."history" DROP COLUMN "entry_pk";
|
||||
File diff suppressed because it is too large
Load Diff
@ -176,13 +176,6 @@
|
||||
"when": 1763932730557,
|
||||
"tag": "0024_fix-season-count",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 25,
|
||||
"version": "7",
|
||||
"when": 1765791459003,
|
||||
"tag": "0025_remove-history-entry",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -45,19 +45,18 @@ import { desc as description } from "~/models/utils/descriptions";
|
||||
import type { EmbeddedVideo } from "~/models/video";
|
||||
|
||||
export const entryProgressQ = db
|
||||
.selectDistinctOn([entryVideoJoin.entryPk], {
|
||||
.selectDistinctOn([history.entryPk], {
|
||||
percent: history.percent,
|
||||
time: history.time,
|
||||
entryPk: entryVideoJoin.entryPk,
|
||||
entryPk: history.entryPk,
|
||||
playedDate: history.playedDate,
|
||||
videoId: videos.id,
|
||||
})
|
||||
.from(history)
|
||||
.innerJoin(videos, eq(history.videoPk, videos.pk))
|
||||
.innerJoin(entryVideoJoin, eq(videos.pk, entryVideoJoin.videoPk))
|
||||
.leftJoin(videos, eq(history.videoPk, videos.pk))
|
||||
.innerJoin(profiles, eq(history.profilePk, profiles.pk))
|
||||
.where(eq(profiles.id, sql.placeholder("userId")))
|
||||
.orderBy(entryVideoJoin.entryPk, desc(history.playedDate))
|
||||
.orderBy(history.entryPk, desc(history.playedDate))
|
||||
.as("progress");
|
||||
|
||||
export const entryFilters: FilterDef = {
|
||||
|
||||
@ -8,19 +8,13 @@ import {
|
||||
lte,
|
||||
ne,
|
||||
sql,
|
||||
TransactionRollbackError,
|
||||
} from "drizzle-orm";
|
||||
import { alias } from "drizzle-orm/pg-core";
|
||||
import Elysia, { t } from "elysia";
|
||||
import { auth, getUserInfo } from "~/auth";
|
||||
import { db, type Transaction } from "~/db";
|
||||
import {
|
||||
entries,
|
||||
entryVideoJoin,
|
||||
history,
|
||||
profiles,
|
||||
shows,
|
||||
videos,
|
||||
} from "~/db/schema";
|
||||
import { entries, history, profiles, shows, videos } from "~/db/schema";
|
||||
import { watchlist } from "~/db/schema/watchlist";
|
||||
import { coalesce, sqlarr } from "~/db/utils";
|
||||
import { Entry } from "~/models/entry";
|
||||
@ -30,6 +24,7 @@ import {
|
||||
AcceptLanguage,
|
||||
createPage,
|
||||
Filter,
|
||||
isUuid,
|
||||
Page,
|
||||
processLanguages,
|
||||
} from "~/models/utils";
|
||||
@ -45,19 +40,27 @@ import {
|
||||
import { getOrCreateProfile } from "./profile";
|
||||
|
||||
export async function updateProgress(userPk: number, progress: SeedHistory[]) {
|
||||
return db.transaction(async (tx) => {
|
||||
const hist = await updateHistory(tx, userPk, progress);
|
||||
if (hist.created.length + hist.updated.length !== progress.length) {
|
||||
tx.rollback();
|
||||
}
|
||||
// only return new and entries whose status has changed.
|
||||
// we don't need to update the watchlist every 10s when watching a video.
|
||||
await updateWatchlist(tx, userPk, [
|
||||
...hist.created,
|
||||
...hist.updated.filter((x) => x.percent >= 95),
|
||||
]);
|
||||
return { status: 201, inserted: hist.created.length };
|
||||
});
|
||||
try {
|
||||
return await db.transaction(async (tx) => {
|
||||
const hist = await updateHistory(tx, userPk, progress);
|
||||
if (hist.created.length + hist.updated.length !== progress.length) {
|
||||
tx.rollback();
|
||||
}
|
||||
// only return new and entries whose status has changed.
|
||||
// we don't need to update the watchlist every 10s when watching a video.
|
||||
await updateWatchlist(tx, userPk, [
|
||||
...hist.created,
|
||||
...hist.updated.filter((x) => x.percent >= 95),
|
||||
]);
|
||||
return { status: 201, inserted: hist.created.length } as const;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!(e instanceof TransactionRollbackError)) throw e;
|
||||
return {
|
||||
status: 404,
|
||||
message: "Invalid entry id/slug in progress array",
|
||||
} as const;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateHistory(
|
||||
@ -86,7 +89,9 @@ async function updateHistory(
|
||||
progress.filter((x) => existing.includes(x.videoId)),
|
||||
);
|
||||
const newEntries = traverse(
|
||||
progress.filter((x) => !existing.includes(x.videoId)),
|
||||
progress
|
||||
.filter((x) => !existing.includes(x.videoId))
|
||||
.map((x) => ({ ...x, entryUseid: isUuid(x.entry) })),
|
||||
);
|
||||
|
||||
const updated =
|
||||
@ -113,6 +118,7 @@ async function updateHistory(
|
||||
),
|
||||
)
|
||||
.returning({
|
||||
entryPk: history.entryPk,
|
||||
videoPk: history.videoPk,
|
||||
percent: history.percent,
|
||||
playedDate: history.playedDate,
|
||||
@ -128,6 +134,7 @@ async function updateHistory(
|
||||
.select({
|
||||
profilePk: sql`${userPk}`.as("profilePk"),
|
||||
videoPk: videos.pk,
|
||||
entryPk: entries.pk,
|
||||
percent: sql`hist.percent`.as("percent"),
|
||||
time: sql`hist.ts`.as("time"),
|
||||
playedDate: coalesce(sql`hist.played_date`, sql`now()`).as(
|
||||
@ -135,14 +142,26 @@ async function updateHistory(
|
||||
),
|
||||
})
|
||||
.from(sql`unnest(
|
||||
${sqlarr(newEntries.entry)}::text[],
|
||||
${sqlarr(newEntries.entryUseid)}::boolean[],
|
||||
${sqlarr(newEntries.videoId)}::uuid[],
|
||||
${sqlarr(newEntries.time)}::integer[],
|
||||
${sqlarr(newEntries.percent)}::integer[],
|
||||
${sqlarr(newEntries.playedDate)}::timestamptz[]
|
||||
) as hist(video_id, ts, percent, played_date)`)
|
||||
.innerJoin(videos, eq(videos.id, sql`hist.video_id`)),
|
||||
) as hist(entry, entry_use_id, video_id, ts, percent, played_date)`)
|
||||
.innerJoin(
|
||||
entries,
|
||||
sql`
|
||||
case
|
||||
when hist.entry_use_id then ${entries.id} = hist.entry::uuid
|
||||
else ${entries.slug} = hist.entry
|
||||
end
|
||||
`,
|
||||
)
|
||||
.leftJoin(videos, eq(videos.id, sql`hist.video_id`)),
|
||||
)
|
||||
.returning({
|
||||
entryPk: history.entryPk,
|
||||
videoPk: history.videoPk,
|
||||
percent: history.percent,
|
||||
playedDate: history.playedDate,
|
||||
@ -155,7 +174,11 @@ async function updateHistory(
|
||||
async function updateWatchlist(
|
||||
tx: Transaction,
|
||||
userPk: number,
|
||||
histArr: { videoPk: number; percent: number; playedDate: string }[],
|
||||
histArr: {
|
||||
entryPk: number;
|
||||
percent: number;
|
||||
playedDate: string;
|
||||
}[],
|
||||
) {
|
||||
if (histArr.length === 0) return;
|
||||
|
||||
@ -186,14 +209,10 @@ async function updateWatchlist(
|
||||
db
|
||||
.select()
|
||||
.from(history)
|
||||
.leftJoin(
|
||||
entryVideoJoin,
|
||||
eq(history.videoPk, entryVideoJoin.videoPk),
|
||||
)
|
||||
.where(
|
||||
and(
|
||||
eq(history.profilePk, userPk),
|
||||
eq(entryVideoJoin.entryPk, entries.pk),
|
||||
eq(history.entryPk, entries.pk),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -248,15 +267,11 @@ async function updateWatchlist(
|
||||
updatedAt: sql`now()`.as("updatedAt"),
|
||||
})
|
||||
.from(sql`unnest(
|
||||
${sqlarr(hist.videoPk)}::integer[],
|
||||
${sqlarr(hist.entryPk)}::integer[],
|
||||
${sqlarr(hist.percent)}::integer[],
|
||||
${sqlarr(hist.playedDate)}::timestamptz[]
|
||||
) as hist(video_pk, percent, played_date)`)
|
||||
.innerJoin(
|
||||
entryVideoJoin,
|
||||
eq(sql`hist.video_pk`, entryVideoJoin.videoPk),
|
||||
)
|
||||
.leftJoin(entries, eq(entries.pk, entryVideoJoin.entryPk))
|
||||
) as hist(entry_pk, percent, played_date)`)
|
||||
.innerJoin(entries, eq(entries.pk, sql`hist.entry_pk`))
|
||||
.leftJoinLateral(nextEntryQ, sql`true`),
|
||||
)
|
||||
.onConflictDoUpdate({
|
||||
@ -297,13 +312,12 @@ const historyProgressQ: typeof entryProgressQ = db
|
||||
.select({
|
||||
percent: history.percent,
|
||||
time: history.time,
|
||||
entryPk: entryVideoJoin.entryPk,
|
||||
entryPk: history.entryPk,
|
||||
playedDate: history.playedDate,
|
||||
videoId: videos.id,
|
||||
})
|
||||
.from(history)
|
||||
.innerJoin(videos, eq(history.videoPk, videos.pk))
|
||||
.innerJoin(entryVideoJoin, eq(videos.pk, entryVideoJoin.videoPk))
|
||||
.leftJoin(videos, eq(history.videoPk, videos.pk))
|
||||
.innerJoin(profiles, eq(history.profilePk, profiles.pk))
|
||||
.where(eq(profiles.id, sql.placeholder("userId")))
|
||||
.as("progress");
|
||||
@ -437,11 +451,8 @@ export const historyH = new Elysia({ tags: ["profiles"] })
|
||||
async ({ body, jwt: { sub }, status }) => {
|
||||
const profilePk = await getOrCreateProfile(sub);
|
||||
|
||||
return db.transaction(async (tx) => {
|
||||
const hist = await updateHistory(tx, profilePk, body);
|
||||
await updateWatchlist(tx, profilePk, hist);
|
||||
return status(201, { status: 201, inserted: hist.length });
|
||||
});
|
||||
const ret = await updateProgress(profilePk, body);
|
||||
return status(ret.status, ret);
|
||||
},
|
||||
{
|
||||
detail: { description: "Bulk add entries/movies to your watch history." },
|
||||
@ -454,6 +465,10 @@ export const historyH = new Elysia({ tags: ["profiles"] })
|
||||
description: "The number of history entry inserted",
|
||||
}),
|
||||
}),
|
||||
404: {
|
||||
...KError,
|
||||
description: "No entry found with the given id or slug.",
|
||||
},
|
||||
422: KError,
|
||||
},
|
||||
},
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { check, index, integer } from "drizzle-orm/pg-core";
|
||||
import { entries } from "./entries";
|
||||
import { profiles } from "./profiles";
|
||||
import { schema, timestamp } from "./utils";
|
||||
import { videos } from "./videos";
|
||||
@ -11,7 +12,12 @@ export const history = schema.table(
|
||||
profilePk: integer()
|
||||
.notNull()
|
||||
.references(() => profiles.pk, { onDelete: "cascade" }),
|
||||
videoPk: integer().notNull().references(() => videos.pk, { onDelete: "cascade" }),
|
||||
// we need to attach an history to an entry because we want to keep history
|
||||
// when we delete a video file
|
||||
entryPk: integer()
|
||||
.notNull()
|
||||
.references(() => entries.pk, { onDelete: "cascade" }),
|
||||
videoPk: integer().references(() => videos.pk, { onDelete: "set null" }),
|
||||
percent: integer().notNull().default(0),
|
||||
time: integer().notNull().default(0),
|
||||
playedDate: timestamp({ withTimezone: true, mode: "iso" })
|
||||
|
||||
@ -27,10 +27,12 @@ export const Progress = t.Object({
|
||||
});
|
||||
export type Progress = typeof Progress.static;
|
||||
|
||||
export const SeedHistory = t.Object({
|
||||
percent: Progress.properties.percent,
|
||||
time: Progress.properties.time,
|
||||
playedDate: Progress.properties.playedDate,
|
||||
videoId: Progress.properties.videoId.anyOf[0],
|
||||
});
|
||||
export const SeedHistory = t.Intersect([
|
||||
Progress,
|
||||
t.Object({
|
||||
entry: t.String({
|
||||
description: "Id or slug of the entry/movie you watched",
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
export type SeedHistory = typeof SeedHistory.static;
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import type { TObject, TString } from "@sinclair/typebox";
|
||||
import Elysia, { type TSchema, t } from "elysia";
|
||||
import { verifyJwt } from "./auth";
|
||||
import { updateHistory, updateWatchlist } from "./controllers/profiles/history";
|
||||
import { updateProgress } from "./controllers/profiles/history";
|
||||
import { getOrCreateProfile } from "./controllers/profiles/profile";
|
||||
import { db } from "./db";
|
||||
import { SeedHistory } from "./models/history";
|
||||
|
||||
const actionMap = {
|
||||
@ -18,13 +17,10 @@ const actionMap = {
|
||||
async message(ws, body) {
|
||||
const profilePk = await getOrCreateProfile(ws.data.jwt.sub);
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
const hist = await updateHistory(tx, profilePk, [
|
||||
{ ...body, playedDate: null },
|
||||
]);
|
||||
await updateWatchlist(tx, profilePk, hist);
|
||||
});
|
||||
ws.send({ response: "ok" });
|
||||
const ret = await updateProgress(profilePk, [
|
||||
{ ...body, playedDate: null },
|
||||
]);
|
||||
ws.send(ret);
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
@ -11,12 +11,7 @@ import {
|
||||
import { expectStatus } from "tests/utils";
|
||||
import { db } from "~/db";
|
||||
import { entries, shows, videos } from "~/db/schema";
|
||||
import {
|
||||
bubble,
|
||||
bubbleVideo,
|
||||
madeInAbyss,
|
||||
madeInAbyssVideo,
|
||||
} from "~/models/examples";
|
||||
import { bubble, madeInAbyss, madeInAbyssVideo } from "~/models/examples";
|
||||
|
||||
beforeAll(async () => {
|
||||
await db.delete(shows);
|
||||
@ -40,13 +35,15 @@ describe("Set & get history", () => {
|
||||
|
||||
const [r, b] = await addToHistory("me", [
|
||||
{
|
||||
entry: miaEntrySlug,
|
||||
videoId: madeInAbyssVideo.id,
|
||||
percent: 58,
|
||||
time: 28 * 60 + 12,
|
||||
playedDate: "2025-02-01",
|
||||
},
|
||||
{
|
||||
videoId: bubbleVideo.id,
|
||||
entry: bubble.slug,
|
||||
videoId: null,
|
||||
percent: 100,
|
||||
time: 2 * 60,
|
||||
playedDate: "2025-02-02",
|
||||
@ -73,6 +70,7 @@ describe("Set & get history", () => {
|
||||
it("Create duplicated history entry", async () => {
|
||||
const [r, b] = await addToHistory("me", [
|
||||
{
|
||||
entry: miaEntrySlug!,
|
||||
videoId: madeInAbyssVideo.id,
|
||||
percent: 100,
|
||||
time: 38 * 60,
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
import { expectStatus } from "tests/utils";
|
||||
import { db } from "~/db";
|
||||
import { entries, shows, videos } from "~/db/schema";
|
||||
import { bubble, bubbleVideo, madeInAbyss, madeInAbyssVideo } from "~/models/examples";
|
||||
import { bubble, madeInAbyss, madeInAbyssVideo } from "~/models/examples";
|
||||
|
||||
beforeAll(async () => {
|
||||
await db.delete(shows);
|
||||
@ -86,13 +86,15 @@ describe("nextup", () => {
|
||||
it("history watching doesn't update", async () => {
|
||||
let [resp, body] = await addToHistory("me", [
|
||||
{
|
||||
entry: miaEntrySlug,
|
||||
videoId: madeInAbyssVideo.id,
|
||||
percent: 58,
|
||||
time: 28 * 60 + 12,
|
||||
playedDate: "2025-02-01",
|
||||
},
|
||||
{
|
||||
videoId: bubbleVideo.id,
|
||||
entry: bubble.slug,
|
||||
videoId: null,
|
||||
percent: 100,
|
||||
time: 2 * 60,
|
||||
playedDate: "2025-02-02",
|
||||
@ -137,6 +139,7 @@ describe("nextup", () => {
|
||||
it("history completed picks next", async () => {
|
||||
let [resp, body] = await addToHistory("me", [
|
||||
{
|
||||
entry: miaEntrySlug,
|
||||
videoId: madeInAbyssVideo.id,
|
||||
percent: 98,
|
||||
time: 28 * 60 + 12,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user