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,
|
"when": 1744120518941,
|
||||||
"tag": "0019_nextup",
|
"tag": "0019_nextup",
|
||||||
"breakpoints": true
|
"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 { entries, entryVideoJoin, shows, videos } from "~/db/schema";
|
||||||
import {
|
import {
|
||||||
conflictUpdateAllExcept,
|
conflictUpdateAllExcept,
|
||||||
|
isUniqueConstraint,
|
||||||
jsonbBuildObject,
|
jsonbBuildObject,
|
||||||
jsonbObjectAgg,
|
jsonbObjectAgg,
|
||||||
values,
|
values,
|
||||||
@ -161,18 +162,33 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
|||||||
"",
|
"",
|
||||||
async ({ body, error }) => {
|
async ({ body, error }) => {
|
||||||
return await db.transaction(async (tx) => {
|
return await db.transaction(async (tx) => {
|
||||||
const vids = await tx
|
let vids: { pk: number; id: string; path: string }[] = [];
|
||||||
.insert(videos)
|
try {
|
||||||
.values(body)
|
vids = await tx
|
||||||
.onConflictDoUpdate({
|
.insert(videos)
|
||||||
target: [videos.path],
|
.values(body)
|
||||||
set: conflictUpdateAllExcept(videos, ["pk", "id", "createdAt"]),
|
.onConflictDoUpdate({
|
||||||
})
|
target: [videos.path],
|
||||||
.returning({
|
set: conflictUpdateAllExcept(videos, ["pk", "id", "createdAt"]),
|
||||||
pk: videos.pk,
|
})
|
||||||
id: videos.id,
|
.returning({
|
||||||
path: videos.path,
|
pk: videos.pk,
|
||||||
|
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) => {
|
const vidEntries = body.flatMap((x) => {
|
||||||
if (!x.for) return [];
|
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({
|
.returning({
|
||||||
slug: entryVideoJoin.slug,
|
slug: entryVideoJoin.slug,
|
||||||
entryPk: entryVideoJoin.entryPk,
|
entryPk: entryVideoJoin.entryPk,
|
||||||
@ -340,7 +360,14 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
|||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
body: t.Array(SeedVideo),
|
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(
|
.delete(
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
primaryKey,
|
primaryKey,
|
||||||
text,
|
text,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
unique,
|
||||||
uuid,
|
uuid,
|
||||||
varchar,
|
varchar,
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
@ -34,6 +35,9 @@ export const videos = schema.table(
|
|||||||
(t) => [
|
(t) => [
|
||||||
check("part_pos", sql`${t.part} >= 0`),
|
check("part_pos", sql`${t.part} >= 0`),
|
||||||
check("version_pos", sql`${t.version} >= 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})`;
|
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",
|
path: "/video/bubble3.mkv",
|
||||||
part: null,
|
part: null,
|
||||||
version: 1,
|
version: 1,
|
||||||
rendering: "oeunhtoeuth",
|
rendering: "oeunhtoeuthoeu",
|
||||||
guess: { title: "bubble", from: "test" },
|
guess: { title: "bubble", from: "test" },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -50,7 +50,7 @@ describe("Video seeding", () => {
|
|||||||
guess: { title: "mia", season: [1], episode: [13], from: "test" },
|
guess: { title: "mia", season: [1], episode: [13], from: "test" },
|
||||||
part: null,
|
part: null,
|
||||||
path: "/video/mia s1e13.mkv",
|
path: "/video/mia s1e13.mkv",
|
||||||
rendering: "sha",
|
rendering: "sha2",
|
||||||
version: 1,
|
version: 1,
|
||||||
for: [{ slug: `${madeInAbyss.slug}-s1e13` }],
|
for: [{ slug: `${madeInAbyss.slug}-s1e13` }],
|
||||||
});
|
});
|
||||||
@ -82,7 +82,7 @@ describe("Video seeding", () => {
|
|||||||
guess: { title: "bubble", from: "test" },
|
guess: { title: "bubble", from: "test" },
|
||||||
part: null,
|
part: null,
|
||||||
path: "/video/bubble.mkv",
|
path: "/video/bubble.mkv",
|
||||||
rendering: "sha",
|
rendering: "sha3",
|
||||||
version: 1,
|
version: 1,
|
||||||
for: [{ movie: bubble.slug }],
|
for: [{ movie: bubble.slug }],
|
||||||
});
|
});
|
||||||
@ -114,7 +114,7 @@ describe("Video seeding", () => {
|
|||||||
guess: { title: "bubble", from: "test" },
|
guess: { title: "bubble", from: "test" },
|
||||||
part: null,
|
part: null,
|
||||||
path: "/video/bubble.mkv",
|
path: "/video/bubble.mkv",
|
||||||
rendering: "sha",
|
rendering: "sha4",
|
||||||
version: 1,
|
version: 1,
|
||||||
for: [{ movie: bubble.slug }],
|
for: [{ movie: bubble.slug }],
|
||||||
});
|
});
|
||||||
@ -221,7 +221,7 @@ describe("Video seeding", () => {
|
|||||||
guess: { title: "mia", season: [0], episode: [3], from: "test" },
|
guess: { title: "mia", season: [0], episode: [3], from: "test" },
|
||||||
part: null,
|
part: null,
|
||||||
path: "/video/mia 13.5.mkv",
|
path: "/video/mia 13.5.mkv",
|
||||||
rendering: "notehu",
|
rendering: "notehu2",
|
||||||
version: 1,
|
version: 1,
|
||||||
for: [
|
for: [
|
||||||
{
|
{
|
||||||
@ -266,7 +266,7 @@ describe("Video seeding", () => {
|
|||||||
},
|
},
|
||||||
part: null,
|
part: null,
|
||||||
path: "/video/mia s1e13 [tmdb=72636].mkv",
|
path: "/video/mia s1e13 [tmdb=72636].mkv",
|
||||||
rendering: "notehu",
|
rendering: "notehu3",
|
||||||
version: 1,
|
version: 1,
|
||||||
for: [
|
for: [
|
||||||
{
|
{
|
||||||
@ -295,7 +295,7 @@ describe("Video seeding", () => {
|
|||||||
expect(body[0].entries).toBeArrayOfSize(1);
|
expect(body[0].entries).toBeArrayOfSize(1);
|
||||||
expect(vid!.evj).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");
|
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");
|
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 () => {
|
it("Two for the same entry", async () => {
|
||||||
const [resp, body] = await createVideo({
|
const [resp, body] = await createVideo({
|
||||||
guess: {
|
guess: {
|
||||||
@ -513,7 +529,7 @@ describe("Video seeding", () => {
|
|||||||
},
|
},
|
||||||
part: null,
|
part: null,
|
||||||
path: "/video/mia s1e13 & s2e1 [tmdb=72636].mkv",
|
path: "/video/mia s1e13 & s2e1 [tmdb=72636].mkv",
|
||||||
rendering: "notehu",
|
rendering: "notehu5",
|
||||||
version: 1,
|
version: 1,
|
||||||
for: [
|
for: [
|
||||||
{ serie: madeInAbyss.slug, season: 1, episode: 13 },
|
{ serie: madeInAbyss.slug, season: 1, episode: 13 },
|
||||||
|
Loading…
x
Reference in New Issue
Block a user