mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add unique constraint on [rendering, version, part]
This commit is contained in:
parent
466b67afe5
commit
45e769828b
5
api/drizzle/0020_video_unique.sql
Normal file
5
api/drizzle/0020_video_unique.sql
Normal file
@ -0,0 +1,5 @@
|
||||
ALTER TABLE "kyoo"."entries" ALTER COLUMN "kind" SET DATA TYPE text;--> statement-breakpoint
|
||||
DROP TYPE "kyoo"."entry_type";--> statement-breakpoint
|
||||
CREATE TYPE "kyoo"."entry_type" AS ENUM('episode', 'movie', 'special', 'extra');--> statement-breakpoint
|
||||
ALTER TABLE "kyoo"."entries" ALTER COLUMN "kind" SET DATA TYPE "kyoo"."entry_type" USING "kind"::"kyoo"."entry_type";--> statement-breakpoint
|
||||
ALTER TABLE "kyoo"."videos" ADD CONSTRAINT "rendering_unique" UNIQUE NULLS NOT DISTINCT("rendering","part","version");
|
1851
api/drizzle/meta/0020_snapshot.json
Normal file
1851
api/drizzle/meta/0020_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -141,6 +141,13 @@
|
||||
"when": 1744120518941,
|
||||
"tag": "0019_nextup",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 20,
|
||||
"version": "7",
|
||||
"when": 1746198322219,
|
||||
"tag": "0020_video_unique",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { db } from "~/db";
|
||||
import { entries, entryVideoJoin, shows, videos } from "~/db/schema";
|
||||
import {
|
||||
conflictUpdateAllExcept,
|
||||
isUniqueConstraint,
|
||||
jsonbBuildObject,
|
||||
jsonbObjectAgg,
|
||||
values,
|
||||
@ -161,7 +162,9 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
"",
|
||||
async ({ body, error }) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const vids = await tx
|
||||
let vids: { pk: number; id: string; path: string }[] = [];
|
||||
try {
|
||||
vids = await tx
|
||||
.insert(videos)
|
||||
.values(body)
|
||||
.onConflictDoUpdate({
|
||||
@ -173,6 +176,19 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
id: videos.id,
|
||||
path: videos.path,
|
||||
});
|
||||
} catch (e) {
|
||||
if (!isUniqueConstraint(e))
|
||||
throw e;
|
||||
return error(409, {
|
||||
status: 409,
|
||||
message: comment`
|
||||
Invalid rendering. A video with the same (rendering, part, version) combo
|
||||
(but with a different path) already exists in db.
|
||||
|
||||
rendering should be computed by the sha of your path (excluding only the version & part numbers)
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
const vidEntries = body.flatMap((x) => {
|
||||
if (!x.for) return [];
|
||||
@ -305,7 +321,11 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
),
|
||||
),
|
||||
)
|
||||
.onConflictDoNothing()
|
||||
.onConflictDoUpdate({
|
||||
target: [entryVideoJoin.entryPk, entryVideoJoin.videoPk],
|
||||
// this is basically a `.onConflictDoNothing()` but we want `returning` to give us the existing data
|
||||
set: { entryPk: sql`excluded.entry_pk` },
|
||||
})
|
||||
.returning({
|
||||
slug: entryVideoJoin.slug,
|
||||
entryPk: entryVideoJoin.entryPk,
|
||||
@ -340,7 +360,14 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
`,
|
||||
},
|
||||
body: t.Array(SeedVideo),
|
||||
response: { 201: t.Array(CreatedVideo) },
|
||||
response: {
|
||||
201: t.Array(CreatedVideo),
|
||||
409: {
|
||||
...KError,
|
||||
description:
|
||||
"Invalid rendering specified. (conflicts with an existing video)",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
.delete(
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
primaryKey,
|
||||
text,
|
||||
timestamp,
|
||||
unique,
|
||||
uuid,
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
@ -34,6 +35,9 @@ export const videos = schema.table(
|
||||
(t) => [
|
||||
check("part_pos", sql`${t.part} >= 0`),
|
||||
check("version_pos", sql`${t.version} >= 0`),
|
||||
unique("rendering_unique")
|
||||
.on(t.rendering, t.part, t.version)
|
||||
.nullsNotDistinct(),
|
||||
],
|
||||
);
|
||||
|
||||
|
@ -142,3 +142,7 @@ export const jsonbBuildObject = <T>(select: JsonFields) => {
|
||||
);
|
||||
return sql<T>`jsonb_build_object(${query})`;
|
||||
};
|
||||
|
||||
export const isUniqueConstraint = (e: unknown): boolean => {
|
||||
return typeof e === "object" && e != null && "code" in e && e.code === "23505";
|
||||
};
|
||||
|
@ -384,7 +384,7 @@ describe("Movie seeding", () => {
|
||||
path: "/video/bubble3.mkv",
|
||||
part: null,
|
||||
version: 1,
|
||||
rendering: "oeunhtoeuth",
|
||||
rendering: "oeunhtoeuthoeu",
|
||||
guess: { title: "bubble", from: "test" },
|
||||
},
|
||||
{
|
||||
|
@ -50,7 +50,7 @@ describe("Video seeding", () => {
|
||||
guess: { title: "mia", season: [1], episode: [13], from: "test" },
|
||||
part: null,
|
||||
path: "/video/mia s1e13.mkv",
|
||||
rendering: "sha",
|
||||
rendering: "sha2",
|
||||
version: 1,
|
||||
for: [{ slug: `${madeInAbyss.slug}-s1e13` }],
|
||||
});
|
||||
@ -82,7 +82,7 @@ describe("Video seeding", () => {
|
||||
guess: { title: "bubble", from: "test" },
|
||||
part: null,
|
||||
path: "/video/bubble.mkv",
|
||||
rendering: "sha",
|
||||
rendering: "sha3",
|
||||
version: 1,
|
||||
for: [{ movie: bubble.slug }],
|
||||
});
|
||||
@ -114,7 +114,7 @@ describe("Video seeding", () => {
|
||||
guess: { title: "bubble", from: "test" },
|
||||
part: null,
|
||||
path: "/video/bubble.mkv",
|
||||
rendering: "sha",
|
||||
rendering: "sha4",
|
||||
version: 1,
|
||||
for: [{ movie: bubble.slug }],
|
||||
});
|
||||
@ -221,7 +221,7 @@ describe("Video seeding", () => {
|
||||
guess: { title: "mia", season: [0], episode: [3], from: "test" },
|
||||
part: null,
|
||||
path: "/video/mia 13.5.mkv",
|
||||
rendering: "notehu",
|
||||
rendering: "notehu2",
|
||||
version: 1,
|
||||
for: [
|
||||
{
|
||||
@ -266,7 +266,7 @@ describe("Video seeding", () => {
|
||||
},
|
||||
part: null,
|
||||
path: "/video/mia s1e13 [tmdb=72636].mkv",
|
||||
rendering: "notehu",
|
||||
rendering: "notehu3",
|
||||
version: 1,
|
||||
for: [
|
||||
{
|
||||
@ -295,7 +295,7 @@ describe("Video seeding", () => {
|
||||
expect(body[0].entries).toBeArrayOfSize(1);
|
||||
expect(vid!.evj).toBeArrayOfSize(1);
|
||||
|
||||
expect(vid!.evj[0].slug).toBe("made-in-abyss-s1e13-notehu");
|
||||
expect(vid!.evj[0].slug).toBe("made-in-abyss-s1e13-notehu3");
|
||||
expect(vid!.evj[0].entry.slug).toBe("made-in-abyss-s1e13");
|
||||
});
|
||||
|
||||
@ -343,6 +343,22 @@ describe("Video seeding", () => {
|
||||
expect(vid!.evj[0].entry.slug).toBe("bubble");
|
||||
});
|
||||
|
||||
it("Different path, same sha", async () => {
|
||||
const [resp, body] = await createVideo({
|
||||
guess: { title: "bubble", from: "test" },
|
||||
part: null,
|
||||
path: "/video/bubble invalid-sha.mkv",
|
||||
rendering: "sha",
|
||||
version: 1,
|
||||
for: [{ movie: bubble.slug }],
|
||||
});
|
||||
|
||||
// conflict with existing video, message will contain an explanation on how to fix this
|
||||
expectStatus(resp, body).toBe(409);
|
||||
expect(body.message).toBeString();
|
||||
});
|
||||
|
||||
|
||||
it("Two for the same entry", async () => {
|
||||
const [resp, body] = await createVideo({
|
||||
guess: {
|
||||
@ -513,7 +529,7 @@ describe("Video seeding", () => {
|
||||
},
|
||||
part: null,
|
||||
path: "/video/mia s1e13 & s2e1 [tmdb=72636].mkv",
|
||||
rendering: "notehu",
|
||||
rendering: "notehu5",
|
||||
version: 1,
|
||||
for: [
|
||||
{ serie: madeInAbyss.slug, season: 1, episode: 13 },
|
||||
|
Loading…
x
Reference in New Issue
Block a user