From 32a1e89b272e065a99dbf38d872e8f5c711365e5 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 28 Jan 2025 21:28:36 +0100 Subject: [PATCH] Add insert seasons --- api/src/controllers/seed/insert/seasons.ts | 61 ++++++++++++++++++++++ api/src/controllers/seed/series.ts | 9 ++++ api/src/models/entry/base-entry.ts | 2 +- api/src/models/examples/bubble.ts | 4 +- api/src/models/examples/made-in-abyss.ts | 18 +++++++ api/src/models/season.ts | 1 + api/src/models/video.ts | 22 ++++---- api/tests/series/seed-serie.test.ts | 35 +++++++++++++ biome.json | 1 + 9 files changed, 140 insertions(+), 13 deletions(-) create mode 100644 api/src/controllers/seed/insert/seasons.ts create mode 100644 api/tests/series/seed-serie.test.ts diff --git a/api/src/controllers/seed/insert/seasons.ts b/api/src/controllers/seed/insert/seasons.ts new file mode 100644 index 00000000..56d649ae --- /dev/null +++ b/api/src/controllers/seed/insert/seasons.ts @@ -0,0 +1,61 @@ +import { db } from "~/db"; +import { seasonTranslations, seasons } from "~/db/schema"; +import { conflictUpdateAllExcept } from "~/db/utils"; +import type { SeedSeason } from "~/models/season"; +import { processOptImage } from "../images"; +import { guessNextRefresh } from "../refresh"; + +type SeasonI = typeof seasons.$inferInsert; +type SeasonTransI = typeof seasonTranslations.$inferInsert; + +export const insertSeasons = async ( + show: { pk: number; slug: string }, + items: SeedSeason[], +) => { + return db.transaction(async (tx) => { + const vals: SeasonI[] = items.map((x) => { + const { translations, ...season } = x; + return { + ...season, + showPk: show.pk, + slug: `${show.slug}-s${season.seasonNumber}`, + nextRefresh: guessNextRefresh(season.startAir ?? new Date()), + }; + }); + const ret = await tx + .insert(seasons) + .values(vals) + .onConflictDoUpdate({ + target: seasons.slug, + set: conflictUpdateAllExcept(seasons, [ + "pk", + "showPk", + "id", + "slug", + "createdAt", + ]), + }) + .returning({ pk: seasons.pk, id: seasons.id, slug: seasons.slug }); + + const trans: SeasonTransI[] = items.flatMap((seed, i) => + Object.entries(seed.translations).map(([lang, tr]) => ({ + // assumes ret is ordered like items. + pk: ret[i].pk, + language: lang, + ...tr, + poster: processOptImage(tr.poster), + thumbnail: processOptImage(tr.thumbnail), + banner: processOptImage(tr.banner), + })), + ); + await tx + .insert(seasonTranslations) + .values(trans) + .onConflictDoUpdate({ + target: [seasonTranslations.pk, seasonTranslations.language], + set: conflictUpdateAllExcept(seasonTranslations, ["pk", "language"]), + }); + + return ret; + }); +}; diff --git a/api/src/controllers/seed/series.ts b/api/src/controllers/seed/series.ts index 3bf8dd9b..349a7101 100644 --- a/api/src/controllers/seed/series.ts +++ b/api/src/controllers/seed/series.ts @@ -4,10 +4,17 @@ import { getYear } from "~/utils"; import { insertEntries } from "./insert/entries"; import { insertShow } from "./insert/shows"; import { guessNextRefresh } from "./refresh"; +import { insertSeasons } from "./insert/seasons"; export const SeedSerieResponse = t.Object({ id: t.String({ format: "uuid" }), slug: t.String({ format: "slug", examples: ["made-in-abyss"] }), + seasons: t.Array( + t.Object({ + id: t.String({ format: "uuid" }), + slug: t.String({ format: "slug", examples: ["made-in-abyss-s1"] }), + }), + ), entries: t.Array( t.Object({ id: t.String({ format: "uuid" }), @@ -55,12 +62,14 @@ export const seedSerie = async ( ); if ("status" in show) return show; + const retSeasons = await insertSeasons(show, seasons); const retEntries = await insertEntries(show, entries); return { updated: show.updated, id: show.id, slug: show.slug, + seasons: retSeasons, entries: retEntries, }; }; diff --git a/api/src/models/entry/base-entry.ts b/api/src/models/entry/base-entry.ts index dc7a880c..f848f6e3 100644 --- a/api/src/models/entry/base-entry.ts +++ b/api/src/models/entry/base-entry.ts @@ -2,7 +2,7 @@ import { t } from "elysia"; import { Image } from "../utils/image"; export const BaseEntry = t.Object({ - airDate: t.Nullable(t.String({ format: "data" })), + airDate: t.Nullable(t.String({ format: "date" })), runtime: t.Nullable( t.Number({ minimum: 0, description: "Runtime of the episode in minutes" }), ), diff --git a/api/src/models/examples/bubble.ts b/api/src/models/examples/bubble.ts index 3b816d9c..0246333c 100644 --- a/api/src/models/examples/bubble.ts +++ b/api/src/models/examples/bubble.ts @@ -1,5 +1,5 @@ -import type { SeedMovie } from "../movie"; -import type { Video } from "../video"; +import type { SeedMovie } from "~/models/movie"; +import type { Video } from "~/models/video"; export const bubbleVideo: Video = { id: "3cd436ee-01ff-4f45-ba98-62aabeb22f25", diff --git a/api/src/models/examples/made-in-abyss.ts b/api/src/models/examples/made-in-abyss.ts index c5cd652b..129008df 100644 --- a/api/src/models/examples/made-in-abyss.ts +++ b/api/src/models/examples/made-in-abyss.ts @@ -1,4 +1,22 @@ import type { SeedSerie } from "~/models/serie"; +import type { Video } from "~/models/video"; + +export const madeInAbyssVideo: Video = { + id: "3cd436ee-01ff-4f45-ba98-654282531234", + slug: "made-in-abyss-s1e1", + path: "/video/Made in abyss S01E01.mkv", + rendering: "459429fa062adeebedcc2bb04b9965de0262bfa453369783132d261be79021bd", + part: null, + version: 1, + guess: { + title: "Made in abyss", + season: [1], + episode: [1], + type: "episode", + from: "guessit", + }, + createdAt: "2024-11-23T15:01:24.968Z", +}; export const madeInAbyss = { slug: "made-in-abyss", diff --git a/api/src/models/season.ts b/api/src/models/season.ts index 4066b0c1..4777a30f 100644 --- a/api/src/models/season.ts +++ b/api/src/models/season.ts @@ -45,6 +45,7 @@ export const SeedSeason = t.Intersect([ ), }), ]); +export type SeedSeason = typeof SeedSeason.static; registerExamples(Season, { ...madeInAbyss.seasons[0], diff --git a/api/src/models/video.ts b/api/src/models/video.ts index 85c5057a..a57bbb6e 100644 --- a/api/src/models/video.ts +++ b/api/src/models/video.ts @@ -37,22 +37,24 @@ export const Video = t.Object({ t.Object( { title: t.String(), - year: t.Array(t.Integer(), { default: [] }), - season: t.Array(t.Integer(), { default: [] }), - episode: t.Array(t.Integer(), { default: [] }), + year: t.Optional(t.Array(t.Integer(), { default: [] })), + season: t.Optional(t.Array(t.Integer(), { default: [] })), + episode: t.Optional(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"])), from: t.String({ description: "Name of the tool that made the guess", }), - history: t.Array(t.Omit(Self, ["history"]), { - default: [], - description: comment` - When another tool refines the guess or a user manually edit it, the history of the guesses - are kept in this \`history\` value. - `, - }), + history: t.Optional( + t.Array(t.Omit(Self, ["history"]), { + default: [], + description: comment` + When another tool refines the guess or a user manually edit it, the history of the guesses + are kept in this \`history\` value. + `, + }), + ), }, { additionalProperties: true, diff --git a/api/tests/series/seed-serie.test.ts b/api/tests/series/seed-serie.test.ts new file mode 100644 index 00000000..976b57bf --- /dev/null +++ b/api/tests/series/seed-serie.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "bun:test"; +import { eq } from "drizzle-orm"; +import { expectStatus } from "tests/utils"; +import { db } from "~/db"; +import { seasons, shows, videos } from "~/db/schema"; +import { madeInAbyss, madeInAbyssVideo } from "~/models/examples"; +import { createSerie } from "../helpers"; + +describe("Serie seeding", () => { + it("Can create a serie with seasons and episodes", async () => { + // create video beforehand to test linking + await db.insert(videos).values(madeInAbyssVideo); + const [resp, body] = await createSerie(madeInAbyss); + + expectStatus(resp, body).toBe(201); + expect(body.id).toBeString(); + expect(body.slug).toBe("made-in-abyss"); + + const ret = await db.query.shows.findFirst({ + where: eq(shows.id, body.id), + with: { + seasons: { orderBy: seasons.seasonNumber }, + entries: true, + }, + }); + + expect(ret).not.toBeNull(); + expect(ret!.seasons).toBeArrayOfSize(2); + expect(ret!.seasons[0].slug).toBe("made-in-abyss-s1"); + expect(ret!.seasons[1].slug).toBe("made-in-abyss-s2"); + // expect(ret!.entries).toBeArrayOfSize( + // madeInAbyss.entries.length + madeInAbyss.extras.length, + // ); + }); +}); diff --git a/biome.json b/biome.json index 84dde2d8..897f520d 100644 --- a/biome.json +++ b/biome.json @@ -2,6 +2,7 @@ "$schema": "https://biomejs.dev/schemas/1.8.1/schema.json", "formatter": { "enabled": true, + "formatWithErrors": true, "indentStyle": "tab", "indentWidth": 2, "lineEnding": "lf",