mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Add POST /series that handle seasons & entries (#797)
This commit is contained in:
commit
1b94d783d9
@ -100,9 +100,9 @@
|
||||
|
||||
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
|
||||
|
||||
"drizzle-kit": ["drizzle-kit@0.30.2", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-vhdLrxWA32WNVF77NabpSnX7pQBornx64VDQDmKddRonOB2Xe/yY4glQ7rECoa+ogqcQNo7VblLUbeBK6Zn9Ow=="],
|
||||
"drizzle-kit": ["drizzle-kit@0.30.3", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.19.7", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-kT8sgyC2hZrtOh5okhEBiwgx8jx+EjLUFoANFVVkBbxIjcb8XjaUorZ0rwCEUEd7THclI3ZARR64pmxloMW3Aw=="],
|
||||
|
||||
"drizzle-orm": ["drizzle-orm@0.38.4", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/react": ">=18", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "react": ">=18", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/react", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "knex", "kysely", "mysql2", "pg", "postgres", "react", "sql.js", "sqlite3"] }, "sha512-s7/5BpLKO+WJRHspvpqTydxFob8i1vo2rEx4pY6TGY7QSMuUfWUuzaY0DIpXCkgHOo37BaFC+SJQb99dDUXT3Q=="],
|
||||
"drizzle-orm": ["drizzle-orm@0.39.0", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/react": ">=18", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "react": ">=18", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/react", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "knex", "kysely", "mysql2", "pg", "postgres", "react", "sql.js", "sqlite3"] }, "sha512-kkZwo3Jvht0fdJD/EWGx0vYcEK0xnGrlNVaY07QYluRZA9N21B9VFbY+54bnb/1xvyzcg97tE65xprSAP/fFGQ=="],
|
||||
|
||||
"elysia": ["elysia@1.2.10", "", { "dependencies": { "@sinclair/typebox": "^0.34.13", "cookie": "^1.0.2", "memoirist": "^0.2.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-QcNl2FjhHFRpKaqy1NoMpyCjJ7OcKBnHwLUkqGu09QwIV84PFb82ILvYJG4GS1RbGv76OA50luaqBLrM3SLZ2w=="],
|
||||
|
||||
|
3
api/drizzle/0007_entries.sql
Normal file
3
api/drizzle/0007_entries.sql
Normal file
@ -0,0 +1,3 @@
|
||||
ALTER TABLE "kyoo"."entries" RENAME COLUMN "type" TO "kind";--> statement-breakpoint
|
||||
ALTER TABLE "kyoo"."entries" ALTER COLUMN "show_pk" SET NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "kyoo"."entries" ADD COLUMN "extra_kind" text;
|
12
api/drizzle/0008_entries.sql
Normal file
12
api/drizzle/0008_entries.sql
Normal file
@ -0,0 +1,12 @@
|
||||
ALTER TABLE "kyoo"."entry_video_jointure" RENAME TO "entry_video_join";--> statement-breakpoint
|
||||
ALTER TABLE "kyoo"."entries" RENAME COLUMN "thumbnails" TO "thumbnail";--> statement-breakpoint
|
||||
ALTER TABLE "kyoo"."entry_video_join" DROP CONSTRAINT "entry_video_jointure_slug_unique";--> statement-breakpoint
|
||||
ALTER TABLE "kyoo"."entry_video_join" DROP CONSTRAINT "entry_video_jointure_entry_entries_pk_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "kyoo"."entry_video_join" DROP CONSTRAINT "entry_video_jointure_video_videos_pk_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "kyoo"."entry_video_join" DROP CONSTRAINT "entry_video_jointure_entry_video_pk";--> statement-breakpoint
|
||||
ALTER TABLE "kyoo"."entry_video_join" ADD CONSTRAINT "entry_video_join_entry_video_pk" PRIMARY KEY("entry","video");--> statement-breakpoint
|
||||
ALTER TABLE "kyoo"."entry_video_join" ADD CONSTRAINT "entry_video_join_entry_entries_pk_fk" FOREIGN KEY ("entry") REFERENCES "kyoo"."entries"("pk") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "kyoo"."entry_video_join" ADD CONSTRAINT "entry_video_join_video_videos_pk_fk" FOREIGN KEY ("video") REFERENCES "kyoo"."videos"("pk") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "kyoo"."entry_video_join" ADD CONSTRAINT "entry_video_join_slug_unique" UNIQUE("slug");
|
952
api/drizzle/meta/0007_snapshot.json
Normal file
952
api/drizzle/meta/0007_snapshot.json
Normal file
@ -0,0 +1,952 @@
|
||||
{
|
||||
"id": "e70b1585-a927-4436-b2a0-d0ef216911f1",
|
||||
"prevId": "ca86d88f-b380-4b41-9c3d-d8acef369e4c",
|
||||
"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
|
||||
},
|
||||
"thumbnails": {
|
||||
"name": "thumbnails",
|
||||
"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
|
||||
},
|
||||
"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": {},
|
||||
"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_jointure": {
|
||||
"name": "entry_video_jointure",
|
||||
"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_jointure_entry_entries_pk_fk": {
|
||||
"name": "entry_video_jointure_entry_entries_pk_fk",
|
||||
"tableFrom": "entry_video_jointure",
|
||||
"tableTo": "entries",
|
||||
"schemaTo": "kyoo",
|
||||
"columnsFrom": ["entry"],
|
||||
"columnsTo": ["pk"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"entry_video_jointure_video_videos_pk_fk": {
|
||||
"name": "entry_video_jointure_video_videos_pk_fk",
|
||||
"tableFrom": "entry_video_jointure",
|
||||
"tableTo": "videos",
|
||||
"schemaTo": "kyoo",
|
||||
"columnsFrom": ["video"],
|
||||
"columnsTo": ["pk"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"entry_video_jointure_entry_video_pk": {
|
||||
"name": "entry_video_jointure_entry_video_pk",
|
||||
"columns": ["entry", "video"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"entry_video_jointure_slug_unique": {
|
||||
"name": "entry_video_jointure_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"]
|
||||
},
|
||||
"kyoo.show_status": {
|
||||
"name": "show_status",
|
||||
"schema": "kyoo",
|
||||
"values": ["unknown", "finished", "airing", "planned"]
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"kyoo": "kyoo"
|
||||
},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
952
api/drizzle/meta/0008_snapshot.json
Normal file
952
api/drizzle/meta/0008_snapshot.json
Normal file
@ -0,0 +1,952 @@
|
||||
{
|
||||
"id": "5c17dd71-409a-4c80-870d-f12386676738",
|
||||
"prevId": "e70b1585-a927-4436-b2a0-d0ef216911f1",
|
||||
"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
|
||||
},
|
||||
"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": {},
|
||||
"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"]
|
||||
},
|
||||
"kyoo.show_status": {
|
||||
"name": "show_status",
|
||||
"schema": "kyoo",
|
||||
"values": ["unknown", "finished", "airing", "planned"]
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"kyoo": "kyoo"
|
||||
},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
@ -50,6 +50,20 @@
|
||||
"when": 1737763164759,
|
||||
"tag": "0006_seasons",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 7,
|
||||
"version": "7",
|
||||
"when": 1737913931275,
|
||||
"tag": "0007_entries",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 8,
|
||||
"version": "7",
|
||||
"when": 1738064522937,
|
||||
"tag": "0008_entries",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -10,8 +10,8 @@
|
||||
"dependencies": {
|
||||
"@elysiajs/jwt": "^1.2.0",
|
||||
"@elysiajs/swagger": "zoriya/elysia-swagger#build",
|
||||
"drizzle-kit": "^0.30.2",
|
||||
"drizzle-orm": "^0.38.4",
|
||||
"drizzle-kit": "^0.30.3",
|
||||
"drizzle-orm": "^0.39.0",
|
||||
"elysia": "^1.2.10",
|
||||
"parjs": "^1.3.9",
|
||||
"pg": "^8.13.1"
|
||||
|
@ -1,11 +1,6 @@
|
||||
import { type SQL, and, eq, exists, sql } from "drizzle-orm";
|
||||
import { Elysia, t } from "elysia";
|
||||
import {
|
||||
entries,
|
||||
entryVideoJointure as entryVideoJoint,
|
||||
showTranslations,
|
||||
shows,
|
||||
} from "~/db/schema";
|
||||
import { entries, entryVideoJoin, showTranslations, shows } from "~/db/schema";
|
||||
import { getColumns, sqlarr } from "~/db/utils";
|
||||
import { KError } from "~/models/error";
|
||||
import { bubble } from "~/models/examples";
|
||||
@ -86,8 +81,8 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
|
||||
exists(
|
||||
db
|
||||
.select()
|
||||
.from(entryVideoJoint)
|
||||
.where(eq(entries.pk, entryVideoJoint.entry)),
|
||||
.from(entryVideoJoin)
|
||||
.where(eq(entries.pk, entryVideoJoin.entry)),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -263,8 +258,8 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
|
||||
exists(
|
||||
db
|
||||
.select()
|
||||
.from(entryVideoJoint)
|
||||
.where(eq(entries.pk, entryVideoJoint.entry)),
|
||||
.from(entryVideoJoin)
|
||||
.where(eq(entries.pk, entryVideoJoin.entry)),
|
||||
),
|
||||
)
|
||||
.as("video");
|
||||
|
@ -1,6 +1,9 @@
|
||||
import type { Image } from "~/models/utils";
|
||||
|
||||
export const processImage = async (url: string): Promise<Image> => {
|
||||
// this will only push a task to the image downloader service and not download it instantly.
|
||||
// this is both done to prevent to many requests to be sent at once and to make sure POST
|
||||
// requests are not blocked by image downloading or blurhash calculation
|
||||
export const processImage = (url: string): Image => {
|
||||
const hasher = new Bun.CryptoHasher("sha256");
|
||||
hasher.update(url);
|
||||
|
||||
@ -13,7 +16,7 @@ export const processImage = async (url: string): Promise<Image> => {
|
||||
};
|
||||
};
|
||||
|
||||
export const processOptImage = (url: string | null): Promise<Image | null> => {
|
||||
if (!url) return Promise.resolve(null);
|
||||
export const processOptImage = (url: string | null): Image | null => {
|
||||
if (!url) return null;
|
||||
return processImage(url);
|
||||
};
|
||||
|
@ -1,29 +1,37 @@
|
||||
import { Value } from "@sinclair/typebox/value";
|
||||
import Elysia from "elysia";
|
||||
import { KError } from "~/models/error";
|
||||
import { Movie, SeedMovie } from "~/models/movie";
|
||||
import { SeedMovie } from "~/models/movie";
|
||||
import { SeedSerie } from "~/models/serie";
|
||||
import { Resource } from "~/models/utils";
|
||||
import { comment } from "~/utils";
|
||||
import { SeedMovieResponse, seedMovie } from "./movies";
|
||||
import { SeedSerieResponse, seedSerie } from "./series";
|
||||
|
||||
export const seed = new Elysia()
|
||||
.model({
|
||||
movie: Movie,
|
||||
"seed-movie": SeedMovie,
|
||||
"seed-movie-response": SeedMovieResponse,
|
||||
"seed-serie": SeedSerie,
|
||||
"seed-serie-response": SeedSerieResponse,
|
||||
})
|
||||
.post(
|
||||
"/movies",
|
||||
async ({ body, error }) => {
|
||||
// needed due to https://github.com/elysiajs/elysia/issues/671
|
||||
body = Value.Decode(SeedMovie, body);
|
||||
const movie = Value.Decode(SeedMovie, body) as SeedMovie;
|
||||
|
||||
const ret = await seedMovie(body);
|
||||
if (ret.status === 422) return error(422, ret);
|
||||
return error(ret.status, ret);
|
||||
const ret = await seedMovie(movie);
|
||||
if ("status" in ret) return error(ret.status, ret as any);
|
||||
return error(ret.updated ? 200 : 201, ret);
|
||||
},
|
||||
{
|
||||
body: "seed-movie",
|
||||
detail: {
|
||||
tags: ["movies"],
|
||||
description:
|
||||
"Create a movie & all related metadata. Can also link videos.",
|
||||
},
|
||||
body: SeedMovie,
|
||||
response: {
|
||||
200: {
|
||||
...SeedMovieResponse,
|
||||
@ -31,18 +39,47 @@ export const seed = new Elysia()
|
||||
},
|
||||
201: { ...SeedMovieResponse, description: "Created a new movie." },
|
||||
409: {
|
||||
...Resource,
|
||||
...Resource(),
|
||||
description: comment`
|
||||
A movie with the same slug but a different air date already exists.
|
||||
Change the slug and re-run the request.
|
||||
`,
|
||||
},
|
||||
422: { ...KError, description: "Invalid schema in body." },
|
||||
422: KError,
|
||||
},
|
||||
},
|
||||
)
|
||||
.post(
|
||||
"/series",
|
||||
async ({ body, error }) => {
|
||||
// needed due to https://github.com/elysiajs/elysia/issues/671
|
||||
const serie = Value.Decode(SeedSerie, body) as SeedSerie;
|
||||
|
||||
const ret = await seedSerie(serie);
|
||||
if ("status" in ret) return error(ret.status, ret as any);
|
||||
return error(ret.updated ? 200 : 201, ret);
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
tags: ["movies"],
|
||||
tags: ["series"],
|
||||
description:
|
||||
"Create a movie & all related metadata. Can also link videos.",
|
||||
"Create a series & all related metadata. Can also link videos.",
|
||||
},
|
||||
body: SeedSerie,
|
||||
response: {
|
||||
200: {
|
||||
...SeedSerieResponse,
|
||||
description: "Existing serie edited/updated.",
|
||||
},
|
||||
201: { ...SeedSerieResponse, description: "Created a new serie." },
|
||||
409: {
|
||||
...Resource(),
|
||||
description: comment`
|
||||
A serie with the same slug but a different air date already exists.
|
||||
Change the slug and re-run the request.
|
||||
`,
|
||||
},
|
||||
422: KError,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
171
api/src/controllers/seed/insert/entries.ts
Normal file
171
api/src/controllers/seed/insert/entries.ts
Normal file
@ -0,0 +1,171 @@
|
||||
import { type Column, type SQL, eq, sql } from "drizzle-orm";
|
||||
import { db } from "~/db";
|
||||
import {
|
||||
entries,
|
||||
entryTranslations,
|
||||
entryVideoJoin,
|
||||
videos,
|
||||
} from "~/db/schema";
|
||||
import { conflictUpdateAllExcept, values } from "~/db/utils";
|
||||
import type { SeedEntry as SEntry, SeedExtra as SExtra } from "~/models/entry";
|
||||
import { processOptImage } from "../images";
|
||||
import { guessNextRefresh } from "../refresh";
|
||||
|
||||
type SeedEntry = SEntry & {
|
||||
video?: undefined;
|
||||
};
|
||||
type SeedExtra = Omit<SExtra, "kind"> & {
|
||||
videos?: undefined;
|
||||
translations?: undefined;
|
||||
kind: "extra";
|
||||
extraKind: SExtra["kind"];
|
||||
};
|
||||
|
||||
type EntryI = typeof entries.$inferInsert;
|
||||
|
||||
const generateSlug = (
|
||||
showSlug: string,
|
||||
entry: SeedEntry | SeedExtra,
|
||||
): string => {
|
||||
switch (entry.kind) {
|
||||
case "episode":
|
||||
return `${showSlug}-s${entry.seasonNumber}e${entry.episodeNumber}`;
|
||||
case "special":
|
||||
return `${showSlug}-sp${entry.number}`;
|
||||
case "movie":
|
||||
if (entry.slug) return entry.slug;
|
||||
return entry.order === 1 ? showSlug : `${showSlug}-${entry.order}`;
|
||||
case "extra":
|
||||
return entry.slug;
|
||||
}
|
||||
};
|
||||
|
||||
export const insertEntries = async (
|
||||
show: { pk: number; slug: string },
|
||||
items: (SeedEntry | SeedExtra)[],
|
||||
) => {
|
||||
if (!items) return [];
|
||||
|
||||
const retEntries = await db.transaction(async (tx) => {
|
||||
const vals: EntryI[] = items.map((seed) => {
|
||||
const { translations, videos, video, ...entry } = seed;
|
||||
return {
|
||||
...entry,
|
||||
showPk: show.pk,
|
||||
slug: generateSlug(show.slug, seed),
|
||||
thumbnail: processOptImage(seed.thumbnail),
|
||||
nextRefresh:
|
||||
entry.kind !== "extra"
|
||||
? guessNextRefresh(entry.airDate ?? new Date())
|
||||
: guessNextRefresh(new Date()),
|
||||
episodeNumber:
|
||||
entry.kind === "episode"
|
||||
? entry.episodeNumber
|
||||
: entry.kind === "special"
|
||||
? entry.number
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
const ret = await tx
|
||||
.insert(entries)
|
||||
.values(vals)
|
||||
.onConflictDoUpdate({
|
||||
target: entries.slug,
|
||||
set: conflictUpdateAllExcept(entries, [
|
||||
"pk",
|
||||
"showPk",
|
||||
"id",
|
||||
"slug",
|
||||
"createdAt",
|
||||
]),
|
||||
})
|
||||
.returning({ pk: entries.pk, id: entries.id, slug: entries.slug });
|
||||
|
||||
const trans = items.flatMap((seed, i) => {
|
||||
if (seed.kind === "extra") {
|
||||
return {
|
||||
pk: ret[i].pk,
|
||||
// yeah we hardcode the language to extra because if we want to support
|
||||
// translations one day it won't be awkward
|
||||
language: "extra",
|
||||
name: seed.name,
|
||||
description: null,
|
||||
};
|
||||
}
|
||||
|
||||
return Object.entries(seed.translations).map(([lang, tr]) => ({
|
||||
// assumes ret is ordered like items.
|
||||
pk: ret[i].pk,
|
||||
language: lang,
|
||||
...tr,
|
||||
}));
|
||||
});
|
||||
await tx
|
||||
.insert(entryTranslations)
|
||||
.values(trans)
|
||||
.onConflictDoUpdate({
|
||||
target: [entryTranslations.pk, entryTranslations.language],
|
||||
set: conflictUpdateAllExcept(entryTranslations, ["pk", "language"]),
|
||||
});
|
||||
|
||||
return ret;
|
||||
});
|
||||
|
||||
const vids = items.flatMap((seed, i) => {
|
||||
if (seed.kind === "extra") {
|
||||
return {
|
||||
videoId: seed.video,
|
||||
entryPk: retEntries[i].pk,
|
||||
needRendering: false,
|
||||
};
|
||||
}
|
||||
if (!seed.videos) return [];
|
||||
return seed.videos.map((x, j) => ({
|
||||
videoId: x,
|
||||
entryPk: retEntries[i].pk,
|
||||
// The first video should not have a rendering.
|
||||
needRendering: j && seed.videos!.length > 1,
|
||||
}));
|
||||
});
|
||||
|
||||
if (vids.length === 0)
|
||||
return retEntries.map((x) => ({ id: x.id, slug: x.slug, videos: [] }));
|
||||
|
||||
const retVideos = await db
|
||||
.insert(entryVideoJoin)
|
||||
.select(
|
||||
db
|
||||
.select({
|
||||
entry: sql<number>`vids.entryPk::integer`.as("entry"),
|
||||
video: sql`${videos.pk}`.as("video"),
|
||||
slug: computeVideoSlug(
|
||||
sql`${show.slug}::text`,
|
||||
sql`vids.needRendering::boolean`,
|
||||
),
|
||||
})
|
||||
.from(values(vids).as("vids"))
|
||||
.innerJoin(videos, eq(videos.id, sql`vids.videoId::uuid`)),
|
||||
)
|
||||
.onConflictDoNothing()
|
||||
.returning({
|
||||
slug: entryVideoJoin.slug,
|
||||
entryPk: entryVideoJoin.entry,
|
||||
});
|
||||
|
||||
return retEntries.map((entry) => ({
|
||||
id: entry.id,
|
||||
slug: entry.slug,
|
||||
videos: retVideos.filter((x) => x.entryPk === entry.pk),
|
||||
}));
|
||||
};
|
||||
|
||||
export function computeVideoSlug(showSlug: SQL | Column, needsRendering: SQL) {
|
||||
return sql<string>`
|
||||
concat(
|
||||
${showSlug},
|
||||
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
|
||||
)
|
||||
`.as("slug");
|
||||
}
|
61
api/src/controllers/seed/insert/seasons.ts
Normal file
61
api/src/controllers/seed/insert/seasons.ts
Normal file
@ -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;
|
||||
});
|
||||
};
|
90
api/src/controllers/seed/insert/shows.ts
Normal file
90
api/src/controllers/seed/insert/shows.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { eq, sql } from "drizzle-orm";
|
||||
import { db } from "~/db";
|
||||
import { showTranslations, shows } from "~/db/schema";
|
||||
import { conflictUpdateAllExcept } from "~/db/utils";
|
||||
import type { SeedMovie } from "~/models/movie";
|
||||
import type { SeedSerie } from "~/models/serie";
|
||||
import { getYear } from "~/utils";
|
||||
import { processOptImage } from "../images";
|
||||
|
||||
type Show = typeof shows.$inferInsert;
|
||||
type ShowTrans = typeof showTranslations.$inferInsert;
|
||||
|
||||
export const insertShow = async (
|
||||
show: Show,
|
||||
translations: SeedMovie["translations"] | SeedSerie["translations"],
|
||||
) => {
|
||||
return await db.transaction(async (tx) => {
|
||||
const ret = await insertBaseShow(tx, show);
|
||||
if ("status" in ret) return ret;
|
||||
|
||||
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;
|
||||
});
|
||||
};
|
||||
|
||||
async function insertBaseShow(
|
||||
tx: Parameters<Parameters<typeof db.transaction>[0]>[0],
|
||||
show: Show,
|
||||
) {
|
||||
function insert() {
|
||||
return tx
|
||||
.insert(shows)
|
||||
.values(show)
|
||||
.onConflictDoUpdate({
|
||||
target: shows.slug,
|
||||
set: conflictUpdateAllExcept(shows, ["pk", "id", "slug", "createdAt"]),
|
||||
// if year is different, this is not an update but a conflict (ex: dune-1984 vs dune-2021)
|
||||
setWhere: sql`date_part('year', ${shows.startAir}) = date_part('year', excluded."start_air")`,
|
||||
})
|
||||
.returning({
|
||||
pk: shows.pk,
|
||||
id: shows.id,
|
||||
slug: shows.slug,
|
||||
// https://stackoverflow.com/questions/39058213/differentiate-inserted-and-updated-rows-in-upsert-using-system-columns/39204667#39204667
|
||||
updated: sql<boolean>`(xmax <> 0)`.as("updated"),
|
||||
});
|
||||
}
|
||||
|
||||
let [ret] = await insert();
|
||||
if (ret) return ret;
|
||||
|
||||
// ret is undefined when the conflict's where return false (meaning we have
|
||||
// a conflicting slug but a different air year.
|
||||
// try to insert adding the year at the end of the slug.
|
||||
if (show.startAir && !show.slug.endsWith(`${getYear(show.startAir)}`)) {
|
||||
show.slug = `${show.slug}-${getYear(show.startAir)}`;
|
||||
[ret] = await insert();
|
||||
if (ret) return ret;
|
||||
}
|
||||
|
||||
// if at this point ret is still undefined, we could not reconciliate.
|
||||
// simply bail and let the caller handle this.
|
||||
const [{ id }] = await db
|
||||
.select({ id: shows.id })
|
||||
.from(shows)
|
||||
.where(eq(shows.slug, show.slug))
|
||||
.limit(1);
|
||||
return {
|
||||
status: 409 as const,
|
||||
id,
|
||||
slug: show.slug,
|
||||
};
|
||||
}
|
@ -1,23 +1,10 @@
|
||||
import { eq, inArray, sql } from "drizzle-orm";
|
||||
import { t } from "elysia";
|
||||
import { db } from "~/db";
|
||||
import {
|
||||
entries,
|
||||
entryTranslations,
|
||||
entryVideoJointure as evj,
|
||||
showTranslations,
|
||||
shows,
|
||||
videos,
|
||||
} from "~/db/schema";
|
||||
import { conflictUpdateAllExcept } from "~/db/utils";
|
||||
import type { SeedMovie } from "~/models/movie";
|
||||
import { processOptImage } from "./images";
|
||||
import { getYear } from "~/utils";
|
||||
import { insertEntries } from "./insert/entries";
|
||||
import { insertShow } from "./insert/shows";
|
||||
import { guessNextRefresh } from "./refresh";
|
||||
|
||||
type Show = typeof shows.$inferInsert;
|
||||
type ShowTrans = typeof showTranslations.$inferInsert;
|
||||
type Entry = typeof entries.$inferInsert;
|
||||
|
||||
export const SeedMovieResponse = t.Object({
|
||||
id: t.String({ format: "uuid" }),
|
||||
slug: t.String({ format: "slug", examples: ["bubble"] }),
|
||||
@ -30,7 +17,8 @@ export type SeedMovieResponse = typeof SeedMovieResponse.static;
|
||||
export const seedMovie = async (
|
||||
seed: SeedMovie,
|
||||
): Promise<
|
||||
| (SeedMovieResponse & { status: "Created" | "OK" | "Conflict" })
|
||||
| (SeedMovieResponse & { updated: boolean })
|
||||
| { status: 409; id: string; slug: string }
|
||||
| { status: 422; message: string }
|
||||
> => {
|
||||
if (seed.slug === "random") {
|
||||
@ -43,152 +31,39 @@ export const seedMovie = async (
|
||||
seed.slug = `random-${getYear(seed.airDate)}`;
|
||||
}
|
||||
|
||||
const { translations, videos: vids, ...bMovie } = seed;
|
||||
const { translations, videos, ...bMovie } = seed;
|
||||
const nextRefresh = guessNextRefresh(bMovie.airDate ?? new Date());
|
||||
|
||||
const ret = await db.transaction(async (tx) => {
|
||||
const movie: Show = {
|
||||
const show = await insertShow(
|
||||
{
|
||||
kind: "movie",
|
||||
startAir: bMovie.airDate,
|
||||
nextRefresh: guessNextRefresh(bMovie.airDate ?? new Date()),
|
||||
nextRefresh,
|
||||
...bMovie,
|
||||
};
|
||||
},
|
||||
translations,
|
||||
);
|
||||
if ("status" in show) return show;
|
||||
|
||||
const insert = () =>
|
||||
tx
|
||||
.insert(shows)
|
||||
.values(movie)
|
||||
.onConflictDoUpdate({
|
||||
target: shows.slug,
|
||||
set: conflictUpdateAllExcept(shows, [
|
||||
"pk",
|
||||
"id",
|
||||
"slug",
|
||||
"createdAt",
|
||||
]),
|
||||
// if year is different, this is not an update but a conflict (ex: dune-1984 vs dune-2021)
|
||||
setWhere: sql`date_part('year', ${shows.startAir}) = date_part('year', excluded."start_air")`,
|
||||
})
|
||||
.returning({
|
||||
pk: shows.pk,
|
||||
id: shows.id,
|
||||
slug: shows.slug,
|
||||
// https://stackoverflow.com/questions/39058213/differentiate-inserted-and-updated-rows-in-upsert-using-system-columns/39204667#39204667
|
||||
updated: sql<boolean>`(xmax <> 0)`.as("updated"),
|
||||
});
|
||||
let [ret] = await insert();
|
||||
if (!ret) {
|
||||
// ret is undefined when the conflict's where return false (meaning we have
|
||||
// a conflicting slug but a different air year.
|
||||
// try to insert adding the year at the end of the slug.
|
||||
if (
|
||||
movie.startAir &&
|
||||
!movie.slug.endsWith(`${getYear(movie.startAir)}`)
|
||||
) {
|
||||
movie.slug = `${movie.slug}-${getYear(movie.startAir)}`;
|
||||
[ret] = await insert();
|
||||
}
|
||||
|
||||
// if at this point ret is still undefined, we could not reconciliate.
|
||||
// simply bail and let the caller handle this.
|
||||
if (!ret) {
|
||||
const [{ id }] = await db
|
||||
.select({ id: shows.id })
|
||||
.from(shows)
|
||||
.where(eq(shows.slug, movie.slug))
|
||||
.limit(1);
|
||||
return {
|
||||
status: "Conflict" as const,
|
||||
id,
|
||||
slug: movie.slug,
|
||||
videos: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// even if never shown to the user, a movie still has an entry.
|
||||
const movieEntry: Entry = { type: "movie", ...bMovie };
|
||||
const [entry] = await tx
|
||||
.insert(entries)
|
||||
.values(movieEntry)
|
||||
.onConflictDoUpdate({
|
||||
target: entries.slug,
|
||||
set: conflictUpdateAllExcept(entries, [
|
||||
"pk",
|
||||
"id",
|
||||
"slug",
|
||||
"createdAt",
|
||||
]),
|
||||
})
|
||||
.returning({ pk: entries.pk });
|
||||
|
||||
const trans: ShowTrans[] = await Promise.all(
|
||||
Object.entries(translations).map(async ([lang, tr]) => ({
|
||||
pk: ret.pk,
|
||||
// TODO: normalize lang or error if invalid
|
||||
language: lang,
|
||||
...tr,
|
||||
poster: await processOptImage(tr.poster),
|
||||
thumbnail: await processOptImage(tr.thumbnail),
|
||||
logo: await processOptImage(tr.logo),
|
||||
banner: await processOptImage(tr.banner),
|
||||
})),
|
||||
);
|
||||
await tx
|
||||
.insert(showTranslations)
|
||||
.values(trans)
|
||||
.onConflictDoUpdate({
|
||||
target: [showTranslations.pk, showTranslations.language],
|
||||
set: conflictUpdateAllExcept(showTranslations, ["pk", "language"]),
|
||||
});
|
||||
|
||||
const entryTrans = trans.map((x) => ({ ...x, pk: entry.pk }));
|
||||
await tx
|
||||
.insert(entryTranslations)
|
||||
.values(entryTrans)
|
||||
.onConflictDoUpdate({
|
||||
target: [entryTranslations.pk, entryTranslations.language],
|
||||
set: conflictUpdateAllExcept(entryTranslations, ["pk", "language"]),
|
||||
});
|
||||
|
||||
return { ...ret, entry: entry.pk };
|
||||
});
|
||||
|
||||
if (ret.status === "Conflict") return ret;
|
||||
|
||||
let retVideos: { slug: string }[] = [];
|
||||
if (vids) {
|
||||
retVideos = await db
|
||||
.insert(evj)
|
||||
.select(
|
||||
db
|
||||
.select({
|
||||
entry: sql<number>`${ret.entry}`.as("entry"),
|
||||
video: videos.pk,
|
||||
// TODO: do not add rendering if all videos of the entry have the same rendering
|
||||
slug: sql<string>`
|
||||
concat(
|
||||
${ret.slug}::text,
|
||||
case when ${videos.part} <> null then concat('-p', ${videos.part}) else '' end,
|
||||
case when ${videos.version} <> 1 then concat('-v', ${videos.version}) else '' end
|
||||
)
|
||||
`.as("slug"),
|
||||
// case when (select count(1) from ${evj} where ${evj.entry} = ${ret.entry}) <> 0 then concat('-', ${videos.rendering}) else '' end
|
||||
})
|
||||
.from(videos)
|
||||
.where(inArray(videos.id, vids)),
|
||||
)
|
||||
.onConflictDoNothing()
|
||||
.returning({ slug: evj.slug });
|
||||
}
|
||||
// even if never shown to the user, a movie still has an entry.
|
||||
const [entry] = await insertEntries(show, [
|
||||
{
|
||||
...bMovie,
|
||||
kind: "movie",
|
||||
order: 1,
|
||||
thumbnail: (bMovie.originalLanguage
|
||||
? translations[bMovie.originalLanguage]
|
||||
: Object.values(translations)[0]
|
||||
)?.thumbnail,
|
||||
translations,
|
||||
videos,
|
||||
},
|
||||
]);
|
||||
|
||||
return {
|
||||
status: ret.updated ? "OK" : "Created",
|
||||
id: ret.id,
|
||||
slug: ret.slug,
|
||||
videos: retVideos,
|
||||
updated: show.updated,
|
||||
id: show.id,
|
||||
slug: show.slug,
|
||||
videos: entry.videos,
|
||||
};
|
||||
};
|
||||
|
||||
function getYear(date: string) {
|
||||
return new Date(date).getUTCFullYear();
|
||||
}
|
||||
|
86
api/src/controllers/seed/series.ts
Normal file
86
api/src/controllers/seed/series.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { t } from "elysia";
|
||||
import type { SeedSerie } from "~/models/serie";
|
||||
import { getYear } from "~/utils";
|
||||
import { insertEntries } from "./insert/entries";
|
||||
import { insertSeasons } from "./insert/seasons";
|
||||
import { insertShow } from "./insert/shows";
|
||||
import { guessNextRefresh } from "./refresh";
|
||||
|
||||
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" }),
|
||||
slug: t.String({ format: "slug", examples: ["made-in-abyss-s1e1"] }),
|
||||
videos: t.Array(
|
||||
t.Object({
|
||||
slug: t.String({
|
||||
format: "slug",
|
||||
examples: ["mode-in-abyss-s1e1v2"],
|
||||
}),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
extras: t.Array(
|
||||
t.Object({
|
||||
id: t.String({ format: "uuid" }),
|
||||
slug: t.String({ format: "slug", examples: ["made-in-abyss-s1e1"] }),
|
||||
}),
|
||||
),
|
||||
});
|
||||
export type SeedSerieResponse = typeof SeedSerieResponse.static;
|
||||
|
||||
export const seedSerie = async (
|
||||
seed: SeedSerie,
|
||||
): Promise<
|
||||
| (SeedSerieResponse & { updated: boolean })
|
||||
| { status: 409; id: string; slug: string }
|
||||
| { status: 422; message: string }
|
||||
> => {
|
||||
if (seed.slug === "random") {
|
||||
if (!seed.startAir) {
|
||||
return {
|
||||
status: 422,
|
||||
message: "`random` is a reserved slug. Use something else.",
|
||||
};
|
||||
}
|
||||
seed.slug = `random-${getYear(seed.startAir)}`;
|
||||
}
|
||||
|
||||
const { translations, seasons, entries, extras, ...serie } = seed;
|
||||
const nextRefresh = guessNextRefresh(serie.startAir ?? new Date());
|
||||
|
||||
const show = await insertShow(
|
||||
{
|
||||
kind: "serie",
|
||||
nextRefresh,
|
||||
...serie,
|
||||
},
|
||||
translations,
|
||||
);
|
||||
if ("status" in show) return show;
|
||||
|
||||
const retSeasons = await insertSeasons(show, seasons);
|
||||
const retEntries = await insertEntries(show, entries);
|
||||
const retExtras = await insertEntries(
|
||||
show,
|
||||
(extras ?? []).map((x) => ({ ...x, kind: "extra", extraKind: x.kind })),
|
||||
);
|
||||
|
||||
return {
|
||||
updated: show.updated,
|
||||
id: show.id,
|
||||
slug: show.slug,
|
||||
seasons: retSeasons,
|
||||
entries: retEntries,
|
||||
extras: retExtras,
|
||||
};
|
||||
};
|
@ -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),
|
||||
@ -20,21 +27,104 @@ export const videos = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
response: { 200: "video" },
|
||||
})
|
||||
.post(
|
||||
"/",
|
||||
async ({ body }) => {
|
||||
return await db
|
||||
.insert(videosT)
|
||||
"",
|
||||
async ({ body, error }) => {
|
||||
const oldRet = await db
|
||||
.insert(videos)
|
||||
.values(body)
|
||||
.onConflictDoNothing()
|
||||
.returning({ id: videosT.id, path: videosT.path });
|
||||
.returning({
|
||||
pk: videos.pk,
|
||||
id: videos.id,
|
||||
path: videos.path,
|
||||
guess: videos.guess,
|
||||
});
|
||||
return error(201, oldRet);
|
||||
|
||||
// TODO: this is a huge untested wip
|
||||
// biome-ignore lint/correctness/noUnreachable: leave me alone
|
||||
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.
|
||||
`,
|
||||
},
|
||||
},
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { relations, sql } from "drizzle-orm";
|
||||
import {
|
||||
check,
|
||||
date,
|
||||
@ -14,6 +14,7 @@ import {
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { shows } from "./shows";
|
||||
import { image, language, schema } from "./utils";
|
||||
import { entryVideoJoin } from "./videos";
|
||||
|
||||
export const entryType = schema.enum("entry_type", [
|
||||
"unknown",
|
||||
@ -51,14 +52,18 @@ export const entries = schema.table(
|
||||
pk: integer().primaryKey().generatedAlwaysAsIdentity(),
|
||||
id: uuid().notNull().unique().defaultRandom(),
|
||||
slug: varchar({ length: 255 }).notNull().unique(),
|
||||
showPk: integer().references(() => shows.pk, { onDelete: "cascade" }),
|
||||
showPk: integer()
|
||||
.notNull()
|
||||
.references(() => shows.pk, { onDelete: "cascade" }),
|
||||
order: real(),
|
||||
seasonNumber: integer(),
|
||||
episodeNumber: integer(),
|
||||
type: entryType().notNull(),
|
||||
kind: entryType().notNull(),
|
||||
// only when kind=extra
|
||||
extraKind: text(),
|
||||
airDate: date(),
|
||||
runtime: integer(),
|
||||
thumbnails: image(),
|
||||
thumbnail: image(),
|
||||
|
||||
externalId: entry_extid(),
|
||||
|
||||
@ -88,3 +93,21 @@ export const entryTranslations = schema.table(
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.pk, t.language] })],
|
||||
);
|
||||
|
||||
export const entryRelations = relations(entries, ({ one, many }) => ({
|
||||
translations: many(entryTranslations, { relationName: "entry_translations" }),
|
||||
evj: many(entryVideoJoin, { relationName: "evj_entry" }),
|
||||
show: one(shows, {
|
||||
relationName: "show_entries",
|
||||
fields: [entries.showPk],
|
||||
references: [shows.pk],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const entryTrRelations = relations(entryTranslations, ({ one }) => ({
|
||||
entry: one(entries, {
|
||||
relationName: "entry_translations",
|
||||
fields: [entryTranslations.pk],
|
||||
references: [entries.pk],
|
||||
}),
|
||||
}));
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
date,
|
||||
index,
|
||||
@ -67,3 +68,22 @@ export const seasonTranslations = schema.table(
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.pk, t.language] })],
|
||||
);
|
||||
|
||||
export const seasonRelations = relations(seasons, ({ one, many }) => ({
|
||||
translations: many(seasonTranslations, {
|
||||
relationName: "season_translations",
|
||||
}),
|
||||
show: one(shows, {
|
||||
relationName: "show_seasons",
|
||||
fields: [seasons.showPk],
|
||||
references: [shows.pk],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const seasonTrRelations = relations(seasonTranslations, ({ one }) => ({
|
||||
season: one(seasons, {
|
||||
relationName: "season_translation",
|
||||
fields: [seasonTranslations.pk],
|
||||
references: [seasons.pk],
|
||||
}),
|
||||
}));
|
||||
|
@ -12,6 +12,8 @@ import {
|
||||
uuid,
|
||||
varchar,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { entries } from "./entries";
|
||||
import { seasons } from "./seasons";
|
||||
import { image, language, schema } from "./utils";
|
||||
|
||||
export const showKind = schema.enum("show_kind", ["serie", "movie"]);
|
||||
@ -120,28 +122,30 @@ export const showTranslations = schema.table(
|
||||
|
||||
export const showsRelations = relations(shows, ({ many, one }) => ({
|
||||
selectedTranslation: many(showTranslations, {
|
||||
relationName: "selectedTranslation",
|
||||
relationName: "selected_translation",
|
||||
}),
|
||||
translations: many(showTranslations, { relationName: "showTranslations" }),
|
||||
translations: many(showTranslations, { relationName: "show_translations" }),
|
||||
originalTranslation: one(showTranslations, {
|
||||
relationName: "originalTranslation",
|
||||
relationName: "original_translation",
|
||||
fields: [shows.pk, shows.originalLanguage],
|
||||
references: [showTranslations.pk, showTranslations.language],
|
||||
}),
|
||||
entries: many(entries, { relationName: "show_entries" }),
|
||||
seasons: many(seasons, { relationName: "show_seasons" }),
|
||||
}));
|
||||
export const showsTrRelations = relations(showTranslations, ({ one }) => ({
|
||||
show: one(shows, {
|
||||
relationName: "showTranslations",
|
||||
relationName: "show_translations",
|
||||
fields: [showTranslations.pk],
|
||||
references: [shows.pk],
|
||||
}),
|
||||
selectedTranslation: one(shows, {
|
||||
relationName: "selectedTranslation",
|
||||
relationName: "selected_translation",
|
||||
fields: [showTranslations.pk],
|
||||
references: [shows.pk],
|
||||
}),
|
||||
originalTranslation: one(shows, {
|
||||
relationName: "originalTranslation",
|
||||
relationName: "original_translation",
|
||||
fields: [showTranslations.pk, showTranslations.language],
|
||||
references: [shows.pk, shows.originalLanguage],
|
||||
}),
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { relations, sql } from "drizzle-orm";
|
||||
import {
|
||||
check,
|
||||
integer,
|
||||
@ -33,8 +33,8 @@ export const videos = schema.table(
|
||||
],
|
||||
);
|
||||
|
||||
export const entryVideoJointure = schema.table(
|
||||
"entry_video_jointure",
|
||||
export const entryVideoJoin = schema.table(
|
||||
"entry_video_join",
|
||||
{
|
||||
entry: integer()
|
||||
.notNull()
|
||||
@ -46,3 +46,22 @@ export const entryVideoJointure = schema.table(
|
||||
},
|
||||
(t) => [primaryKey({ columns: [t.entry, t.video] })],
|
||||
);
|
||||
|
||||
export const videosRelations = relations(videos, ({ many }) => ({
|
||||
evj: many(entryVideoJoin, {
|
||||
relationName: "evj_video",
|
||||
}),
|
||||
}));
|
||||
|
||||
export const evjRelations = relations(entryVideoJoin, ({ one }) => ({
|
||||
video: one(videos, {
|
||||
relationName: "evj_video",
|
||||
fields: [entryVideoJoin.video],
|
||||
references: [videos.pk],
|
||||
}),
|
||||
entry: one(entries, {
|
||||
relationName: "evj_entry",
|
||||
fields: [entryVideoJoin.entry],
|
||||
references: [entries.pk],
|
||||
}),
|
||||
}));
|
||||
|
@ -70,3 +70,25 @@ export function conflictUpdateAllExcept<
|
||||
export function sqlarr(array: unknown[]) {
|
||||
return `{${array.map((item) => `"${item}"`).join(",")}}`;
|
||||
}
|
||||
|
||||
// See https://github.com/drizzle-team/drizzle-orm/issues/4044
|
||||
// TODO: type values (everything is a `text` for now)
|
||||
export function values(items: Record<string, unknown>[]) {
|
||||
const [firstProp, ...props] = Object.keys(items[0]);
|
||||
const values = items
|
||||
.map((x) => {
|
||||
let ret = sql`(${x[firstProp]}`;
|
||||
for (const val of props) {
|
||||
ret = sql`${ret}, ${x[val]}`;
|
||||
}
|
||||
return sql`${ret})`;
|
||||
})
|
||||
.reduce((acc, x) => sql`${acc}, ${x}`);
|
||||
const valueNames = [firstProp, ...props].join(", ");
|
||||
|
||||
return {
|
||||
as: (name: string) => {
|
||||
return sql`(values ${values}) as ${sql.raw(name)}(${sql.raw(valueNames)})`;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -1,4 +1,11 @@
|
||||
import Elysia from "elysia";
|
||||
import { Elysia } from "elysia";
|
||||
import { entries } from "./controllers/entries";
|
||||
import { movies } from "./controllers/movies";
|
||||
import { seasonsH } from "./controllers/seasons";
|
||||
import { seed } from "./controllers/seed";
|
||||
import { series } from "./controllers/series";
|
||||
import { videosH } from "./controllers/videos";
|
||||
|
||||
import type { KError } from "./models/error";
|
||||
|
||||
export const base = new Elysia({ name: "base" })
|
||||
@ -30,3 +37,12 @@ export const base = new Elysia({ name: "base" })
|
||||
return error;
|
||||
})
|
||||
.as("plugin");
|
||||
|
||||
export const app = new Elysia()
|
||||
.use(base)
|
||||
.use(movies)
|
||||
.use(series)
|
||||
.use(entries)
|
||||
.use(seasonsH)
|
||||
.use(videosH)
|
||||
.use(seed);
|
@ -1,15 +1,7 @@
|
||||
import jwt from "@elysiajs/jwt";
|
||||
import { swagger } from "@elysiajs/swagger";
|
||||
import { Elysia } from "elysia";
|
||||
import { base } from "./base";
|
||||
import { entries } from "./controllers/entries";
|
||||
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 { migrate } from "./db";
|
||||
import { Image } from "./models/utils";
|
||||
import { app } from "./elysia";
|
||||
import { comment } from "./utils";
|
||||
|
||||
await migrate();
|
||||
@ -31,8 +23,7 @@ if (!secret) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const app = new Elysia()
|
||||
.use(base)
|
||||
app
|
||||
.use(jwt({ secret }))
|
||||
.use(
|
||||
swagger({
|
||||
@ -70,13 +61,6 @@ const app = new Elysia()
|
||||
},
|
||||
}),
|
||||
)
|
||||
.model({ image: Image })
|
||||
.use(movies)
|
||||
.use(series)
|
||||
.use(entries)
|
||||
.use(seasonsH)
|
||||
.use(videos)
|
||||
.use(seed)
|
||||
.listen(3000);
|
||||
|
||||
console.log(`Api running at ${app.server?.hostname}:${app.server?.port}`);
|
||||
|
@ -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" }),
|
||||
),
|
||||
@ -16,15 +16,3 @@ export const EntryTranslation = t.Object({
|
||||
name: t.Nullable(t.String()),
|
||||
description: t.Nullable(t.String()),
|
||||
});
|
||||
|
||||
// export const SeedEntry = t.Intersect([
|
||||
// Entry,
|
||||
// t.Object({ videos: t.Optional(t.Array(Video)) }),
|
||||
// ]);
|
||||
// export type SeedEntry = typeof SeedEntry.static;
|
||||
//
|
||||
// export const SeedExtra = t.Intersect([
|
||||
// Extra,
|
||||
// t.Object({ video: t.Optional(Video) }),
|
||||
// ]);
|
||||
// export type SeedExtra = typeof SeedExtra.static;
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { t } from "elysia";
|
||||
import { EpisodeId } from "../utils/external-id";
|
||||
import { Resource } from "../utils/resource";
|
||||
import { EpisodeId, Resource, SeedImage, TranslationRecord } from "../utils";
|
||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||
|
||||
export const BaseEpisode = t.Intersect([
|
||||
@ -14,5 +13,15 @@ export const BaseEpisode = t.Intersect([
|
||||
}),
|
||||
]);
|
||||
|
||||
export const Episode = t.Intersect([Resource, BaseEpisode, EntryTranslation]);
|
||||
export const Episode = t.Intersect([Resource(), BaseEpisode, EntryTranslation]);
|
||||
export type Episode = typeof Episode.static;
|
||||
|
||||
export const SeedEpisode = t.Intersect([
|
||||
t.Omit(BaseEpisode, ["thumbnail", "createdAt", "nextRefresh"]),
|
||||
t.Object({
|
||||
thumbnail: t.Nullable(SeedImage),
|
||||
translations: TranslationRecord(EntryTranslation),
|
||||
videos: t.Optional(t.Array(t.String({ format: "uuid" }))),
|
||||
}),
|
||||
]);
|
||||
export type SeedEpisode = typeof SeedEpisode.static;
|
||||
|
@ -1,26 +1,25 @@
|
||||
import { t } from "elysia";
|
||||
import { comment } from "../../utils";
|
||||
import { EpisodeId } from "../utils/external-id";
|
||||
import { comment } from "~/utils";
|
||||
import { SeedImage } from "../utils";
|
||||
import { Resource } from "../utils/resource";
|
||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||
import { BaseEntry } from "./base-entry";
|
||||
|
||||
export const ExtraType = t.UnionEnum([
|
||||
"other",
|
||||
"trailers",
|
||||
"trailer",
|
||||
"interview",
|
||||
"behind-the-scenes",
|
||||
"deleted-scenes",
|
||||
"bloopers",
|
||||
"behind-the-scene",
|
||||
"deleted-scene",
|
||||
"blooper",
|
||||
]);
|
||||
export type ExtraType = typeof ExtraType.static;
|
||||
|
||||
export const BaseExtra = t.Intersect(
|
||||
[
|
||||
BaseEntry,
|
||||
t.Omit(BaseEntry, ["nextRefresh", "airDate"]),
|
||||
t.Object({
|
||||
kind: ExtraType,
|
||||
// not sure about this id type
|
||||
externalId: EpisodeId,
|
||||
name: t.String(),
|
||||
}),
|
||||
],
|
||||
{
|
||||
@ -31,5 +30,15 @@ export const BaseExtra = t.Intersect(
|
||||
},
|
||||
);
|
||||
|
||||
export const Extra = t.Intersect([Resource, BaseExtra, EntryTranslation]);
|
||||
export const Extra = t.Intersect([Resource(), BaseExtra]);
|
||||
export type Extra = typeof Extra.static;
|
||||
|
||||
export const SeedExtra = t.Intersect([
|
||||
t.Omit(BaseExtra, ["thumbnail", "createdAt"]),
|
||||
t.Object({
|
||||
slug: t.String({ format: "slug" }),
|
||||
thumbnail: t.Nullable(SeedImage),
|
||||
video: t.String({ format: "uuid" }),
|
||||
}),
|
||||
]);
|
||||
export type SeedExtra = typeof SeedExtra.static;
|
||||
|
@ -1,9 +1,19 @@
|
||||
import { t } from "elysia";
|
||||
import { Episode, MovieEntry, Special } from "../entry";
|
||||
import {
|
||||
Episode,
|
||||
MovieEntry,
|
||||
SeedEpisode,
|
||||
SeedMovieEntry,
|
||||
SeedSpecial,
|
||||
Special,
|
||||
} from "../entry";
|
||||
|
||||
export const Entry = t.Union([Episode, MovieEntry, Special]);
|
||||
export type Entry = typeof Entry.static;
|
||||
|
||||
export const SeedEntry = t.Union([SeedEpisode, SeedMovieEntry, SeedSpecial]);
|
||||
export type SeedEntry = typeof SeedEntry.static;
|
||||
|
||||
export * from "./episode";
|
||||
export * from "./movie-entry";
|
||||
export * from "./special";
|
||||
|
@ -1,13 +1,17 @@
|
||||
import { t } from "elysia";
|
||||
import { comment } from "../../utils";
|
||||
import { ExternalId } from "../utils/external-id";
|
||||
import { Image } from "../utils/image";
|
||||
import { Resource } from "../utils/resource";
|
||||
import {
|
||||
ExternalId,
|
||||
Image,
|
||||
Resource,
|
||||
SeedImage,
|
||||
TranslationRecord,
|
||||
} from "../utils";
|
||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||
|
||||
export const BaseMovieEntry = t.Intersect(
|
||||
[
|
||||
t.Omit(BaseEntry, ["thumbnail"]),
|
||||
BaseEntry,
|
||||
t.Object({
|
||||
kind: t.Literal("movie"),
|
||||
order: t.Number({
|
||||
@ -29,13 +33,29 @@ export const MovieEntryTranslation = t.Intersect([
|
||||
EntryTranslation,
|
||||
t.Object({
|
||||
tagline: t.Nullable(t.String()),
|
||||
thumbnail: t.Nullable(Image),
|
||||
poster: t.Nullable(Image),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const MovieEntry = t.Intersect([
|
||||
Resource,
|
||||
Resource(),
|
||||
BaseMovieEntry,
|
||||
MovieEntryTranslation,
|
||||
]);
|
||||
export type MovieEntry = typeof MovieEntry.static;
|
||||
|
||||
export const SeedMovieEntry = t.Intersect([
|
||||
t.Omit(BaseMovieEntry, ["thumbnail", "createdAt", "nextRefresh"]),
|
||||
t.Object({
|
||||
slug: t.Optional(t.String({ format: "slug" })),
|
||||
thumbnail: t.Nullable(SeedImage),
|
||||
translations: TranslationRecord(
|
||||
t.Intersect([
|
||||
t.Omit(MovieEntryTranslation, ["poster"]),
|
||||
t.Object({ poster: t.Nullable(SeedImage) }),
|
||||
]),
|
||||
),
|
||||
videos: t.Optional(t.Array(t.String({ format: "uuid" }))),
|
||||
}),
|
||||
]);
|
||||
export type SeedMovieEntry = typeof SeedMovieEntry.static;
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { t } from "elysia";
|
||||
import { comment } from "../../utils";
|
||||
import { EpisodeId } from "../utils/external-id";
|
||||
import { Resource } from "../utils/resource";
|
||||
import { EpisodeId, Resource, SeedImage, TranslationRecord } from "../utils";
|
||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||
|
||||
export const BaseSpecial = t.Intersect(
|
||||
@ -25,5 +24,15 @@ export const BaseSpecial = t.Intersect(
|
||||
},
|
||||
);
|
||||
|
||||
export const Special = t.Intersect([Resource, BaseSpecial, EntryTranslation]);
|
||||
export const Special = t.Intersect([Resource(), BaseSpecial, EntryTranslation]);
|
||||
export type Special = typeof Special.static;
|
||||
|
||||
export const SeedSpecial = t.Intersect([
|
||||
t.Omit(BaseSpecial, ["thumbnail", "createdAt", "nextRefresh"]),
|
||||
t.Object({
|
||||
thumbnail: t.Nullable(SeedImage),
|
||||
translations: TranslationRecord(EntryTranslation),
|
||||
videos: t.Optional(t.Array(t.String({ format: "uuid" }))),
|
||||
}),
|
||||
]);
|
||||
export type SeedSpecial = typeof SeedSpecial.static;
|
||||
|
@ -23,7 +23,7 @@ export const UnknownEntryTranslation = t.Omit(EntryTranslation, [
|
||||
]);
|
||||
|
||||
export const UnknownEntry = t.Intersect([
|
||||
Resource,
|
||||
Resource(),
|
||||
BaseUnknownEntry,
|
||||
UnknownEntryTranslation,
|
||||
]);
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
@ -117,22 +135,20 @@ export const madeInAbyss = {
|
||||
entries: [
|
||||
{
|
||||
kind: "episode",
|
||||
id: "ab912364-61c8-4752-ac93-5802212467d8",
|
||||
slug: "made-in-abyss-s1e13",
|
||||
order: 13,
|
||||
seasonNumber: 1,
|
||||
episodeNumber: 13,
|
||||
name: "The Challengers",
|
||||
description:
|
||||
"Nanachi and Mitty's past is revealed. How did they become what they are and who is responsible for it? Meanwhile, Riko is on the mend after her injuries.",
|
||||
translations: {
|
||||
en: {
|
||||
name: "The Challengers",
|
||||
description:
|
||||
"Nanachi and Mitty's past is revealed. How did they become what they are and who is responsible for it? Meanwhile, Riko is on the mend after her injuries.",
|
||||
},
|
||||
},
|
||||
runtime: 47,
|
||||
airDate: "2017-09-29",
|
||||
thumbnail: {
|
||||
id: "c2bfd626-bfdb-dee8-caa6-b6a7e7cb74ad",
|
||||
source:
|
||||
"https://image.tmdb.org/t/p/original/j9t1quh24suXxBetV7Q77YngID6.jpg",
|
||||
blurhash: "L370#nD*^jEN}r$$$%J8i_-URkNc",
|
||||
},
|
||||
thumbnail:
|
||||
"https://image.tmdb.org/t/p/original/j9t1quh24suXxBetV7Q77YngID6.jpg",
|
||||
externalId: {
|
||||
themoviedatabase: {
|
||||
serieId: "72636",
|
||||
@ -141,39 +157,23 @@ export const madeInAbyss = {
|
||||
link: "https://www.themoviedb.org/tv/72636/season/1/episode/13",
|
||||
},
|
||||
},
|
||||
createdAt: "2024-10-06T20:09:09.28103Z",
|
||||
nextRefresh: "2024-12-06T20:08:42.366583Z",
|
||||
videos: [
|
||||
{
|
||||
id: "0905bddd-8b93-403c-9b9c-db472e55d6cc",
|
||||
slug: "made-in-abyss-s1e13",
|
||||
path: "/video/Made in Abyss/Made in Abyss S01E13.mkv",
|
||||
rendering:
|
||||
"e27f226fe5e8d87cd396d0c3d24e1b1135aa563fcfca081bf68c6a71b44de107",
|
||||
part: null,
|
||||
version: 1,
|
||||
createdAt: "2024-10-06T20:09:09.28103Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "special",
|
||||
id: "1a83288a-3089-447f-9710-94297d614c51",
|
||||
slug: "made-in-abyss-ova3",
|
||||
// beween s1e13 & movie (which has 13.5 for the `order field`)
|
||||
// between s1e13 & movie (which has 13.5 for the `order field`)
|
||||
order: 13.25,
|
||||
number: 3,
|
||||
name: "Maruruk's Everday 3 - Cleaning",
|
||||
description:
|
||||
"Short played before Made in Abyss Movie 3: Dawn of the Deep Soul in Japan's theatrical screenings before the main movie from 2020-01-17 to 2020-01-23.",
|
||||
translations: {
|
||||
en: {
|
||||
name: "Maruruk's Everday 3 - Cleaning",
|
||||
description:
|
||||
"Short played before Made in Abyss Movie 3: Dawn of the Deep Soul in Japan's theatrical screenings before the main movie from 2020-01-17 to 2020-01-23.",
|
||||
},
|
||||
},
|
||||
runtime: 3,
|
||||
airDate: "2020-01-31",
|
||||
thumbnail: {
|
||||
id: "f4ac4b0a-c857-ea95-4042-601314a26e71",
|
||||
source:
|
||||
"https://image.tmdb.org/t/p/original/4cMeg2ihvACsGVaSUcQJJZd96Je.jpg",
|
||||
blurhash: "LAD,Pg%dc}tPDQfk.7kBo|ayR7WC",
|
||||
},
|
||||
thumbnail:
|
||||
"https://image.tmdb.org/t/p/original/4cMeg2ihvACsGVaSUcQJJZd96Je.jpg",
|
||||
externalId: {
|
||||
themoviedatabase: {
|
||||
serieId: "72636",
|
||||
@ -182,77 +182,48 @@ export const madeInAbyss = {
|
||||
link: "https://www.themoviedb.org/tv/72636/season/0/episode/3",
|
||||
},
|
||||
},
|
||||
createdAt: "2024-10-06T20:09:17.551272Z",
|
||||
nextRefresh: "2024-12-06T20:08:29.463394Z",
|
||||
videos: [
|
||||
{
|
||||
id: "9153f7dc-b635-4a04-a2db-9c08ea205ec3",
|
||||
slug: "made-in-abyss-ova3",
|
||||
path: "/video/Made in Abyss/Made in Abyss S00E03.mkv",
|
||||
rendering:
|
||||
"0391acf2268983de705f65381d252f1b0cd3c3563209303dc50cf71ab400ebf4",
|
||||
part: null,
|
||||
version: 1,
|
||||
createdAt: "2024-10-06T20:09:17.551272Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "movie",
|
||||
id: "59312db0-df8c-446e-be26-2b2107d0cbde",
|
||||
slug: "made-in-abyss-dawn-of-the-deep-soul",
|
||||
order: 13.5,
|
||||
name: "Made in Abyss: Dawn of the Deep Soul",
|
||||
tagline: "Defy the darkness",
|
||||
description:
|
||||
"A continuation of the epic adventure of plucky Riko and Reg who are joined by their new friend Nanachi. Together they descend into the Abyss' treacherous fifth layer, the Sea of Corpses, and encounter the mysterious Bondrewd, a legendary White Whistle whose shadow looms over Nanachi's troubled past. Bondrewd is ingratiatingly hospitable, but the brave adventurers know things are not always as they seem in the enigmatic Abyss.",
|
||||
translations: {
|
||||
en: {
|
||||
name: "Made in Abyss: Dawn of the Deep Soul",
|
||||
tagline: "Defy the darkness",
|
||||
description:
|
||||
"A continuation of the epic adventure of plucky Riko and Reg who are joined by their new friend Nanachi. Together they descend into the Abyss' treacherous fifth layer, the Sea of Corpses, and encounter the mysterious Bondrewd, a legendary White Whistle whose shadow looms over Nanachi's troubled past. Bondrewd is ingratiatingly hospitable, but the brave adventurers know things are not always as they seem in the enigmatic Abyss.",
|
||||
poster:
|
||||
"https://image.tmdb.org/t/p/original/4cMeg2ihvACsGVaSUcQJJZd96Je.jpg",
|
||||
},
|
||||
},
|
||||
thumbnail:
|
||||
"https://image.tmdb.org/t/p/original/4cMeg2ihvACsGVaSUcQJJZd96Je.jpg",
|
||||
runtime: 105,
|
||||
airDate: "2020-01-17",
|
||||
poster: {
|
||||
id: "f4ac4b0a-c857-ea95-4042-601314a26e71",
|
||||
source:
|
||||
"https://image.tmdb.org/t/p/original/4cMeg2ihvACsGVaSUcQJJZd96Je.jpg",
|
||||
blurhash: "LAD,Pg%dc}tPDQfk.7kBo|ayR7WC",
|
||||
},
|
||||
externalId: {
|
||||
themoviedatabase: {
|
||||
dataId: "72636",
|
||||
link: "https://www.themoviedb.org/tv/72636/season/0/episode/3",
|
||||
},
|
||||
},
|
||||
createdAt: "2024-10-06T20:09:17.551272Z",
|
||||
nextRefresh: "2024-12-06T20:08:29.463394Z",
|
||||
videos: [
|
||||
{
|
||||
id: "d3cedfc5-23f4-4aab-b4d3-98bef2954442",
|
||||
slug: "made-in-abyss-dawn-of-the-deep-soul",
|
||||
path: "/video/Made in Abyss/Made in Abyss Dawn of the Deep Soul.mkv",
|
||||
rendering:
|
||||
"a59ba5d88a4935d900db312422eec6f16827ce2572cc8c0eb6c8fffc5e235d6d",
|
||||
part: null,
|
||||
version: 1,
|
||||
createdAt: "2024-10-06T20:09:17.551272Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "episode",
|
||||
id: "bd155be3-39d0-4253-bb29-a60bedb62943",
|
||||
slug: "made-in-abyss-s2e1",
|
||||
order: 14,
|
||||
seasonNumber: 2,
|
||||
episodeNumber: 1,
|
||||
name: "The Compass Pointed to the Darkness",
|
||||
description:
|
||||
"An old man speaks of a golden city that lies within a devouring abyss somewhere in uncharted waters. One explorer may be the key to finding both.",
|
||||
translations: {
|
||||
en: {
|
||||
name: "The Compass Pointed to the Darkness",
|
||||
description:
|
||||
"An old man speaks of a golden city that lies within a devouring abyss somewhere in uncharted waters. One explorer may be the key to finding both.",
|
||||
},
|
||||
},
|
||||
runtime: 23,
|
||||
airDate: "2022-07-06",
|
||||
thumbnail: {
|
||||
id: "072da617-f349-4a68-eb27-d097624b373c",
|
||||
source:
|
||||
"https://image.tmdb.org/t/p/original/Tgu6E3aMf7sFHFbEIMEjetnpMi.jpg",
|
||||
blurhash: "LOI#x]yE01xtE2D*kWt7NGjENGM|",
|
||||
},
|
||||
thumbnail:
|
||||
"https://image.tmdb.org/t/p/original/Tgu6E3aMf7sFHFbEIMEjetnpMi.jpg",
|
||||
externalId: {
|
||||
themoviedatabase: {
|
||||
serieId: "72636",
|
||||
@ -261,62 +232,16 @@ export const madeInAbyss = {
|
||||
link: "https://www.themoviedb.org/tv/72636/season/2/episode/1",
|
||||
},
|
||||
},
|
||||
createdAt: "2024-10-06T20:09:05.651996Z",
|
||||
nextRefresh: "2024-12-06T20:08:22.854073Z",
|
||||
videos: [
|
||||
{
|
||||
id: "3cbcc337-f1da-486a-93bd-c705a58545eb",
|
||||
slug: "made-in-abyss-s2e1-p1",
|
||||
path: "/video/Made in Abyss/Made In Abyss S02E01 Part 1.mkv",
|
||||
rendering:
|
||||
"6239d558696fd1cbcd70a67346e748382fe141bbe7ea01a5d702cdcc02aa996f",
|
||||
part: 1,
|
||||
version: 1,
|
||||
createdAt: "2024-10-06T20:09:05.651996Z",
|
||||
},
|
||||
{
|
||||
id: "67b37a00-7459-4287-9bbf-e058675850b5",
|
||||
slug: "made-in-abyss-s2e1-p2",
|
||||
path: "/video/Made in Abyss/Made In Abyss S02E01 Part 2.mkv",
|
||||
rendering:
|
||||
"6239d558696fd1cbcd70a67346e748382fe141bbe7ea01a5d702cdcc02aa996f",
|
||||
part: 2,
|
||||
version: 1,
|
||||
createdAt: "2024-10-06T20:09:05.651996Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
extras: [
|
||||
{
|
||||
kind: "behind-the-scenes",
|
||||
id: "a9b27fcc-9423-44ad-b875-d35a7a25b613",
|
||||
slug: "made-in-abyss-the-making-of-01",
|
||||
kind: "behind-the-scene",
|
||||
slug: "made-in-abyss-making-of",
|
||||
name: "The Making of MADE IN ABYSS 01",
|
||||
description: null,
|
||||
runtime: 17,
|
||||
airDate: "2017-10-25",
|
||||
thumbnail: null,
|
||||
externalId: {
|
||||
themoviedatabase: {
|
||||
serieId: "72636",
|
||||
season: 0,
|
||||
episode: 13,
|
||||
link: "https://thetvdb.com/series/made-in-abyss/episodes/8835068",
|
||||
},
|
||||
},
|
||||
createdAt: "2024-10-06T20:09:05.651996Z",
|
||||
nextRefresh: "2024-12-06T20:08:22.854073Z",
|
||||
video: {
|
||||
id: "ee3f58eb-0f72-423e-b247-0695cfabfa88",
|
||||
slug: "made-in-abyss-s2e1-p2",
|
||||
path: "/video/Made in Abyss/Made In Abyss S02E01 Part 2.mkv",
|
||||
rendering:
|
||||
"6239d558696fd1cbcd70a67346e748382fe141bbe7ea01a5d702cdcc02aa996f",
|
||||
part: 2,
|
||||
version: 1,
|
||||
createdAt: "2024-10-06T20:09:05.651996Z",
|
||||
},
|
||||
video: "3cd436ee-01ff-4f45-ba98-654282531234",
|
||||
},
|
||||
],
|
||||
} satisfies SeedSerie;
|
||||
|
@ -52,7 +52,7 @@ export const MovieTranslation = t.Object({
|
||||
export type MovieTranslation = typeof MovieTranslation.static;
|
||||
|
||||
export const Movie = t.Intersect([
|
||||
Resource,
|
||||
Resource(),
|
||||
MovieTranslation,
|
||||
BaseMovie,
|
||||
t.Object({ isAvailable: t.Boolean() }),
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { t } from "elysia";
|
||||
import { bubbleImages, madeInAbyss, registerExamples } from "./examples";
|
||||
import { SeasonId } from "./utils/external-id";
|
||||
import { Image, SeedImage } from "./utils/image";
|
||||
import { TranslationRecord } from "./utils/language";
|
||||
@ -25,13 +26,13 @@ export const SeasonTranslation = t.Object({
|
||||
});
|
||||
export type SeasonTranslation = typeof SeasonTranslation.static;
|
||||
|
||||
export const Season = t.Intersect([Resource, BaseSeason, SeasonTranslation]);
|
||||
export const Season = t.Intersect([Resource(), BaseSeason, SeasonTranslation]);
|
||||
export type Season = typeof Season.static;
|
||||
|
||||
export const SeedSeason = t.Intersect([
|
||||
t.Omit(BaseSeason, ["createdAt", "nextRefresh"]),
|
||||
t.Object({
|
||||
slug: t.String({ format: "slug" }),
|
||||
slug: t.String({ format: "slug", examples: ["made-in-abyss-s1"] }),
|
||||
translations: TranslationRecord(
|
||||
t.Intersect([
|
||||
t.Omit(SeasonTranslation, ["poster", "thumbnail", "banner"]),
|
||||
@ -44,3 +45,10 @@ export const SeedSeason = t.Intersect([
|
||||
),
|
||||
}),
|
||||
]);
|
||||
export type SeedSeason = typeof SeedSeason.static;
|
||||
|
||||
registerExamples(Season, {
|
||||
...madeInAbyss.seasons[0],
|
||||
...madeInAbyss.seasons[0].translations.en,
|
||||
...bubbleImages,
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { t } from "elysia";
|
||||
import { SeedEntry, SeedExtra } from "./entry";
|
||||
import { bubbleImages, madeInAbyss, registerExamples } from "./examples";
|
||||
import { SeedSeason } from "./season";
|
||||
import { ExternalId } from "./utils/external-id";
|
||||
@ -55,7 +56,7 @@ export const SerieTranslation = t.Object({
|
||||
});
|
||||
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 const SeedSerie = t.Intersect([
|
||||
@ -74,8 +75,8 @@ export const SeedSerie = t.Intersect([
|
||||
]),
|
||||
),
|
||||
seasons: t.Array(SeedSeason),
|
||||
// entries: t.Array(SeedEntry),
|
||||
// extras: t.Optional(t.Array(SeedExtra)),
|
||||
entries: t.Array(SeedEntry),
|
||||
extras: t.Optional(t.Array(SeedExtra)),
|
||||
}),
|
||||
]);
|
||||
export type SeedSerie = typeof SeedSerie.static;
|
||||
@ -85,4 +86,3 @@ registerExamples(Serie, {
|
||||
...madeInAbyss.translations.en,
|
||||
...bubbleImages,
|
||||
});
|
||||
registerExamples(SeedSerie, madeInAbyss);
|
||||
|
@ -8,10 +8,11 @@ FormatRegistry.Set("slug", (slug) => {
|
||||
return /^[a-z0-9-]+$/g.test(slug);
|
||||
});
|
||||
|
||||
export const Resource = t.Object({
|
||||
id: t.String({ format: "uuid" }),
|
||||
slug: t.String({ format: "slug" }),
|
||||
});
|
||||
export const Resource = () =>
|
||||
t.Object({
|
||||
id: t.String({ format: "uuid" }),
|
||||
slug: t.String({ format: "slug" }),
|
||||
});
|
||||
|
||||
const checker = TypeCompiler.Compile(t.String({ format: "uuid" }));
|
||||
export const isUuid = (id: string) => checker.Check(id);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { t } from "elysia";
|
||||
import { type TSchema, t } from "elysia";
|
||||
import { comment } from "../utils";
|
||||
import { bubbleVideo, registerExamples } from "./examples";
|
||||
|
||||
@ -31,8 +31,46 @@ export const Video = t.Object({
|
||||
}),
|
||||
|
||||
createdAt: t.String({ format: "date-time" }),
|
||||
|
||||
guess: t.Optional(
|
||||
t.Recursive((Self) =>
|
||||
t.Object(
|
||||
{
|
||||
title: t.String(),
|
||||
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.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,
|
||||
description: comment`
|
||||
Metadata guessed from the filename. Kyoo can use those informations to bypass
|
||||
the scanner/metadata fetching and just register videos to movies/entries that already
|
||||
exists. If Kyoo can't find a matching movie/entry, this information will be sent to
|
||||
the scanner.
|
||||
`,
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
});
|
||||
export type Video = typeof Video.static;
|
||||
registerExamples(Video, bubbleVideo);
|
||||
|
||||
export const SeedVideo = t.Omit(Video, ["id", "slug", "createdAt"]);
|
||||
export type SeedVideo = typeof SeedVideo.static;
|
||||
|
@ -6,3 +6,7 @@ export const comment = (str: TemplateStringsArray, ...values: any[]) =>
|
||||
.replace(/^[ \t]+/gm, "") // leading spaces
|
||||
.replace(/([^\n])\n([^\n])/g, "$1 $2") // two lines to space separated line
|
||||
.replace(/\n{2}/g, "\n"); // keep newline if there's an empty line
|
||||
|
||||
export function getYear(date: string) {
|
||||
return new Date(date).getUTCFullYear();
|
||||
}
|
||||
|
5
api/tests/helpers/index.ts
Normal file
5
api/tests/helpers/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export * from "./movies-helper";
|
||||
export * from "./series-helper";
|
||||
export * from "./videos-helper";
|
||||
|
||||
export * from "~/elysia";
|
@ -1,17 +1,12 @@
|
||||
import Elysia from "elysia";
|
||||
import { buildUrl } from "tests/utils";
|
||||
import { base } from "~/base";
|
||||
import { movies } from "~/controllers/movies";
|
||||
import { seed } from "~/controllers/seed";
|
||||
import { app } from "~/elysia";
|
||||
import type { SeedMovie } from "~/models/movie";
|
||||
|
||||
export const movieApp = new Elysia().use(base).use(movies).use(seed);
|
||||
|
||||
export const getMovie = async (
|
||||
id: string,
|
||||
{ langs, ...query }: { langs?: string; preferOriginal?: boolean },
|
||||
) => {
|
||||
const resp = await movieApp.handle(
|
||||
const resp = await app.handle(
|
||||
new Request(buildUrl(`movies/${id}`, query), {
|
||||
method: "GET",
|
||||
headers: langs
|
||||
@ -37,7 +32,7 @@ export const getMovies = async ({
|
||||
langs?: string;
|
||||
preferOriginal?: boolean;
|
||||
}) => {
|
||||
const resp = await movieApp.handle(
|
||||
const resp = await app.handle(
|
||||
new Request(buildUrl("movies", query), {
|
||||
method: "GET",
|
||||
headers: langs
|
||||
@ -52,7 +47,7 @@ export const getMovies = async ({
|
||||
};
|
||||
|
||||
export const createMovie = async (movie: SeedMovie) => {
|
||||
const resp = await movieApp.handle(
|
||||
const resp = await app.handle(
|
||||
new Request(buildUrl("movies"), {
|
||||
method: "POST",
|
||||
body: JSON.stringify(movie),
|
17
api/tests/helpers/series-helper.ts
Normal file
17
api/tests/helpers/series-helper.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { buildUrl } from "tests/utils";
|
||||
import { app } from "~/elysia";
|
||||
import type { SeedSerie } from "~/models/serie";
|
||||
|
||||
export const createSerie = async (serie: SeedSerie) => {
|
||||
const resp = await app.handle(
|
||||
new Request(buildUrl("series"), {
|
||||
method: "POST",
|
||||
body: JSON.stringify(serie),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const body = await resp.json();
|
||||
return [resp, body] as const;
|
||||
};
|
17
api/tests/helpers/videos-helper.ts
Normal file
17
api/tests/helpers/videos-helper.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { buildUrl } from "tests/utils";
|
||||
import { app } from "~/elysia";
|
||||
import type { SeedVideo } from "~/models/video";
|
||||
|
||||
export const createVideo = async (video: SeedVideo | SeedVideo[]) => {
|
||||
const resp = await app.handle(
|
||||
new Request(buildUrl("videos"), {
|
||||
method: "POST",
|
||||
body: JSON.stringify(Array.isArray(video) ? video : [video]),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}),
|
||||
);
|
||||
const body = await resp.json();
|
||||
return [resp, body] as const;
|
||||
};
|
@ -1,5 +1,4 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { beforeAll, describe, expect, it } from "bun:test";
|
||||
import { expectStatus } from "tests/utils";
|
||||
import { seedMovie } from "~/controllers/seed/movies";
|
||||
import { db } from "~/db";
|
||||
@ -7,18 +6,15 @@ import { shows } from "~/db/schema";
|
||||
import { bubble } from "~/models/examples";
|
||||
import { dune1984 } from "~/models/examples/dune-1984";
|
||||
import { dune } from "~/models/examples/dune-2021";
|
||||
import { createMovie, getMovies, movieApp } from "./movies-helper";
|
||||
import { app, createMovie, getMovies } from "../helpers";
|
||||
|
||||
beforeAll(async () => {
|
||||
await db.delete(shows);
|
||||
for (const movie of [bubble, dune1984, dune]) await seedMovie(movie);
|
||||
});
|
||||
afterAll(async () => {
|
||||
await db.delete(shows);
|
||||
});
|
||||
|
||||
describe("with a null value", () => {
|
||||
// Those before/after hooks are NOT scopped to the describe due to a bun bug
|
||||
// Those before/after hooks are NOT scoped to the describe due to a bun bug
|
||||
// instead we just make a new file for those /shrug
|
||||
// see: https://github.com/oven-sh/bun/issues/5738
|
||||
beforeAll(async () => {
|
||||
@ -47,9 +43,6 @@ describe("with a null value", () => {
|
||||
externalId: {},
|
||||
});
|
||||
});
|
||||
afterAll(async () => {
|
||||
await db.delete(shows).where(eq(shows.slug, "no-air-date"));
|
||||
});
|
||||
|
||||
it("sort by dates desc with a null value", async () => {
|
||||
let [resp, body] = await getMovies({
|
||||
@ -77,7 +70,7 @@ describe("with a null value", () => {
|
||||
),
|
||||
});
|
||||
|
||||
resp = await movieApp.handle(new Request(next));
|
||||
resp = await app.handle(new Request(next));
|
||||
body = await resp.json();
|
||||
|
||||
expectStatus(resp, body).toBe(200);
|
||||
@ -124,7 +117,7 @@ describe("with a null value", () => {
|
||||
),
|
||||
});
|
||||
|
||||
resp = await movieApp.handle(new Request(next));
|
||||
resp = await app.handle(new Request(next));
|
||||
body = await resp.json();
|
||||
|
||||
expectStatus(resp, body).toBe(200);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
||||
import { beforeAll, describe, expect, it } from "bun:test";
|
||||
import { expectStatus } from "tests/utils";
|
||||
import { seedMovie } from "~/controllers/seed/movies";
|
||||
import { db } from "~/db";
|
||||
@ -8,15 +8,12 @@ import { dune1984 } from "~/models/examples/dune-1984";
|
||||
import { dune } from "~/models/examples/dune-2021";
|
||||
import type { Movie } from "~/models/movie";
|
||||
import { isUuid } from "~/models/utils";
|
||||
import { getMovies, movieApp } from "./movies-helper";
|
||||
import { app, getMovies } from "../helpers";
|
||||
|
||||
beforeAll(async () => {
|
||||
await db.delete(shows);
|
||||
for (const movie of [bubble, dune1984, dune]) await seedMovie(movie);
|
||||
});
|
||||
afterAll(async () => {
|
||||
await db.delete(shows);
|
||||
});
|
||||
|
||||
describe("Get all movies", () => {
|
||||
it("Invalid filter params", async () => {
|
||||
@ -73,7 +70,7 @@ describe("Get all movies", () => {
|
||||
});
|
||||
expectStatus(resp, body).toBe(200);
|
||||
|
||||
resp = await movieApp.handle(new Request(body.next));
|
||||
resp = await app.handle(new Request(body.next));
|
||||
body = await resp.json();
|
||||
|
||||
expectStatus(resp, body).toBe(200);
|
||||
@ -106,7 +103,7 @@ describe("Get all movies", () => {
|
||||
),
|
||||
});
|
||||
|
||||
resp = await movieApp.handle(new Request(next));
|
||||
resp = await app.handle(new Request(next));
|
||||
body = await resp.json();
|
||||
|
||||
expectStatus(resp, body).toBe(200);
|
||||
@ -162,7 +159,7 @@ describe("Get all movies", () => {
|
||||
expect(items.length).toBe(1);
|
||||
expect(items[0].id).toBe(expectedIds[0]);
|
||||
// Get Second Page
|
||||
resp = await movieApp.handle(new Request(body.next));
|
||||
resp = await app.handle(new Request(body.next));
|
||||
body = await resp.json();
|
||||
|
||||
expectStatus(resp, body).toBe(200);
|
||||
@ -177,7 +174,7 @@ describe("Get all movies", () => {
|
||||
});
|
||||
expectStatus(resp, body).toBe(200);
|
||||
|
||||
const resp2 = await movieApp.handle(new Request(body.next));
|
||||
const resp2 = await app.handle(new Request(body.next));
|
||||
const body2 = await resp2.json();
|
||||
expectStatus(resp2, body).toBe(200);
|
||||
|
||||
@ -188,7 +185,7 @@ describe("Get all movies", () => {
|
||||
});
|
||||
|
||||
it("Get /random", async () => {
|
||||
const resp = await movieApp.handle(
|
||||
const resp = await app.handle(
|
||||
new Request("http://localhost/movies/random"),
|
||||
);
|
||||
expect(resp.status).toBe(302);
|
||||
|
@ -2,13 +2,13 @@ import { beforeAll, describe, expect, it } from "bun:test";
|
||||
import { expectStatus } from "tests/utils";
|
||||
import { seedMovie } from "~/controllers/seed/movies";
|
||||
import { bubble } from "~/models/examples";
|
||||
import { getMovie } from "./movies-helper";
|
||||
import { getMovie } from "../helpers";
|
||||
|
||||
let bubbleId = "";
|
||||
|
||||
beforeAll(async () => {
|
||||
const ret = await seedMovie(bubble);
|
||||
if (ret.status !== 422) bubbleId = ret.id;
|
||||
if (!("status" in ret)) bubbleId = ret.id;
|
||||
});
|
||||
|
||||
describe("Get movie", () => {
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { afterAll, beforeAll, describe, expect, it, test } from "bun:test";
|
||||
import { beforeAll, describe, expect, it } from "bun:test";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { expectStatus } from "tests/utils";
|
||||
import { db } from "~/db";
|
||||
import { showTranslations, shows, videos } from "~/db/schema";
|
||||
import { bubble } from "~/models/examples";
|
||||
import { dune, duneVideo } from "~/models/examples/dune-2021";
|
||||
import { createMovie } from "./movies-helper";
|
||||
import { createMovie, createVideo } from "../helpers";
|
||||
|
||||
describe("Movie seeding", () => {
|
||||
it("Can create a movie", async () => {
|
||||
@ -293,9 +293,116 @@ describe("Movie seeding", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test.todo("Create correct video slug (version)", async () => {});
|
||||
test.todo("Create correct video slug (part)", async () => {});
|
||||
test.todo("Create correct video slug (rendering)", async () => {});
|
||||
it("Create correct video slug", async () => {
|
||||
const [vresp, video] = await createVideo({
|
||||
path: "/video/bubble.mkv",
|
||||
part: null,
|
||||
version: 1,
|
||||
rendering: "oeunhtoeuth",
|
||||
});
|
||||
expectStatus(vresp, video).toBe(201);
|
||||
|
||||
const [resp, body] = await createMovie({
|
||||
...bubble,
|
||||
slug: "video-slug-test1",
|
||||
videos: [video[0].id],
|
||||
});
|
||||
expectStatus(resp, body).toBe(201);
|
||||
|
||||
const ret = await db.query.videos.findFirst({
|
||||
where: eq(videos.id, video[0].id),
|
||||
with: { evj: { with: { entry: true } } },
|
||||
});
|
||||
expect(ret).not.toBe(undefined);
|
||||
expect(ret!.evj).toBeArrayOfSize(1);
|
||||
expect(ret!.evj[0].slug).toBe("video-slug-test1");
|
||||
});
|
||||
|
||||
it("Create correct video slug (version)", async () => {
|
||||
const [vresp, video] = await createVideo({
|
||||
path: "/video/bubble2.mkv",
|
||||
part: null,
|
||||
version: 2,
|
||||
rendering: "oeunhtoeuth",
|
||||
});
|
||||
expectStatus(vresp, video).toBe(201);
|
||||
|
||||
const [resp, body] = await createMovie({
|
||||
...bubble,
|
||||
slug: "bubble-vtest",
|
||||
videos: [video[0].id],
|
||||
});
|
||||
expectStatus(resp, body).toBe(201);
|
||||
|
||||
const ret = await db.query.videos.findFirst({
|
||||
where: eq(videos.id, video[0].id),
|
||||
with: { evj: { with: { entry: true } } },
|
||||
});
|
||||
expect(ret).not.toBe(undefined);
|
||||
expect(ret!.evj).toBeArrayOfSize(1);
|
||||
expect(ret!.evj[0].slug).toBe("bubble-vtest-v2");
|
||||
});
|
||||
it("Create correct video slug (part)", async () => {
|
||||
const [vresp, video] = await createVideo({
|
||||
path: "/video/bubble5.mkv",
|
||||
part: 1,
|
||||
version: 2,
|
||||
rendering: "oaoeueunhtoeuth",
|
||||
});
|
||||
expectStatus(vresp, video).toBe(201);
|
||||
|
||||
const [resp, body] = await createMovie({
|
||||
...bubble,
|
||||
slug: "bubble-ptest",
|
||||
videos: [video[0].id],
|
||||
});
|
||||
expectStatus(resp, body).toBe(201);
|
||||
|
||||
const ret = await db.query.videos.findFirst({
|
||||
where: eq(videos.id, video[0].id),
|
||||
with: { evj: { with: { entry: true } } },
|
||||
});
|
||||
expect(ret).not.toBe(undefined);
|
||||
expect(ret!.evj).toBeArrayOfSize(1);
|
||||
expect(ret!.evj[0].slug).toBe("bubble-ptest-p1-v2");
|
||||
});
|
||||
it("Create correct video slug (rendering)", async () => {
|
||||
const [vresp, video] = await createVideo([
|
||||
{
|
||||
path: "/video/bubble3.mkv",
|
||||
part: null,
|
||||
version: 1,
|
||||
rendering: "oeunhtoeuth",
|
||||
},
|
||||
{
|
||||
path: "/video/bubble4.mkv",
|
||||
part: null,
|
||||
version: 1,
|
||||
rendering: "aoeuaoeu",
|
||||
},
|
||||
]);
|
||||
expectStatus(vresp, video).toBe(201);
|
||||
|
||||
const [resp, body] = await createMovie({
|
||||
...bubble,
|
||||
slug: "bubble-rtest",
|
||||
videos: [video[0].id, video[1].id],
|
||||
});
|
||||
expectStatus(resp, body).toBe(201);
|
||||
|
||||
const ret = await db.query.shows.findFirst({
|
||||
where: eq(shows.id, body.id),
|
||||
with: { entries: { with: { evj: { with: { entry: true } } } } },
|
||||
});
|
||||
expect(ret).not.toBe(undefined);
|
||||
expect(ret!.entries).toBeArrayOfSize(1);
|
||||
expect(ret!.entries[0].slug).toBe("bubble-rtest");
|
||||
expect(ret!.entries[0].evj).toBeArrayOfSize(2);
|
||||
expect(ret!.entries[0].evj).toContainValues([
|
||||
expect.objectContaining({ slug: "bubble-rtest" }),
|
||||
expect.objectContaining({ slug: "bubble-rtest-aoeuaoeu" }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
const cleanup = async () => {
|
||||
@ -304,4 +411,3 @@ const cleanup = async () => {
|
||||
};
|
||||
// cleanup db beforehand to unsure tests are consistent
|
||||
beforeAll(cleanup);
|
||||
afterAll(cleanup);
|
||||
|
88
api/tests/series/seed-serie.test.ts
Normal file
88
api/tests/series/seed-serie.test.ts
Normal file
@ -0,0 +1,88 @@
|
||||
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: { with: { translations: 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,
|
||||
);
|
||||
|
||||
const ep13 = madeInAbyss.entries.find((x) => x.order === 13)!;
|
||||
expect(ret!.entries.find((x) => x.order === 13)).toMatchObject({
|
||||
...ep13,
|
||||
slug: "made-in-abyss-s1e13",
|
||||
thumbnail: { source: ep13.thumbnail },
|
||||
translations: [
|
||||
{
|
||||
language: "en",
|
||||
...ep13.translations.en,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { number, ...special } = madeInAbyss.entries.find(
|
||||
(x) => x.kind === "special",
|
||||
)!;
|
||||
expect(ret!.entries.find((x) => x.kind === "special")).toMatchObject({
|
||||
...special,
|
||||
slug: "made-in-abyss-sp3",
|
||||
episodeNumber: number,
|
||||
thumbnail: { source: special.thumbnail },
|
||||
translations: [
|
||||
{
|
||||
language: "en",
|
||||
...special.translations.en,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const movie = madeInAbyss.entries.find((x) => x.kind === "movie")!;
|
||||
expect(ret!.entries.find((x) => x.kind === "movie")).toMatchObject({
|
||||
...movie,
|
||||
thumbnail: { source: movie.thumbnail },
|
||||
translations: [
|
||||
{
|
||||
language: "en",
|
||||
...movie.translations.en,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { name, video, kind, ...extra } = madeInAbyss.extras[0];
|
||||
expect(ret!.entries.find((x) => x.kind === "extra")).toMatchObject({
|
||||
...extra,
|
||||
extraKind: kind,
|
||||
translations: [
|
||||
{
|
||||
language: "extra",
|
||||
name,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
@ -2,7 +2,7 @@
|
||||
"$schema": "https://biomejs.dev/schemas/1.8.1/schema.json",
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"formatWithErrors": false,
|
||||
"formatWithErrors": true,
|
||||
"indentStyle": "tab",
|
||||
"indentWidth": 2,
|
||||
"lineEnding": "lf",
|
||||
|
Loading…
x
Reference in New Issue
Block a user