diff --git a/api/src/controllers/videos.ts b/api/src/controllers/videos.ts index 292c0485..38ad0fdd 100644 --- a/api/src/controllers/videos.ts +++ b/api/src/controllers/videos.ts @@ -14,6 +14,7 @@ import { KError } from "~/models/error"; import { bubbleVideo } from "~/models/examples"; import { Page, + type Resource, Sort, createPage, isUuid, @@ -54,8 +55,9 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] }) slug: shows.slug, }) .from(videos) - .crossJoin( + .leftJoin( sql`jsonb_array_elements_text(${videos.guess}->'year') as year`, + sql`true`, ) .innerJoin(entryVideoJoin, eq(entryVideoJoin.videoPk, videos.pk)) .innerJoin(entries, eq(entries.pk, entryVideoJoin.entryPk)) @@ -78,7 +80,10 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] }) const [{ guesses }] = await db .with(years, guess) .select({ - guesses: jsonbObjectAgg(guess.guess, guess.years), + guesses: jsonbObjectAgg>( + guess.guess, + guess.years, + ), }) .from(guess); @@ -98,7 +103,7 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] }) return { paths: paths.map((x) => x.path), - guesses, + guesses: guesses ?? {}, unmatched: unmatched.map((x) => x.path), }; }, @@ -177,8 +182,7 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] }) path: videos.path, }); } catch (e) { - if (!isUniqueConstraint(e)) - throw e; + if (!isUniqueConstraint(e)) throw e; return error(409, { status: 409, message: comment` diff --git a/api/src/db/utils.ts b/api/src/db/utils.ts index c0caa80a..c57acffa 100644 --- a/api/src/db/utils.ts +++ b/api/src/db/utils.ts @@ -144,5 +144,7 @@ export const jsonbBuildObject = (select: JsonFields) => { }; export const isUniqueConstraint = (e: unknown): boolean => { - return typeof e === "object" && e != null && "code" in e && e.code === "23505"; + return ( + typeof e === "object" && e != null && "code" in e && e.code === "23505" + ); }; diff --git a/api/src/models/utils/resource.ts b/api/src/models/utils/resource.ts index 2fe7465d..878b956b 100644 --- a/api/src/models/utils/resource.ts +++ b/api/src/models/utils/resource.ts @@ -13,6 +13,7 @@ export const Resource = () => id: t.String({ format: "uuid" }), slug: t.String({ format: "slug" }), }); +export type Resource = ReturnType["static"]; const checker = TypeCompiler.Compile(t.String({ format: "uuid" })); export const isUuid = (id: string) => checker.Check(id); diff --git a/api/src/models/video.ts b/api/src/models/video.ts index 15ce5fab..f9cefaf2 100644 --- a/api/src/models/video.ts +++ b/api/src/models/video.ts @@ -167,10 +167,7 @@ export const Guesses = t.Object({ paths: t.Array(t.String()), guesses: t.Record( t.String(), - t.Record( - t.Union([t.Literal("unknown"), t.String({ pattern: "[1-9][0-9]*" })]), - Resource(), - ), + t.Record(t.String({ pattern: "^([1-9][0-9]{3})|unknown$" }), Resource()), ), unmatched: t.Array(t.String()), }); @@ -188,7 +185,7 @@ registerExamples(Guesses, { id: "43b742f5-9ce6-467d-ad29-74460624020a", slug: "evangelion", }, - 1995: { + "1995": { id: "43b742f5-9ce6-467d-ad29-74460624020a", slug: "evangelion", }, diff --git a/api/tests/helpers/videos-helper.ts b/api/tests/helpers/videos-helper.ts index 380cd81d..cc750a1a 100644 --- a/api/tests/helpers/videos-helper.ts +++ b/api/tests/helpers/videos-helper.ts @@ -17,3 +17,29 @@ export const createVideo = async (video: SeedVideo | SeedVideo[]) => { const body = await resp.json(); return [resp, body] as const; }; + +export const getVideos = async () => { + const resp = await app.handle( + new Request(buildUrl("videos"), { + method: "GET", + headers: await getJwtHeaders(), + }), + ); + const body = await resp.json(); + return [resp, body] as const; +}; + +export const deleteVideo = async (paths: string[]) => { + const resp = await app.handle( + new Request(buildUrl("videos"), { + method: "DELETE", + body: JSON.stringify(paths), + headers: { + "Content-Type": "application/json", + ...(await getJwtHeaders()), + }, + }), + ); + const body = await resp.json(); + return [resp, body] as const; +}; diff --git a/api/tests/manual.ts b/api/tests/manual.ts index ca821e3a..c038486b 100644 --- a/api/tests/manual.ts +++ b/api/tests/manual.ts @@ -1,7 +1,7 @@ import { db, migrate } from "~/db"; import { profiles, shows } from "~/db/schema"; -import { madeInAbyss } from "~/models/examples"; -import { createSerie, createVideo } from "./helpers"; +import { bubble, madeInAbyss } from "~/models/examples"; +import { createMovie, createSerie, createVideo, getVideos } from "./helpers"; // test file used to run manually using `bun tests/manual.ts` // run those before running this script @@ -12,22 +12,42 @@ await migrate(); await db.delete(shows); await db.delete(profiles); -const [__, ser] = await createSerie(madeInAbyss); -console.log(ser); -const [_, body] = await createVideo({ - guess: { title: "mia", season: [1], episode: [13], from: "test" }, - part: null, - path: "/video/mia s1e13.mkv", - rendering: "renderingsha", - version: 1, - for: [ - { - serie: madeInAbyss.slug, - season: madeInAbyss.entries[0].seasonNumber!, - episode: madeInAbyss.entries[0].episodeNumber!, +const [_, ser] = await createSerie(madeInAbyss); +const [__, mov] = await createMovie(bubble); +const [resp, body] = await createVideo([ + { + guess: { title: "mia", season: [1], episode: [13], from: "test" }, + part: null, + path: "/video/mia s1e13.mkv", + rendering: "sha2", + version: 1, + for: [{ slug: `${madeInAbyss.slug}-s1e13` }], + }, + { + guess: { + title: "mia", + season: [2], + episode: [1], + year: [2017], + from: "test", }, - ], -}); + part: null, + path: "/video/mia 2017 s2e1.mkv", + rendering: "sha8", + version: 1, + for: [{ slug: `${madeInAbyss.slug}-s2e1` }], + }, + { + guess: { title: "bubble", from: "test" }, + part: null, + path: "/video/bubble.mkv", + rendering: "sha5", + version: 1, + for: [{ movie: bubble.slug }], + }, +]); console.log(body); +const [___, ret] = await getVideos(); +console.log(JSON.stringify(ret, undefined, 4)); process.exit(0); diff --git a/api/tests/videos/getdel.test.ts b/api/tests/videos/getdel.test.ts new file mode 100644 index 00000000..e9c7075b --- /dev/null +++ b/api/tests/videos/getdel.test.ts @@ -0,0 +1,154 @@ +import { beforeAll, describe, expect, it } from "bun:test"; +import { + createMovie, + createSerie, + createVideo, + getVideos, +} from "tests/helpers"; +import { expectStatus } from "tests/utils"; +import { db } from "~/db"; +import { shows, videos } from "~/db/schema"; +import { bubble, madeInAbyss } from "~/models/examples"; + +beforeAll(async () => { + await db.delete(shows); + await db.delete(videos); + + let [ret, body] = await createSerie(madeInAbyss); + expectStatus(ret, body).toBe(201); + [ret, body] = await createMovie(bubble); + expectStatus(ret, body).toBe(201); + + [ret, body] = await createVideo([ + { + guess: { title: "mia", season: [1], episode: [13], from: "test" }, + part: null, + path: "/video/mia s1e13.mkv", + rendering: "sha2", + version: 1, + for: [{ slug: `${madeInAbyss.slug}-s1e13` }], + }, + { + guess: { + title: "mia", + season: [2], + episode: [1], + year: [2017], + from: "test", + }, + part: null, + path: "/video/mia 2017 s2e1.mkv", + rendering: "sha8", + version: 1, + for: [{ slug: `${madeInAbyss.slug}-s2e1` }], + }, + { + guess: { title: "bubble", from: "test" }, + part: null, + path: "/video/bubble.mkv", + rendering: "sha5", + version: 1, + for: [{ movie: bubble.slug }], + }, + ]); + expectStatus(ret, body).toBe(201); + expect(body).toBeArrayOfSize(3); + expect(body[0].entries).toBeArrayOfSize(1); + expect(body[1].entries).toBeArrayOfSize(1); + expect(body[2].entries).toBeArrayOfSize(1); +}); + +describe("Video get/deletion", () => { + it("Get current state", async () => { + const [resp, body] = await getVideos(); + expectStatus(resp, body).toBe(200); + expect(body.guesses).toMatchObject({ + mia: { + unknown: { + id: expect.any(String), + slug: "made-in-abyss", + }, + "2017": { + id: expect.any(String), + slug: "made-in-abyss", + }, + }, + bubble: { + unknown: { + id: expect.any(String), + slug: "bubble", + }, + }, + }); + }); + + it("With unknown", async () => { + let [resp, body] = await createVideo({ + guess: { title: "mia", season: [1], episode: [13], from: "test" }, + part: null, + path: "/video/mia s1e13 unknown test.mkv", + rendering: "shanthnth", + version: 1, + }); + expectStatus(resp, body).toBe(201); + + [resp, body] = await getVideos(); + expectStatus(resp, body).toBe(200); + expect(body.guesses).toMatchObject({ + mia: { + unknown: { + id: expect.any(String), + slug: "made-in-abyss", + }, + "2017": { + id: expect.any(String), + slug: "made-in-abyss", + }, + }, + bubble: { + unknown: { + id: expect.any(String), + slug: "bubble", + }, + }, + }); + expect(body.unmatched).toBeArrayOfSize(1); + expect(body.unmatched[0]).toBe("/video/mia s1e13 unknown test.mkv"); + }); + + it("Mismatch title guess", async () => { + let [resp, body] = await createVideo({ + guess: { title: "mia", season: [1], episode: [13], from: "test" }, + part: null, + path: "/video/mia s1e13 mismatch.mkv", + rendering: "mismatch", + version: 1, + for: [{ movie: "bubble" }], + }); + expectStatus(resp, body).toBe(201); + + [resp, body] = await getVideos(); + expectStatus(resp, body).toBe(200); + expect(body.guesses).toMatchObject({ + mia: { + unknown: { + id: expect.any(String), + // take the latest slug + slug: "bubble", + }, + "2017": { + id: expect.any(String), + slug: "made-in-abyss", + }, + }, + bubble: { + unknown: { + id: expect.any(String), + slug: "bubble", + }, + }, + }); + }); + + it.todo("Delete video", async () => {}); +}); diff --git a/api/tests/videos/scanner.test.ts b/api/tests/videos/scanner.test.ts index 070be851..011c2094 100644 --- a/api/tests/videos/scanner.test.ts +++ b/api/tests/videos/scanner.test.ts @@ -8,7 +8,6 @@ import { bubble, madeInAbyss } from "~/models/examples"; beforeAll(async () => { await db.delete(shows); - await db.delete(entries); await db.delete(videos); let [ret, body] = await createSerie(madeInAbyss); expectStatus(ret, body).toBe(201); @@ -358,7 +357,6 @@ describe("Video seeding", () => { expect(body.message).toBeString(); }); - it("Two for the same entry", async () => { const [resp, body] = await createVideo({ guess: {