Add put routes for videos

This commit is contained in:
Zoe Roux
2026-03-10 13:53:55 +01:00
parent ef1486deaf
commit 32bfdb0ce0
10 changed files with 353 additions and 190 deletions
+121 -80
View File
@@ -28,6 +28,91 @@ const CreatedVideo = t.Object({
),
});
async function createVideos(body: SeedVideo[], clearLinks: boolean) {
if (body.length === 0) {
return { status: 422, message: "No videos" } as const;
}
return await db.transaction(async (tx) => {
let vids: { pk: number; id: string; path: string; guess: Guess }[] = [];
try {
vids = await tx
.insert(videos)
.select(unnestValues(body, videos))
.onConflictDoUpdate({
target: [videos.path],
set: conflictUpdateAllExcept(videos, ["pk", "id", "createdAt"]),
})
.returning({
pk: videos.pk,
id: videos.id,
path: videos.path,
guess: videos.guess,
});
} catch (e) {
if (!isUniqueConstraint(e)) throw e;
return {
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)
`,
} as const;
}
const vidEntries = body.flatMap((x) => {
if (!x.for) return [];
return x.for.map((e) => ({
video: vids.find((v) => v.path === x.path)!.pk,
entry: {
...e,
movie:
"movie" in e
? isUuid(e.movie)
? { id: e.movie }
: { slug: e.movie }
: undefined,
serie:
"serie" in e
? isUuid(e.serie)
? { id: e.serie }
: { slug: e.serie }
: undefined,
},
}));
});
if (!vidEntries.length) {
return vids.map((x) => ({
id: x.id,
path: x.path,
guess: x.guess,
entries: [],
}));
}
if (clearLinks) {
await tx
.delete(entryVideoJoin)
.where(
eq(
entryVideoJoin.videoPk,
sql`any(${sqlarr(vids.map((x) => x.pk))})`,
),
);
}
const links = await linkVideos(tx, vidEntries);
return vids.map((x) => ({
id: x.id,
path: x.path,
guess: x.guess,
entries: links[x.pk] ?? [],
}));
});
}
export const videosWriteH = new Elysia({ prefix: "/videos", tags: ["videos"] })
.model({
video: Video,
@@ -38,84 +123,9 @@ export const videosWriteH = new Elysia({ prefix: "/videos", tags: ["videos"] })
.post(
"",
async ({ body, status }) => {
if (body.length === 0) {
return status(422, { status: 422, message: "No videos" });
}
return await db.transaction(async (tx) => {
let vids: { pk: number; id: string; path: string; guess: Guess }[] = [];
try {
vids = await tx
.insert(videos)
.select(unnestValues(body, videos))
.onConflictDoUpdate({
target: [videos.path],
set: conflictUpdateAllExcept(videos, ["pk", "id", "createdAt"]),
})
.returning({
pk: videos.pk,
id: videos.id,
path: videos.path,
guess: videos.guess,
});
} catch (e) {
if (!isUniqueConstraint(e)) throw e;
return status(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 [];
return x.for.map((e) => ({
video: vids.find((v) => v.path === x.path)!.pk,
entry: {
...e,
movie:
"movie" in e
? isUuid(e.movie)
? { id: e.movie }
: { slug: e.movie }
: undefined,
serie:
"serie" in e
? isUuid(e.serie)
? { id: e.serie }
: { slug: e.serie }
: undefined,
},
}));
});
if (!vidEntries.length) {
return status(
201,
vids.map((x) => ({
id: x.id,
path: x.path,
guess: x.guess,
entries: [],
})),
);
}
const links = await linkVideos(tx, vidEntries);
return status(
201,
vids.map((x) => ({
id: x.id,
path: x.path,
guess: x.guess,
entries: links[x.pk] ?? [],
})),
);
});
const ret = await createVideos(body, false);
if ("status" in ret) return status(ret.status, ret);
return status(201, ret);
},
{
detail: {
@@ -123,8 +133,39 @@ export const videosWriteH = new Elysia({ prefix: "/videos", tags: ["videos"] })
Create videos in bulk.
Duplicated videos will simply be ignored.
If a videos has a \`guess\` field, it will be used to automatically register the video under an existing
movie or entry.
The \`for\` field of each video can be used to link the video to an existing entry.
If the video was already registered, links will be merged (existing and new ones will be kept).
`,
},
body: t.Array(SeedVideo),
response: {
201: t.Array(CreatedVideo),
409: {
...KError,
description:
"Invalid rendering specified. (conflicts with an existing video)",
},
422: KError,
},
},
)
.put(
"",
async ({ body, status }) => {
const ret = await createVideos(body, true);
if ("status" in ret) return status(ret.status, ret);
return status(201, ret);
},
{
detail: {
description: comment`
Create videos in bulk.
Duplicated videos will simply be ignored.
The \`for\` field of each video can be used to link the video to an existing entry.
If the video was already registered, links will be overriden (existing will be removed and new ones will be created).
`,
},
body: t.Array(SeedVideo),
+24 -1
View File
@@ -11,6 +11,8 @@ import {
or,
type SQL,
sql,
inArray,
WithSubquery,
} from "drizzle-orm";
import { alias } from "drizzle-orm/pg-core";
import { Elysia, t } from "elysia";
@@ -275,6 +277,7 @@ export async function getVideos({
preferOriginal = false,
relations = [],
userId,
cte = [],
}: {
after?: string;
limit: number;
@@ -285,8 +288,10 @@ export async function getVideos({
preferOriginal?: boolean;
relations?: (keyof typeof videoRelations)[];
userId: string;
cte?: WithSubquery[];
}) {
let ret = await db
.with(...cte)
.select({
...getColumns(videos),
...buildRelations(relations, videoRelations, {
@@ -620,9 +625,27 @@ export const videosReadH = new Elysia({ tags: ["videos"] })
});
}
const titleGuess = db.$with("title_guess").as(
db
.selectDistinctOn([sql<string>`${videos.guess}->>'title'`], {
title: sql<string>`${videos.guess}->>'title'`.as("title"),
})
.from(videos)
.leftJoin(evJoin, eq(videos.pk, evJoin.videoPk))
.leftJoin(entries, eq(entries.pk, evJoin.entryPk))
.where(eq(entries.showPk, serie.pk)),
);
const languages = processLanguages(langs);
const items = await getVideos({
filter: eq(entries.showPk, serie.pk),
cte: [titleGuess],
filter: or(
eq(entries.showPk, serie.pk),
inArray(
sql<string>`${videos.guess}->>'title'`,
db.select().from(titleGuess),
),
),
limit,
after,
query,