diff --git a/api/src/controllers/videos.ts b/api/src/controllers/videos.ts index a51766cd..20c55628 100644 --- a/api/src/controllers/videos.ts +++ b/api/src/controllers/videos.ts @@ -1,16 +1,23 @@ +import { and, eq, inArray, sql } from "drizzle-orm"; import { Elysia, t } from "elysia"; import { db } from "~/db"; -import { videos as videosT } from "~/db/schema"; +import { entries, entryVideoJoin, shows, videos } from "~/db/schema"; import { bubbleVideo } from "~/models/examples"; import { SeedVideo, Video } from "~/models/video"; import { comment } from "~/utils"; +import { computeVideoSlug } from "./seed/insert/entries"; const CreatedVideo = t.Object({ id: t.String({ format: "uuid" }), - path: t.String({ example: bubbleVideo.path }), + path: t.String({ examples: [bubbleVideo.path] }), + // entries: t.Array( + // t.Object({ + // slug: t.String({ format: "slug", examples: ["bubble-v2"] }), + // }), + // ), }); -export const videos = new Elysia({ prefix: "/videos", tags: ["videos"] }) +export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] }) .model({ video: Video, "created-videos": t.Array(CreatedVideo), @@ -22,20 +29,101 @@ export const videos = new Elysia({ prefix: "/videos", tags: ["videos"] }) .post( "", async ({ body, error }) => { - const ret = await db - .insert(videosT) + const oldRet = await db + .insert(videos) .values(body) .onConflictDoNothing() - .returning({ id: videosT.id, path: videosT.path }); - return error(201, ret); + .returning({ + pk: videos.pk, + id: videos.id, + path: videos.path, + guess: videos.guess, + }); + return error(201, oldRet); + + // TODO: this is a huge untested wip + const vidsI = db.$with("vidsI").as( + db.insert(videos).values(body).onConflictDoNothing().returning({ + pk: videos.pk, + id: videos.id, + path: videos.path, + guess: videos.guess, + }), + ); + + const findEntriesQ = db + .select({ + guess: videos.guess, + entryPk: entries.pk, + showSlug: shows.slug, + // TODO: handle extras here + // guessit can't know if an episode is a special or not. treat specials like a normal episode. + kind: sql` + case when ${entries.kind} = 'movie' then 'movie' else 'episode' end + `.as("kind"), + season: entries.seasonNumber, + episode: entries.episodeNumber, + }) + .from(entries) + .leftJoin(entryVideoJoin, eq(entryVideoJoin.entry, entries.pk)) + .leftJoin(videos, eq(videos.pk, entryVideoJoin.video)) + .leftJoin(shows, eq(shows.pk, entries.showPk)) + .as("find_entries"); + + const hasRenderingQ = db + .select() + .from(entryVideoJoin) + .where(eq(entryVideoJoin.entry, findEntriesQ.entryPk)); + + const ret = await db + .with(vidsI) + .insert(entryVideoJoin) + .select( + db + .select({ + entry: findEntriesQ.entryPk, + video: vidsI.pk, + slug: computeVideoSlug( + findEntriesQ.showSlug, + sql`exists(${hasRenderingQ})`, + ), + }) + .from(vidsI) + .leftJoin( + findEntriesQ, + and( + eq( + sql`${findEntriesQ.guess}->'title'`, + sql`${vidsI.guess}->'title'`, + ), + // TODO: find if @> with a jsonb created on the fly is + // better than multiples checks + sql`${vidsI.guess} @> {"kind": }::jsonb`, + inArray(findEntriesQ.kind, sql`${vidsI.guess}->'type'`), + inArray(findEntriesQ.episode, sql`${vidsI.guess}->'episode'`), + inArray(findEntriesQ.season, sql`${vidsI.guess}->'season'`), + ), + ), + ) + .onConflictDoNothing() + .returning({ + slug: entryVideoJoin.slug, + entryPk: entryVideoJoin.entry, + id: vidsI.id, + path: vidsI.path, + }); + return error(201, ret as any); }, { body: t.Array(SeedVideo), - response: { 201: "created-videos" }, + response: { 201: t.Array(CreatedVideo) }, detail: { description: comment` 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. `, }, }, diff --git a/api/src/index.ts b/api/src/index.ts index 5950fd48..348d459b 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -7,7 +7,7 @@ import { movies } from "./controllers/movies"; import { seasonsH } from "./controllers/seasons"; import { seed } from "./controllers/seed"; import { series } from "./controllers/series"; -import { videos } from "./controllers/videos"; +import { videosH } from "./controllers/videos"; import { migrate } from "./db"; import { Image } from "./models/utils"; import { comment } from "./utils"; @@ -75,7 +75,7 @@ const app = new Elysia() .use(series) .use(entries) .use(seasonsH) - .use(videos) + .use(videosH) .use(seed) .listen(3000); diff --git a/api/src/models/video.ts b/api/src/models/video.ts index 502a4f10..85c5057a 100644 --- a/api/src/models/video.ts +++ b/api/src/models/video.ts @@ -2,9 +2,6 @@ import { type TSchema, t } from "elysia"; import { comment } from "../utils"; import { bubbleVideo, registerExamples } from "./examples"; -const Guess = (schema: T) => - t.Optional(t.Union([schema, t.Array(schema)])); - export const Video = t.Object({ id: t.String({ format: "uuid" }), slug: t.String({ format: "slug" }), @@ -40,9 +37,9 @@ export const Video = t.Object({ t.Object( { title: t.String(), - year: Guess(t.Integer()), - season: Guess(t.Integer()), - episode: Guess(t.Integer()), + year: t.Array(t.Integer(), { default: [] }), + season: t.Array(t.Integer(), { default: [] }), + episode: t.Array(t.Integer(), { default: [] }), // TODO: maybe replace "extra" with the `extraKind` value (aka behind-the-scene, trailer, etc) type: t.Optional(t.UnionEnum(["episode", "movie", "extra"])), diff --git a/api/tests/helper.ts b/api/tests/helper.ts index 089570ac..80950c61 100644 --- a/api/tests/helper.ts +++ b/api/tests/helper.ts @@ -4,7 +4,7 @@ import { base } from "~/base"; import { movies } from "~/controllers/movies"; import { seed } from "~/controllers/seed"; import { series } from "~/controllers/series"; -import { videos } from "~/controllers/videos"; +import { videosH } from "~/controllers/videos"; import type { SeedMovie } from "~/models/movie"; import type { SeedVideo } from "~/models/video"; @@ -12,7 +12,7 @@ export const app = new Elysia() .use(base) .use(movies) .use(series) - .use(videos) + .use(videosH) .use(seed); export const getMovie = async (