From 07a41bb17514a3caf574ee674587922d3937321b Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Thu, 1 May 2025 18:49:19 +0200 Subject: [PATCH] Fix `POST /videos` --- api/src/controllers/videos.ts | 105 +++++++++++++++++-------------- api/src/db/utils.ts | 1 + api/src/models/video.ts | 7 ++- api/tests/videos/scanner.test.ts | 6 +- shell.nix | 2 +- 5 files changed, 68 insertions(+), 53 deletions(-) diff --git a/api/src/controllers/videos.ts b/api/src/controllers/videos.ts index 5b9298d4..3a8406d7 100644 --- a/api/src/controllers/videos.ts +++ b/api/src/controllers/videos.ts @@ -161,20 +161,49 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] }) .post( "", async ({ body, error }) => { - const vidsI = db.$with("vidsI").as( - db - .insert(videos) - .values(body) - .onConflictDoUpdate({ - target: [videos.path], - set: conflictUpdateAllExcept(videos, ["pk", "id", "createdAt"]), - }) - .returning({ - pk: videos.pk, - id: videos.id, - path: videos.path, - }), - ); + const vids = await db + .insert(videos) + .values(body) + .onConflictDoUpdate({ + target: [videos.path], + set: conflictUpdateAllExcept(videos, ["pk", "id", "createdAt"]), + }) + .returning({ + pk: videos.pk, + id: videos.id, + path: videos.path, + }); + + const vidEntries = body.flatMap((x) => { + if (!x.for) return []; + return x.for.map((e) => ({ + video: vids.find((v) => v.path === x.path)!.pk, + path: x.path, + needRendering: x.for!.length > 1, + 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 error( + 201, + vids.map((x) => ({ id: x.id, path: x.path, entries: [] })), + ); + } const entriesQ = db .select({ @@ -197,45 +226,18 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] }) .where(eq(entryVideoJoin.entryPk, entriesQ.pk)); const ret = await db - .with(vidsI) .insert(entryVideoJoin) .select( db .select({ entry: entries.pk, - video: vidsI.pk, + video: sql`j.video`, slug: computeVideoSlug( entriesQ.showSlug, sql`j.needRendering::boolean || exists(${hasRenderingQ})`, ), }) - .from( - values( - body.flatMap((x) => { - if (!x.for) return []; - return x.for.map((e) => ({ - path: x.path, - needRendering: x.for!.length > 1, - 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, - }, - })); - }), - ).as("j"), - ) - .innerJoin(vidsI, eq(vidsI.path, sql`j.path`)) + .from(values(vidEntries).as("j")) .innerJoin( entriesQ, or( @@ -279,11 +281,20 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] }) .returning({ slug: entryVideoJoin.slug, entryPk: entryVideoJoin.entryPk, - id: vidsI.id, - path: vidsI.path, + videoPk: entryVideoJoin.videoPk, }); - return error(201, ret); - // return error(201, ret.map(x => ({ id: x.id, slug: x.}))); + const entr = ret.reduce( + (acc, x) => { + acc[x.videoPk] ??= []; + acc[x.videoPk].push({ slug: x.slug }); + return acc; + }, + {} as Record, + ); + return error( + 201, + vids.map((x) => ({ id: x.id, path: x.path, entries: entr[x.pk] })), + ); }, { detail: { diff --git a/api/src/db/utils.ts b/api/src/db/utils.ts index 5681eac9..71950631 100644 --- a/api/src/db/utils.ts +++ b/api/src/db/utils.ts @@ -76,6 +76,7 @@ export function sqlarr(array: unknown[]) { // See https://github.com/drizzle-team/drizzle-orm/issues/4044 // TODO: type values (everything is a `text` for now) export function values(items: Record[]) { + if (items[0] === undefined) throw new Error("Invalid values, expecting at least one items") const [firstProp, ...props] = Object.keys(items[0]); const values = items .map((x) => { diff --git a/api/src/models/video.ts b/api/src/models/video.ts index 4877093a..2cd899e6 100644 --- a/api/src/models/video.ts +++ b/api/src/models/video.ts @@ -122,8 +122,11 @@ export const Video = t.Intersect([ ]); export type Video = Prettify; -// type used in entry responses -export const EmbeddedVideo = t.Omit(Video, ["guess", "createdAt", "updatedAt"]); +// type used in entry responses (the slug comes from the entryVideoJoin) +export const EmbeddedVideo = t.Intersect([ + t.Object({ slug: t.String({ format: "slug" }) }), + t.Omit(Video, ["guess", "createdAt", "updatedAt"]), +]); export type EmbeddedVideo = Prettify; registerExamples(Video, bubbleVideo); diff --git a/api/tests/videos/scanner.test.ts b/api/tests/videos/scanner.test.ts index 89525760..02bd7535 100644 --- a/api/tests/videos/scanner.test.ts +++ b/api/tests/videos/scanner.test.ts @@ -39,7 +39,7 @@ describe("Video seeding", () => { expect(vid).not.toBeNil(); expect(vid!.path).toBe("/video/unknown s1e13.mkv"); - expect(vid!.guess).toBe({ title: "unknown", from: "test" }); + expect(vid!.guess).toMatchObject({ title: "unknown", from: "test" }); expect(body[0].entries).toBeArrayOfSize(0); expect(vid!.evj).toBeArrayOfSize(0); @@ -68,7 +68,7 @@ describe("Video seeding", () => { expect(vid).not.toBeNil(); expect(vid!.path).toBe("/video/mia s1e13.mkv"); - expect(vid!.guess).toBe({ title: "mia", from: "test" }); + expect(vid!.guess).toMatchObject({ title: "mia", from: "test" }); expect(body[0].entries).toBeArrayOfSize(1); expect(vid!.evj).toBeArrayOfSize(1); @@ -106,7 +106,7 @@ describe("Video seeding", () => { expect(vid).not.toBeNil(); expect(vid!.path).toBe("/video/mia s1e13.mkv"); - expect(vid!.guess).toBe({ title: "mia", from: "test" }); + expect(vid!.guess).toMatchObject({ title: "mia", from: "test" }); expect(body[0].entries).toBeArrayOfSize(1); expect(vid!.evj).toBeArrayOfSize(1); diff --git a/shell.nix b/shell.nix index b7ce3bfb..18e08f33 100644 --- a/shell.nix +++ b/shell.nix @@ -20,7 +20,7 @@ in pkgs.mkShell { packages = with pkgs; [ - nodejs-18_x + # nodejs-18_x nodePackages.yarn dotnet csharpier