Add collections in v5 (#821)

This commit is contained in:
Zoe Roux 2025-03-02 17:32:26 +01:00 committed by GitHub
commit 9b1bd69eae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 1255 additions and 30 deletions

View File

@ -8,7 +8,7 @@ The many-to-many relation between entries (episodes/movies) & videos is NOT a mi
erDiagram erDiagram
shows { shows {
guid id PK guid id PK
kind kind "serie|movie" kind kind "serie|movie|collection"
string(128) slug UK string(128) slug UK
genre[] genres genre[] genres
int rating "From 0 to 100" int rating "From 0 to 100"
@ -20,6 +20,7 @@ erDiagram
jsonb external_id jsonb external_id
guid studio_id FK guid studio_id FK
string original_language string original_language
guid collection_id FK
} }
show_translations { show_translations {
guid id PK, FK guid id PK, FK
@ -37,6 +38,7 @@ erDiagram
} }
shows ||--|{ show_translations : has shows ||--|{ show_translations : has
shows |o--|| entries : has shows |o--|| entries : has
shows |o--|| shows : has_collection
entries { entries {
guid id PK guid id PK
@ -70,23 +72,6 @@ erDiagram
} }
video }|--|{ entries : for video }|--|{ entries : for
collections {
guid id PK
string(256) slug UK
datetime added_date
datetime next_refresh
}
collection_translations {
guid id PK, FK
string language PK
string name "NN"
jsonb poster
jsonb thumbnail
}
collections ||--|{ collection_translations : has
collections |o--|{ shows : has
seasons { seasons {
guid id PK guid id PK
string(256) slug UK string(256) slug UK

View File

@ -0,0 +1,3 @@
ALTER TYPE "kyoo"."show_kind" ADD VALUE 'collection';--> statement-breakpoint
ALTER TABLE "kyoo"."shows" ADD COLUMN "collection_pk" integer;--> statement-breakpoint
ALTER TABLE "kyoo"."shows" ADD CONSTRAINT "shows_collection_pk_shows_pk_fk" FOREIGN KEY ("collection_pk") REFERENCES "kyoo"."shows"("pk") ON DELETE set null ON UPDATE no action;

View File

@ -0,0 +1,969 @@
{
"id": "7a04670c-5fb9-4535-b6be-dc291b8b0b09",
"prevId": "5c17dd71-409a-4c80-870d-f12386676738",
"version": "7",
"dialect": "postgresql",
"tables": {
"kyoo.entries": {
"name": "entries",
"schema": "kyoo",
"columns": {
"pk": {
"name": "pk",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "entries_pk_seq",
"schema": "kyoo",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"show_pk": {
"name": "show_pk",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"order": {
"name": "order",
"type": "real",
"primaryKey": false,
"notNull": false
},
"season_number": {
"name": "season_number",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"episode_number": {
"name": "episode_number",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"kind": {
"name": "kind",
"type": "entry_type",
"typeSchema": "kyoo",
"primaryKey": false,
"notNull": true
},
"extra_kind": {
"name": "extra_kind",
"type": "text",
"primaryKey": false,
"notNull": false
},
"air_date": {
"name": "air_date",
"type": "date",
"primaryKey": false,
"notNull": false
},
"runtime": {
"name": "runtime",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"thumbnail": {
"name": "thumbnail",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"external_id": {
"name": "external_id",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"next_refresh": {
"name": "next_refresh",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"entries_show_pk_shows_pk_fk": {
"name": "entries_show_pk_shows_pk_fk",
"tableFrom": "entries",
"tableTo": "shows",
"schemaTo": "kyoo",
"columnsFrom": ["show_pk"],
"columnsTo": ["pk"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"entries_id_unique": {
"name": "entries_id_unique",
"nullsNotDistinct": false,
"columns": ["id"]
},
"entries_slug_unique": {
"name": "entries_slug_unique",
"nullsNotDistinct": false,
"columns": ["slug"]
},
"entries_showPk_seasonNumber_episodeNumber_unique": {
"name": "entries_showPk_seasonNumber_episodeNumber_unique",
"nullsNotDistinct": false,
"columns": ["show_pk", "season_number", "episode_number"]
}
},
"policies": {},
"checkConstraints": {
"order_positive": {
"name": "order_positive",
"value": "\"kyoo\".\"entries\".\"order\" >= 0"
}
},
"isRLSEnabled": false
},
"kyoo.entry_translations": {
"name": "entry_translations",
"schema": "kyoo",
"columns": {
"pk": {
"name": "pk",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"language": {
"name": "language",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"tagline": {
"name": "tagline",
"type": "text",
"primaryKey": false,
"notNull": false
},
"poster": {
"name": "poster",
"type": "jsonb",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"entry_translations_pk_entries_pk_fk": {
"name": "entry_translations_pk_entries_pk_fk",
"tableFrom": "entry_translations",
"tableTo": "entries",
"schemaTo": "kyoo",
"columnsFrom": ["pk"],
"columnsTo": ["pk"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"entry_translations_pk_language_pk": {
"name": "entry_translations_pk_language_pk",
"columns": ["pk", "language"]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"kyoo.season_translations": {
"name": "season_translations",
"schema": "kyoo",
"columns": {
"pk": {
"name": "pk",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"language": {
"name": "language",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"poster": {
"name": "poster",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"thumbnail": {
"name": "thumbnail",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"banner": {
"name": "banner",
"type": "jsonb",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"season_translations_pk_seasons_pk_fk": {
"name": "season_translations_pk_seasons_pk_fk",
"tableFrom": "season_translations",
"tableTo": "seasons",
"schemaTo": "kyoo",
"columnsFrom": ["pk"],
"columnsTo": ["pk"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"season_translations_pk_language_pk": {
"name": "season_translations_pk_language_pk",
"columns": ["pk", "language"]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"kyoo.seasons": {
"name": "seasons",
"schema": "kyoo",
"columns": {
"pk": {
"name": "pk",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "seasons_pk_seq",
"schema": "kyoo",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"show_pk": {
"name": "show_pk",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"season_number": {
"name": "season_number",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"start_air": {
"name": "start_air",
"type": "date",
"primaryKey": false,
"notNull": false
},
"end_air": {
"name": "end_air",
"type": "date",
"primaryKey": false,
"notNull": false
},
"external_id": {
"name": "external_id",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"next_refresh": {
"name": "next_refresh",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"show_fk": {
"name": "show_fk",
"columns": [
{
"expression": "show_pk",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "hash",
"with": {}
}
},
"foreignKeys": {
"seasons_show_pk_shows_pk_fk": {
"name": "seasons_show_pk_shows_pk_fk",
"tableFrom": "seasons",
"tableTo": "shows",
"schemaTo": "kyoo",
"columnsFrom": ["show_pk"],
"columnsTo": ["pk"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"seasons_id_unique": {
"name": "seasons_id_unique",
"nullsNotDistinct": false,
"columns": ["id"]
},
"seasons_slug_unique": {
"name": "seasons_slug_unique",
"nullsNotDistinct": false,
"columns": ["slug"]
},
"seasons_showPk_seasonNumber_unique": {
"name": "seasons_showPk_seasonNumber_unique",
"nullsNotDistinct": false,
"columns": ["show_pk", "season_number"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"kyoo.show_translations": {
"name": "show_translations",
"schema": "kyoo",
"columns": {
"pk": {
"name": "pk",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"language": {
"name": "language",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"tagline": {
"name": "tagline",
"type": "text",
"primaryKey": false,
"notNull": false
},
"aliases": {
"name": "aliases",
"type": "text[]",
"primaryKey": false,
"notNull": true
},
"tags": {
"name": "tags",
"type": "text[]",
"primaryKey": false,
"notNull": true
},
"poster": {
"name": "poster",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"thumbnail": {
"name": "thumbnail",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"banner": {
"name": "banner",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"logo": {
"name": "logo",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"trailer_url": {
"name": "trailer_url",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"name_trgm": {
"name": "name_trgm",
"columns": [
{
"expression": "\"name\" gin_trgm_ops",
"asc": true,
"isExpression": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "gin",
"with": {}
},
"tags": {
"name": "tags",
"columns": [
{
"expression": "tags",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"show_translations_pk_shows_pk_fk": {
"name": "show_translations_pk_shows_pk_fk",
"tableFrom": "show_translations",
"tableTo": "shows",
"schemaTo": "kyoo",
"columnsFrom": ["pk"],
"columnsTo": ["pk"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"show_translations_pk_language_pk": {
"name": "show_translations_pk_language_pk",
"columns": ["pk", "language"]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"kyoo.shows": {
"name": "shows",
"schema": "kyoo",
"columns": {
"pk": {
"name": "pk",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "shows_pk_seq",
"schema": "kyoo",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"kind": {
"name": "kind",
"type": "show_kind",
"typeSchema": "kyoo",
"primaryKey": false,
"notNull": true
},
"genres": {
"name": "genres",
"type": "genres[]",
"primaryKey": false,
"notNull": true
},
"rating": {
"name": "rating",
"type": "smallint",
"primaryKey": false,
"notNull": false
},
"runtime": {
"name": "runtime",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "show_status",
"typeSchema": "kyoo",
"primaryKey": false,
"notNull": true
},
"start_air": {
"name": "start_air",
"type": "date",
"primaryKey": false,
"notNull": false
},
"end_air": {
"name": "end_air",
"type": "date",
"primaryKey": false,
"notNull": false
},
"original_language": {
"name": "original_language",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false
},
"collection_pk": {
"name": "collection_pk",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"external_id": {
"name": "external_id",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
},
"next_refresh": {
"name": "next_refresh",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"kind": {
"name": "kind",
"columns": [
{
"expression": "kind",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "hash",
"with": {}
},
"rating": {
"name": "rating",
"columns": [
{
"expression": "rating",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"startAir": {
"name": "startAir",
"columns": [
{
"expression": "start_air",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"shows_collection_pk_shows_pk_fk": {
"name": "shows_collection_pk_shows_pk_fk",
"tableFrom": "shows",
"tableTo": "shows",
"schemaTo": "kyoo",
"columnsFrom": ["collection_pk"],
"columnsTo": ["pk"],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"shows_id_unique": {
"name": "shows_id_unique",
"nullsNotDistinct": false,
"columns": ["id"]
},
"shows_slug_unique": {
"name": "shows_slug_unique",
"nullsNotDistinct": false,
"columns": ["slug"]
}
},
"policies": {},
"checkConstraints": {
"rating_valid": {
"name": "rating_valid",
"value": "\"kyoo\".\"shows\".\"rating\" between 0 and 100"
},
"runtime_valid": {
"name": "runtime_valid",
"value": "\"kyoo\".\"shows\".\"runtime\" >= 0"
}
},
"isRLSEnabled": false
},
"kyoo.entry_video_join": {
"name": "entry_video_join",
"schema": "kyoo",
"columns": {
"entry": {
"name": "entry",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"video": {
"name": "video",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"entry_video_join_entry_entries_pk_fk": {
"name": "entry_video_join_entry_entries_pk_fk",
"tableFrom": "entry_video_join",
"tableTo": "entries",
"schemaTo": "kyoo",
"columnsFrom": ["entry"],
"columnsTo": ["pk"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"entry_video_join_video_videos_pk_fk": {
"name": "entry_video_join_video_videos_pk_fk",
"tableFrom": "entry_video_join",
"tableTo": "videos",
"schemaTo": "kyoo",
"columnsFrom": ["video"],
"columnsTo": ["pk"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"entry_video_join_entry_video_pk": {
"name": "entry_video_join_entry_video_pk",
"columns": ["entry", "video"]
}
},
"uniqueConstraints": {
"entry_video_join_slug_unique": {
"name": "entry_video_join_slug_unique",
"nullsNotDistinct": false,
"columns": ["slug"]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"kyoo.videos": {
"name": "videos",
"schema": "kyoo",
"columns": {
"pk": {
"name": "pk",
"type": "integer",
"primaryKey": true,
"notNull": true,
"identity": {
"type": "always",
"name": "videos_pk_seq",
"schema": "kyoo",
"increment": "1",
"startWith": "1",
"minValue": "1",
"maxValue": "2147483647",
"cache": "1",
"cycle": false
}
},
"id": {
"name": "id",
"type": "uuid",
"primaryKey": false,
"notNull": true,
"default": "gen_random_uuid()"
},
"path": {
"name": "path",
"type": "text",
"primaryKey": false,
"notNull": true
},
"rendering": {
"name": "rendering",
"type": "text",
"primaryKey": false,
"notNull": true
},
"part": {
"name": "part",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"version": {
"name": "version",
"type": "integer",
"primaryKey": false,
"notNull": true,
"default": 1
},
"guess": {
"name": "guess",
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{}'::jsonb"
},
"created_at": {
"name": "created_at",
"type": "timestamp with time zone",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"videos_id_unique": {
"name": "videos_id_unique",
"nullsNotDistinct": false,
"columns": ["id"]
},
"videos_path_unique": {
"name": "videos_path_unique",
"nullsNotDistinct": false,
"columns": ["path"]
}
},
"policies": {},
"checkConstraints": {
"part_pos": {
"name": "part_pos",
"value": "\"kyoo\".\"videos\".\"part\" >= 0"
},
"version_pos": {
"name": "version_pos",
"value": "\"kyoo\".\"videos\".\"version\" >= 0"
}
},
"isRLSEnabled": false
}
},
"enums": {
"kyoo.entry_type": {
"name": "entry_type",
"schema": "kyoo",
"values": ["unknown", "episode", "movie", "special", "extra"]
},
"kyoo.genres": {
"name": "genres",
"schema": "kyoo",
"values": [
"action",
"adventure",
"animation",
"comedy",
"crime",
"documentary",
"drama",
"family",
"fantasy",
"history",
"horror",
"music",
"mystery",
"romance",
"science-fiction",
"thriller",
"war",
"western",
"kids",
"reality",
"politics",
"soap",
"talk"
]
},
"kyoo.show_kind": {
"name": "show_kind",
"schema": "kyoo",
"values": ["serie", "movie", "collection"]
},
"kyoo.show_status": {
"name": "show_status",
"schema": "kyoo",
"values": ["unknown", "finished", "airing", "planned"]
}
},
"schemas": {
"kyoo": "kyoo"
},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@ -64,6 +64,13 @@
"when": 1738064522937, "when": 1738064522937,
"tag": "0008_entries", "tag": "0008_entries",
"breakpoints": true "breakpoints": true
},
{
"idx": 9,
"version": "7",
"when": 1740872363604,
"tag": "0009_collections",
"breakpoints": true
} }
] ]
} }

View File

@ -1,5 +1,6 @@
import { type SQL, and, eq, exists, sql } from "drizzle-orm"; import { type SQL, and, eq, exists, sql } from "drizzle-orm";
import { Elysia, t } from "elysia"; import { Elysia, t } from "elysia";
import { db } from "~/db";
import { entries, entryVideoJoin, showTranslations, shows } from "~/db/schema"; import { entries, entryVideoJoin, showTranslations, shows } from "~/db/schema";
import { getColumns, sqlarr } from "~/db/utils"; import { getColumns, sqlarr } from "~/db/utils";
import { KError } from "~/models/error"; import { KError } from "~/models/error";
@ -25,7 +26,6 @@ import {
sortToSql, sortToSql,
} from "~/models/utils"; } from "~/models/utils";
import { desc } from "~/models/utils/descriptions"; import { desc } from "~/models/utils/descriptions";
import { db } from "../db";
const movieFilters: FilterDef = { const movieFilters: FilterDef = {
genres: { genres: {

View File

@ -0,0 +1,69 @@
import { sql } from "drizzle-orm";
import { db } from "~/db";
import { showTranslations, shows } from "~/db/schema";
import { conflictUpdateAllExcept } from "~/db/utils";
import type { SeedCollection } from "~/models/collections";
import type { SeedMovie } from "~/models/movie";
import type { SeedSerie } from "~/models/serie";
import { processOptImage } from "../images";
type ShowTrans = typeof showTranslations.$inferInsert;
export const insertCollection = async (
collection: SeedCollection | undefined,
show: (({ kind: "movie" } & SeedMovie) | ({ kind: "serie" } & SeedSerie)) & {
nextRefresh: string;
},
) => {
if (!collection) return null;
const { translations, ...col } = collection;
return await db.transaction(async (tx) => {
const [ret] = await tx
.insert(shows)
.values({
kind: "collection",
status: "unknown",
startAir: show.kind === "movie" ? show.airDate : show.startAir,
endAir: show.kind === "movie" ? show.airDate : show.endAir,
nextRefresh: show.nextRefresh,
...col,
})
.onConflictDoUpdate({
target: shows.slug,
set: {
...conflictUpdateAllExcept(shows, [
"pk",
"id",
"slug",
"createdAt",
"startAir",
"endAir",
]),
startAir: sql`least(${shows.startAir}, excluded.start_air)`,
endAir: sql`greatest(${shows.endAir}, excluded.end_air)`,
},
})
.returning({ pk: shows.pk, id: shows.id, slug: shows.slug });
const trans: ShowTrans[] = Object.entries(translations).map(
([lang, tr]) => ({
pk: ret.pk,
language: lang,
...tr,
poster: processOptImage(tr.poster),
thumbnail: processOptImage(tr.thumbnail),
logo: processOptImage(tr.logo),
banner: processOptImage(tr.banner),
}),
);
await tx
.insert(showTranslations)
.values(trans)
.onConflictDoUpdate({
target: [showTranslations.pk, showTranslations.language],
set: conflictUpdateAllExcept(showTranslations, ["pk", "language"]),
});
return ret;
});
};

View File

@ -2,6 +2,7 @@ import { eq, sql } from "drizzle-orm";
import { db } from "~/db"; import { db } from "~/db";
import { showTranslations, shows } from "~/db/schema"; import { showTranslations, shows } from "~/db/schema";
import { conflictUpdateAllExcept } from "~/db/utils"; import { conflictUpdateAllExcept } from "~/db/utils";
import type { SeedCollection } from "~/models/collections";
import type { SeedMovie } from "~/models/movie"; import type { SeedMovie } from "~/models/movie";
import type { SeedSerie } from "~/models/serie"; import type { SeedSerie } from "~/models/serie";
import { getYear } from "~/utils"; import { getYear } from "~/utils";
@ -12,7 +13,10 @@ type ShowTrans = typeof showTranslations.$inferInsert;
export const insertShow = async ( export const insertShow = async (
show: Show, show: Show,
translations: SeedMovie["translations"] | SeedSerie["translations"], translations:
| SeedMovie["translations"]
| SeedSerie["translations"]
| SeedCollection["translations"],
) => { ) => {
return await db.transaction(async (tx) => { return await db.transaction(async (tx) => {
const ret = await insertBaseShow(tx, show); const ret = await insertBaseShow(tx, show);
@ -77,13 +81,14 @@ async function insertBaseShow(
// if at this point ret is still undefined, we could not reconciliate. // if at this point ret is still undefined, we could not reconciliate.
// simply bail and let the caller handle this. // simply bail and let the caller handle this.
const [{ id }] = await db const [{ pk, id }] = await db
.select({ id: shows.id }) .select({ pk: shows.pk, id: shows.id })
.from(shows) .from(shows)
.where(eq(shows.slug, show.slug)) .where(eq(shows.slug, show.slug))
.limit(1); .limit(1);
return { return {
status: 409 as const, status: 409 as const,
pk,
id, id,
slug: show.slug, slug: show.slug,
}; };

View File

@ -1,6 +1,7 @@
import { t } from "elysia"; import { t } from "elysia";
import type { SeedMovie } from "~/models/movie"; import type { SeedMovie } from "~/models/movie";
import { getYear } from "~/utils"; import { getYear } from "~/utils";
import { insertCollection } from "./insert/collection";
import { insertEntries } from "./insert/entries"; import { insertEntries } from "./insert/entries";
import { insertShow } from "./insert/shows"; import { insertShow } from "./insert/shows";
import { guessNextRefresh } from "./refresh"; import { guessNextRefresh } from "./refresh";
@ -11,6 +12,12 @@ export const SeedMovieResponse = t.Object({
videos: t.Array( videos: t.Array(
t.Object({ slug: t.String({ format: "slug", examples: ["bubble-v2"] }) }), t.Object({ slug: t.String({ format: "slug", examples: ["bubble-v2"] }) }),
), ),
collection: t.Nullable(
t.Object({
id: t.String({ format: "uuid" }),
slug: t.String({ format: "slug", examples: ["sawano-collection"] }),
}),
),
}); });
export type SeedMovieResponse = typeof SeedMovieResponse.static; export type SeedMovieResponse = typeof SeedMovieResponse.static;
@ -31,14 +38,21 @@ export const seedMovie = async (
seed.slug = `random-${getYear(seed.airDate)}`; seed.slug = `random-${getYear(seed.airDate)}`;
} }
const { translations, videos, ...bMovie } = seed; const { translations, videos, collection, ...bMovie } = seed;
const nextRefresh = guessNextRefresh(bMovie.airDate ?? new Date()); const nextRefresh = guessNextRefresh(bMovie.airDate ?? new Date());
const col = await insertCollection(collection, {
kind: "movie",
nextRefresh,
...seed,
});
const show = await insertShow( const show = await insertShow(
{ {
kind: "movie", kind: "movie",
startAir: bMovie.airDate, startAir: bMovie.airDate,
nextRefresh, nextRefresh,
collectionPk: col?.pk,
...bMovie, ...bMovie,
}, },
translations, translations,
@ -65,5 +79,6 @@ export const seedMovie = async (
id: show.id, id: show.id,
slug: show.slug, slug: show.slug,
videos: entry.videos, videos: entry.videos,
collection: col,
}; };
}; };

View File

@ -1,6 +1,7 @@
import { t } from "elysia"; import { t } from "elysia";
import type { SeedSerie } from "~/models/serie"; import type { SeedSerie } from "~/models/serie";
import { getYear } from "~/utils"; import { getYear } from "~/utils";
import { insertCollection } from "./insert/collection";
import { insertEntries } from "./insert/entries"; import { insertEntries } from "./insert/entries";
import { insertSeasons } from "./insert/seasons"; import { insertSeasons } from "./insert/seasons";
import { insertShow } from "./insert/shows"; import { insertShow } from "./insert/shows";
@ -35,6 +36,15 @@ export const SeedSerieResponse = t.Object({
slug: t.String({ format: "slug", examples: ["made-in-abyss-s1e1"] }), slug: t.String({ format: "slug", examples: ["made-in-abyss-s1e1"] }),
}), }),
), ),
collection: t.Nullable(
t.Object({
id: t.String({ format: "uuid" }),
slug: t.String({
format: "slug",
examples: ["made-in-abyss-collection"],
}),
}),
),
}); });
export type SeedSerieResponse = typeof SeedSerieResponse.static; export type SeedSerieResponse = typeof SeedSerieResponse.static;
@ -55,13 +65,20 @@ export const seedSerie = async (
seed.slug = `random-${getYear(seed.startAir)}`; seed.slug = `random-${getYear(seed.startAir)}`;
} }
const { translations, seasons, entries, extras, ...serie } = seed; const { translations, seasons, entries, extras, collection, ...serie } = seed;
const nextRefresh = guessNextRefresh(serie.startAir ?? new Date()); const nextRefresh = guessNextRefresh(serie.startAir ?? new Date());
const col = await insertCollection(collection, {
kind: "serie",
nextRefresh,
...seed,
});
const show = await insertShow( const show = await insertShow(
{ {
kind: "serie", kind: "serie",
nextRefresh, nextRefresh,
collectionPk: col?.pk,
...serie, ...serie,
}, },
translations, translations,
@ -82,5 +99,6 @@ export const seedSerie = async (
seasons: retSeasons, seasons: retSeasons,
entries: retEntries, entries: retEntries,
extras: retExtras, extras: retExtras,
collection: col,
}; };
}; };

View File

@ -1,5 +1,6 @@
import { relations, sql } from "drizzle-orm"; import { relations, sql } from "drizzle-orm";
import { import {
type AnyPgColumn,
check, check,
date, date,
index, index,
@ -16,7 +17,11 @@ import { entries } from "./entries";
import { seasons } from "./seasons"; import { seasons } from "./seasons";
import { image, language, schema } from "./utils"; import { image, language, schema } from "./utils";
export const showKind = schema.enum("show_kind", ["serie", "movie"]); export const showKind = schema.enum("show_kind", [
"serie",
"movie",
"collection",
]);
export const showStatus = schema.enum("show_status", [ export const showStatus = schema.enum("show_status", [
"unknown", "unknown",
"finished", "finished",
@ -78,6 +83,10 @@ export const shows = schema.table(
endAir: date(), endAir: date(),
originalLanguage: language(), originalLanguage: language(),
collectionPk: integer().references((): AnyPgColumn => shows.pk, {
onDelete: "set null",
}),
externalId: externalid(), externalId: externalid(),
createdAt: timestamp({ withTimezone: true, mode: "string" }) createdAt: timestamp({ withTimezone: true, mode: "string" })

View File

@ -0,0 +1,83 @@
import { t } from "elysia";
import type { Prettify } from "elysia/dist/types";
import { bubbleImages, duneCollection, registerExamples } from "./examples";
import {
ExternalId,
Genre,
Image,
Resource,
SeedImage,
TranslationRecord,
} from "./utils";
const BaseCollection = t.Object({
genres: t.Array(Genre),
rating: t.Nullable(t.Integer({ minimum: 0, maximum: 100 })),
startAir: t.Nullable(
t.String({
format: "date",
descrpition: "Date of the first item of the collection",
}),
),
endAir: t.Nullable(
t.String({
format: "date",
descrpition: "Date of the last item of the collection",
}),
),
createdAt: t.String({ format: "date-time" }),
nextRefresh: t.String({ format: "date-time" }),
externalId: ExternalId,
});
export const CollectionTranslation = t.Object({
name: t.String(),
description: t.Nullable(t.String()),
tagline: t.Nullable(t.String()),
aliases: t.Array(t.String()),
tags: t.Array(t.String()),
poster: t.Nullable(Image),
thumbnail: t.Nullable(Image),
banner: t.Nullable(Image),
logo: t.Nullable(Image),
});
export const Collection = t.Intersect([
Resource(),
CollectionTranslation,
BaseCollection,
]);
export type Collection = Prettify<typeof Collection.static>;
export const SeedCollection = t.Intersect([
t.Omit(BaseCollection, ["startAir", "endAir", "createdAt", "nextRefresh"]),
t.Object({
slug: t.String({ format: "slug" }),
translations: TranslationRecord(
t.Intersect([
t.Omit(CollectionTranslation, [
"poster",
"thumbnail",
"banner",
"logo",
]),
t.Object({
poster: t.Nullable(SeedImage),
thumbnail: t.Nullable(SeedImage),
banner: t.Nullable(SeedImage),
logo: t.Nullable(SeedImage),
}),
]),
),
}),
]);
export type SeedCollection = Prettify<typeof SeedCollection.static>;
registerExamples(Collection, {
...duneCollection,
...duneCollection.translations.en,
...bubbleImages,
});

View File

@ -1,5 +1,5 @@
import type { SeedMovie } from "../movie"; import type { SeedMovie } from "~/models/movie";
import type { Video } from "../video"; import type { Video } from "~/models/video";
export const duneVideo: Video = { export const duneVideo: Video = {
id: "c9a0d02e-6b8e-4ac1-b431-45b022ec0708", id: "c9a0d02e-6b8e-4ac1-b431-45b022ec0708",
@ -38,8 +38,8 @@ export const dune: SeedMovie = {
originalLanguage: "en", originalLanguage: "en",
externalId: { externalId: {
themoviedatabase: { themoviedatabase: {
dataId: "496243", dataId: "438631",
link: "https://www.themoviedb.org/movie/496243", link: "https://www.themoviedb.org/movie/438631-dune",
}, },
imdb: { imdb: {
dataId: "tt1160419", dataId: "tt1160419",

View File

@ -0,0 +1,29 @@
import type { SeedCollection } from "~/models/collections";
export const duneCollection: SeedCollection = {
slug: "dune-collection",
translations: {
en: {
name: " Dune Collection",
tagline: "A mythic and emotionally charged hero's journey.",
description:
"The saga of Paul Atreides and his rise to power on the deadly planet Arrakis.",
aliases: [],
tags: ["sci-fi", "adventure", "drama", "action", "epic"],
poster:
"https://image.tmdb.org/t/p/original/wD57HqZ6fXwwDdfQLo4hXLRwGV1.jpg",
thumbnail:
"https://image.tmdb.org/t/p/original/k2ocXnNkmvE6rJomRkExIStFq3v.jpg",
banner: null,
logo: "https://image.tmdb.org/t/p/original/5nDsd3u1c6kDphbtIqkHseLg7HL.png",
},
},
genres: ["adventure", "science-fiction"],
rating: 80,
externalId: {
themoviedatabase: {
dataId: "726871",
link: "https://www.themoviedb.org/collection/726871-dune-collection",
},
},
};

View File

@ -32,3 +32,7 @@ export const registerExamples = <T extends TSchema>(
export * from "./bubble"; export * from "./bubble";
export * from "./made-in-abyss"; export * from "./made-in-abyss";
export * from "./dune-1984";
export * from "./dune-2021";
export * from "./dune-collection";
export * from "./others";

View File

@ -1,5 +1,6 @@
import { t } from "elysia"; import { t } from "elysia";
import type { Prettify } from "~/utils"; import type { Prettify } from "~/utils";
import { SeedCollection } from "./collections";
import { bubble, registerExamples } from "./examples"; import { bubble, registerExamples } from "./examples";
import { bubbleImages } from "./examples/bubble"; import { bubbleImages } from "./examples/bubble";
import { import {
@ -85,6 +86,7 @@ export const SeedMovie = t.Intersect([
]), ]),
), ),
videos: t.Optional(t.Array(t.String({ format: "uuid" }))), videos: t.Optional(t.Array(t.String({ format: "uuid" }))),
collection: t.Optional(SeedCollection),
}), }),
]); ]);
export type SeedMovie = Prettify<typeof SeedMovie.static>; export type SeedMovie = Prettify<typeof SeedMovie.static>;

View File

@ -1,4 +1,6 @@
import { t } from "elysia"; import { t } from "elysia";
import type { Prettify } from "~/utils";
import { SeedCollection } from "./collections";
import { SeedEntry, SeedExtra } from "./entry"; import { SeedEntry, SeedExtra } from "./entry";
import { bubbleImages, madeInAbyss, registerExamples } from "./examples"; import { bubbleImages, madeInAbyss, registerExamples } from "./examples";
import { SeedSeason } from "./season"; import { SeedSeason } from "./season";
@ -57,7 +59,7 @@ export const SerieTranslation = t.Object({
export type SerieTranslation = typeof SerieTranslation.static; export type SerieTranslation = typeof SerieTranslation.static;
export const Serie = t.Intersect([Resource(), SerieTranslation, BaseSerie]); export const Serie = t.Intersect([Resource(), SerieTranslation, BaseSerie]);
export type Serie = typeof Serie.static; export type Serie = Prettify<typeof Serie.static>;
export const SeedSerie = t.Intersect([ export const SeedSerie = t.Intersect([
t.Omit(BaseSerie, ["createdAt", "nextRefresh"]), t.Omit(BaseSerie, ["createdAt", "nextRefresh"]),
@ -77,6 +79,7 @@ export const SeedSerie = t.Intersect([
seasons: t.Array(SeedSeason), seasons: t.Array(SeedSeason),
entries: t.Array(SeedEntry), entries: t.Array(SeedEntry),
extras: t.Optional(t.Array(SeedExtra)), extras: t.Optional(t.Array(SeedExtra)),
collection: t.Optional(SeedCollection),
}), }),
]); ]);
export type SeedSerie = typeof SeedSerie.static; export type SeedSerie = typeof SeedSerie.static;

View File

@ -0,0 +1,24 @@
import { beforeAll, describe, expect, it } from "bun:test";
import { createMovie } from "tests/helpers";
import { expectStatus } from "tests/utils";
import { db } from "~/db";
import { shows } from "~/db/schema";
import { dune } from "~/models/examples/dune-2021";
import { duneCollection } from "~/models/examples/dune-collection";
beforeAll(async () => {
await db.delete(shows);
});
describe("Collection seeding", () => {
it("Can create a movie with a collection", async () => {
const [resp, body] = await createMovie({
...dune,
collection: duneCollection,
});
expectStatus(resp, body).toBe(201);
expect(body.id).toBeString();
expect(body.slug).toBe("dune");
expect(body.collection.slug).toBe("dune-collection");
});
});