diff --git a/api/src/controllers/profiles/history.ts b/api/src/controllers/profiles/history.ts index 660922c1..02b57cf2 100644 --- a/api/src/controllers/profiles/history.ts +++ b/api/src/controllers/profiles/history.ts @@ -79,7 +79,6 @@ export const historyH = new Elysia({ tags: ["profiles"] }) filter: and( isNotNull(entryProgressQ.playedDate), ne(entries.kind, "extra"), - ne(entries.kind, "unknown"), filter, ), languages: langs, @@ -125,7 +124,6 @@ export const historyH = new Elysia({ tags: ["profiles"] }) filter: and( isNotNull(entryProgressQ.playedDate), ne(entries.kind, "extra"), - ne(entries.kind, "unknown"), filter, ), languages: langs, diff --git a/api/src/controllers/seed/insert/entries.ts b/api/src/controllers/seed/insert/entries.ts index ee62d4a8..941e635b 100644 --- a/api/src/controllers/seed/insert/entries.ts +++ b/api/src/controllers/seed/insert/entries.ts @@ -212,10 +212,10 @@ export const insertEntries = async ( })); }; -export function computeVideoSlug(showSlug: SQL | Column, needsRendering: SQL) { +export function computeVideoSlug(entrySlug: SQL | Column, needsRendering: SQL) { return sql` concat( - ${showSlug}, + ${entrySlug}, case when ${videos.part} is not null then ('-p' || ${videos.part}) else '' end, case when ${videos.version} <> 1 then ('-v' || ${videos.version}) else '' end, case when ${needsRendering} then concat('-', ${videos.rendering}) else '' end diff --git a/api/src/controllers/videos.ts b/api/src/controllers/videos.ts index ded21415..b9d51e03 100644 --- a/api/src/controllers/videos.ts +++ b/api/src/controllers/videos.ts @@ -210,11 +210,12 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] }) pk: entries.pk, id: entries.id, slug: entries.slug, + kind: entries.kind, seasonNumber: entries.seasonNumber, episodeNumber: entries.episodeNumber, order: entries.order, - showId: shows.id, - showSlug: shows.slug, + showId: sql`${shows.id}`.as("showId"), + showSlug: sql`${shows.slug}`.as("showSlug"), }) .from(entries) .innerJoin(shows, eq(entries.showPk, shows.pk)) @@ -231,10 +232,10 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] }) db .select({ entryPk: entriesQ.pk, - videoPk: sql`j.video`, + videoPk: videos.pk, slug: computeVideoSlug( - entriesQ.showSlug, - sql`j.needRendering || exists(${hasRenderingQ})`, + entriesQ.slug, + sql`j.needRendering or exists(${hasRenderingQ})`, ), }) .from( @@ -244,39 +245,51 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] }) entry: "jsonb", }).as("j"), ) + .innerJoin(videos, eq(videos.pk, sql`j.video`)) .innerJoin( entriesQ, or( and( sql`j.entry ? 'slug'`, - eq(entriesQ.slug, sql`j.entry->'slug'`), + eq(entriesQ.slug, sql`j.entry->>'slug'`), ), and( sql`j.entry ? 'movie'`, or( - eq(entriesQ.showId, sql`j.entry #> '{movie, id}'`), - eq(entriesQ.showSlug, sql`j.entry #> '{movie, slug}'`), + eq(entriesQ.showId, sql`(j.entry #>> '{movie, id}')::uuid`), + eq(entriesQ.showSlug, sql`j.entry #>> '{movie, slug}'`), ), + eq(entriesQ.kind, "movie"), ), and( sql`j.entry ? 'serie'`, or( - eq(entriesQ.showId, sql`j.entry #> '{serie, id}'`), - eq(entriesQ.showSlug, sql`j.entry #> '{serie, slug}'`), + eq(entriesQ.showId, sql`(j.entry #>> '{serie, id}')::uuid`), + eq(entriesQ.showSlug, sql`j.entry #>> '{serie, slug}'`), ), or( and( sql`j.entry ?& array['season', 'episode']`, - eq(entriesQ.seasonNumber, sql`j.entry->'season'`), - eq(entriesQ.episodeNumber, sql`j.entry->'episode'`), + eq( + entriesQ.seasonNumber, + sql`(j.entry->>'season')::integer`, + ), + eq( + entriesQ.episodeNumber, + sql`(j.entry->>'episode')::integer`, + ), ), and( sql`j.entry ? 'order'`, - eq(entriesQ.order, sql`j.entry->'order'`), + eq(entriesQ.order, sql`(j.entry->>'order')::float`), ), and( sql`j.entry ? 'special'`, - eq(entriesQ.episodeNumber, sql`j.entry->'special'`), + eq( + entriesQ.episodeNumber, + sql`(j.entry->>'special')::integer`, + ), + eq(entriesQ.kind, "special"), ), ), ), @@ -299,7 +312,11 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] }) ); return error( 201, - vids.map((x) => ({ id: x.id, path: x.path, entries: entr[x.pk] })), + vids.map((x) => ({ + id: x.id, + path: x.path, + entries: entr[x.pk] ?? [], + })), ); }, { @@ -313,7 +330,7 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] }) `, }, body: t.Array(SeedVideo), - // response: { 201: t.Array(CreatedVideo) }, + response: { 201: t.Array(CreatedVideo) }, }, ) .delete( diff --git a/api/src/models/examples/made-in-abyss.ts b/api/src/models/examples/made-in-abyss.ts index addcd277..58d1dfcf 100644 --- a/api/src/models/examples/made-in-abyss.ts +++ b/api/src/models/examples/made-in-abyss.ts @@ -3,7 +3,6 @@ import type { Video } from "~/models/video"; export const madeInAbyssVideo: Video = { id: "3cd436ee-01ff-4f45-ba98-654282531234", - slug: "made-in-abyss-s1e13", path: "/video/Made in abyss S01E13.mkv", rendering: "459429fa062adeebedcc2bb04b9965de0262bfa453369783132d261be79021bd", part: null, diff --git a/api/src/models/video.ts b/api/src/models/video.ts index 2cd899e6..04b98bd8 100644 --- a/api/src/models/video.ts +++ b/api/src/models/video.ts @@ -78,7 +78,7 @@ export const SeedVideo = t.Object({ }), }), t.Object({ - externalId: t.Optional(t.Union([EpisodeId, ExternalId()])), + externalId: t.Union([EpisodeId, ExternalId()]), }), t.Object({ movie: t.Union([ diff --git a/api/tests/manual.ts b/api/tests/manual.ts index 6ad8c521..9c7661d5 100644 --- a/api/tests/manual.ts +++ b/api/tests/manual.ts @@ -1,29 +1,34 @@ import { db, migrate } from "~/db"; import { profiles, shows } from "~/db/schema"; import { madeInAbyss } from "~/models/examples"; -import { createSerie, getSerie, setSerieStatus } from "./helpers"; -import { getJwtHeaders } from "./helpers/jwt"; +import { createSerie, createVideo } from "./helpers"; // test file used to run manually using `bun tests/manual.ts` +// run those before running this script +// export JWT_SECRET="this is a secret"; +// export JWT_ISSUER="https://kyoo.zoriya.dev"; + await migrate(); await db.delete(shows); await db.delete(profiles); -console.log(await getJwtHeaders()); - -const [_, ser] = await createSerie(madeInAbyss); +const [__, ser] = await createSerie(madeInAbyss); console.log(ser); -const [__, ret] = await setSerieStatus(madeInAbyss.slug, { - status: "watching", - startedAt: "2024-12-21", - completedAt: "2024-12-21", - seenCount: 2, - score: 85, +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!, + }, + ], }); -console.log(ret); - -const [___, got] = await getSerie(madeInAbyss.slug, {}); -console.log(JSON.stringify(got, undefined, 4)); +console.log(body); process.exit(0); diff --git a/api/tests/series/get-entries.test.ts b/api/tests/series/get-entries.test.ts index 1a31465d..67cd70c7 100644 --- a/api/tests/series/get-entries.test.ts +++ b/api/tests/series/get-entries.test.ts @@ -48,7 +48,7 @@ describe("Get entries", () => { expect(body.items[0].videos).toBeArrayOfSize(1); expect(body.items[0].videos[0]).toMatchObject({ path: madeInAbyssVideo.path, - slug: madeInAbyssVideo.slug, + slug: `${madeInAbyss.slug}-s1e13`, version: madeInAbyssVideo.version, rendering: madeInAbyssVideo.rendering, part: madeInAbyssVideo.part, @@ -63,7 +63,7 @@ describe("Get entries", () => { expect(body.items[0].videos).toBeArrayOfSize(1); expect(body.items[0].videos[0]).toMatchObject({ path: madeInAbyssVideo.path, - slug: madeInAbyssVideo.slug, + slug: `${madeInAbyss.slug}-s1e13`, version: madeInAbyssVideo.version, rendering: madeInAbyssVideo.rendering, part: madeInAbyssVideo.part, diff --git a/api/tests/series/seed-serie.test.ts b/api/tests/series/seed-serie.test.ts index d6a0111f..44c73f9d 100644 --- a/api/tests/series/seed-serie.test.ts +++ b/api/tests/series/seed-serie.test.ts @@ -57,7 +57,7 @@ describe("Serie seeding", () => { ], evj: [ expect.objectContaining({ - slug: madeInAbyssVideo.slug, + slug: `${madeInAbyss.slug}-s1e13`, video: expect.objectContaining({ path: madeInAbyssVideo.path }), }), ], diff --git a/api/tests/videos/scanner.test.ts b/api/tests/videos/scanner.test.ts index 02bd7535..c5302079 100644 --- a/api/tests/videos/scanner.test.ts +++ b/api/tests/videos/scanner.test.ts @@ -77,18 +77,50 @@ describe("Video seeding", () => { expect(vid!.evj[0].entry.slug).toBe(`${madeInAbyss.slug}-s1e13`); }); + it("With movie", async () => { + const [resp, body] = await createVideo({ + guess: { title: "bubble", from: "test" }, + part: null, + path: "/video/bubble.mkv", + rendering: "sha", + version: 1, + for: [{ movie: bubble.slug }], + }); + + expectStatus(resp, body).toBe(201); + expect(body).toBeArrayOfSize(1); + expect(body[0].id).toBeString(); + + const vid = await db.query.videos.findFirst({ + where: eq(videos.id, body[0].id), + with: { + evj: { with: { entry: true } }, + }, + }); + + expect(vid).not.toBeNil(); + expect(vid!.path).toBe("/video/bubble.mkv"); + expect(vid!.guess).toMatchObject({ title: "bubble", from: "test" }); + + expect(body[0].entries).toBeArrayOfSize(1); + expect(vid!.evj).toBeArrayOfSize(1); + + expect(vid!.evj[0].slug).toBe(bubble.slug); + expect(vid!.evj[0].entry.slug).toBe(bubble.slug); + }); + it("With season/episode", async () => { const [resp, body] = await createVideo({ - guess: { title: "mia", season: [1], episode: [13], from: "test" }, + guess: { title: "mia", season: [2], episode: [1], from: "test" }, part: null, - path: "/video/mia s1e13.mkv", + path: "/video/mia s2e1.mkv", rendering: "renderingsha", version: 1, for: [ { serie: madeInAbyss.slug, - season: madeInAbyss.entries[0].seasonNumber!, - episode: madeInAbyss.entries[0].episodeNumber!, + season: madeInAbyss.entries[3].seasonNumber!, + episode: madeInAbyss.entries[3].episodeNumber!, }, ], }); @@ -105,13 +137,87 @@ describe("Video seeding", () => { }); expect(vid).not.toBeNil(); - expect(vid!.path).toBe("/video/mia s1e13.mkv"); + expect(vid!.path).toBe("/video/mia s2e1.mkv"); expect(vid!.guess).toMatchObject({ title: "mia", from: "test" }); expect(body[0].entries).toBeArrayOfSize(1); expect(vid!.evj).toBeArrayOfSize(1); - expect(vid!.evj[0].slug).toBe(`${madeInAbyss.slug}-s1e13-renderingsha`); - expect(vid!.evj[0].entry.slug).toBe(`${madeInAbyss.slug}-s1e13`); + expect(vid!.evj[0].slug).toBe(`${madeInAbyss.slug}-s2e1`); + expect(vid!.evj[0].entry.slug).toBe(`${madeInAbyss.slug}-s2e1`); + }); + + it("With special", async () => { + const [resp, body] = await createVideo({ + guess: { title: "mia", season: [0], episode: [3], from: "test" }, + part: null, + path: "/video/mia sp3.mkv", + rendering: "notehu", + version: 1, + for: [ + { + serie: madeInAbyss.slug, + special: madeInAbyss.entries[1].number!, + }, + ], + }); + + expectStatus(resp, body).toBe(201); + expect(body).toBeArrayOfSize(1); + expect(body[0].id).toBeString(); + + const vid = await db.query.videos.findFirst({ + where: eq(videos.id, body[0].id), + with: { + evj: { with: { entry: true } }, + }, + }); + + expect(vid).not.toBeNil(); + expect(vid!.path).toBe("/video/mia sp3.mkv"); + expect(vid!.guess).toMatchObject({ title: "mia", from: "test" }); + + expect(body[0].entries).toBeArrayOfSize(1); + expect(vid!.evj).toBeArrayOfSize(1); + + expect(vid!.evj[0].slug).toBe(`${madeInAbyss.slug}-sp3`); + expect(vid!.evj[0].entry.slug).toBe(`${madeInAbyss.slug}-sp3`); + }); + + it("With order", async () => { + const [resp, body] = await createVideo({ + guess: { title: "mia", season: [0], episode: [3], from: "test" }, + part: null, + path: "/video/mia 13.5.mkv", + rendering: "notehu", + version: 1, + for: [ + { + serie: madeInAbyss.slug, + order: 13.5, + }, + ], + }); + + expectStatus(resp, body).toBe(201); + expect(body).toBeArrayOfSize(1); + expect(body[0].id).toBeString(); + + const vid = await db.query.videos.findFirst({ + where: eq(videos.id, body[0].id), + with: { + evj: { with: { entry: true } }, + }, + }); + + expect(vid).not.toBeNil(); + expect(vid!.path).toBe("/video/mia 13.5.mkv"); + expect(vid!.guess).toMatchObject({ title: "mia", from: "test" }); + + expect(body[0].entries).toBeArrayOfSize(1); + expect(vid!.evj).toBeArrayOfSize(1); + + expect(vid!.evj[0].slug).toBe("made-in-abyss-dawn-of-the-deep-soul"); + expect(vid!.evj[0].entry.slug).toBe("made-in-abyss-dawn-of-the-deep-soul"); }); });