mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-11-03 19:17:16 -05: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=="],
 | 
					    "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=="],
 | 
					    "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,
 | 
								"when": 1737763164759,
 | 
				
			||||||
			"tag": "0006_seasons",
 | 
								"tag": "0006_seasons",
 | 
				
			||||||
			"breakpoints": true
 | 
								"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": {
 | 
						"dependencies": {
 | 
				
			||||||
		"@elysiajs/jwt": "^1.2.0",
 | 
							"@elysiajs/jwt": "^1.2.0",
 | 
				
			||||||
		"@elysiajs/swagger": "zoriya/elysia-swagger#build",
 | 
							"@elysiajs/swagger": "zoriya/elysia-swagger#build",
 | 
				
			||||||
		"drizzle-kit": "^0.30.2",
 | 
							"drizzle-kit": "^0.30.3",
 | 
				
			||||||
		"drizzle-orm": "^0.38.4",
 | 
							"drizzle-orm": "^0.39.0",
 | 
				
			||||||
		"elysia": "^1.2.10",
 | 
							"elysia": "^1.2.10",
 | 
				
			||||||
		"parjs": "^1.3.9",
 | 
							"parjs": "^1.3.9",
 | 
				
			||||||
		"pg": "^8.13.1"
 | 
							"pg": "^8.13.1"
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,6 @@
 | 
				
			|||||||
import { type SQL, and, eq, exists, sql } from "drizzle-orm";
 | 
					import { type SQL, and, eq, exists, sql } from "drizzle-orm";
 | 
				
			||||||
import { Elysia, t } from "elysia";
 | 
					import { Elysia, t } from "elysia";
 | 
				
			||||||
import {
 | 
					import { entries, entryVideoJoin, showTranslations, shows } from "~/db/schema";
 | 
				
			||||||
	entries,
 | 
					 | 
				
			||||||
	entryVideoJointure as entryVideoJoint,
 | 
					 | 
				
			||||||
	showTranslations,
 | 
					 | 
				
			||||||
	shows,
 | 
					 | 
				
			||||||
} from "~/db/schema";
 | 
					 | 
				
			||||||
import { getColumns, sqlarr } from "~/db/utils";
 | 
					import { getColumns, sqlarr } from "~/db/utils";
 | 
				
			||||||
import { KError } from "~/models/error";
 | 
					import { KError } from "~/models/error";
 | 
				
			||||||
import { bubble } from "~/models/examples";
 | 
					import { bubble } from "~/models/examples";
 | 
				
			||||||
@ -86,8 +81,8 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
 | 
				
			|||||||
									exists(
 | 
														exists(
 | 
				
			||||||
										db
 | 
															db
 | 
				
			||||||
											.select()
 | 
																.select()
 | 
				
			||||||
											.from(entryVideoJoint)
 | 
																.from(entryVideoJoin)
 | 
				
			||||||
											.where(eq(entries.pk, entryVideoJoint.entry)),
 | 
																.where(eq(entries.pk, entryVideoJoin.entry)),
 | 
				
			||||||
									),
 | 
														),
 | 
				
			||||||
								),
 | 
													),
 | 
				
			||||||
							),
 | 
												),
 | 
				
			||||||
@ -263,8 +258,8 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
 | 
				
			|||||||
					exists(
 | 
										exists(
 | 
				
			||||||
						db
 | 
											db
 | 
				
			||||||
							.select()
 | 
												.select()
 | 
				
			||||||
							.from(entryVideoJoint)
 | 
												.from(entryVideoJoin)
 | 
				
			||||||
							.where(eq(entries.pk, entryVideoJoint.entry)),
 | 
												.where(eq(entries.pk, entryVideoJoin.entry)),
 | 
				
			||||||
					),
 | 
										),
 | 
				
			||||||
				)
 | 
									)
 | 
				
			||||||
				.as("video");
 | 
									.as("video");
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,9 @@
 | 
				
			|||||||
import type { Image } from "~/models/utils";
 | 
					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");
 | 
						const hasher = new Bun.CryptoHasher("sha256");
 | 
				
			||||||
	hasher.update(url);
 | 
						hasher.update(url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -13,7 +16,7 @@ export const processImage = async (url: string): Promise<Image> => {
 | 
				
			|||||||
	};
 | 
						};
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const processOptImage = (url: string | null): Promise<Image | null> => {
 | 
					export const processOptImage = (url: string | null): Image | null => {
 | 
				
			||||||
	if (!url) return Promise.resolve(null);
 | 
						if (!url) return null;
 | 
				
			||||||
	return processImage(url);
 | 
						return processImage(url);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -1,29 +1,37 @@
 | 
				
			|||||||
import { Value } from "@sinclair/typebox/value";
 | 
					import { Value } from "@sinclair/typebox/value";
 | 
				
			||||||
import Elysia from "elysia";
 | 
					import Elysia from "elysia";
 | 
				
			||||||
import { KError } from "~/models/error";
 | 
					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 { Resource } from "~/models/utils";
 | 
				
			||||||
import { comment } from "~/utils";
 | 
					import { comment } from "~/utils";
 | 
				
			||||||
import { SeedMovieResponse, seedMovie } from "./movies";
 | 
					import { SeedMovieResponse, seedMovie } from "./movies";
 | 
				
			||||||
 | 
					import { SeedSerieResponse, seedSerie } from "./series";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const seed = new Elysia()
 | 
					export const seed = new Elysia()
 | 
				
			||||||
	.model({
 | 
						.model({
 | 
				
			||||||
		movie: Movie,
 | 
					 | 
				
			||||||
		"seed-movie": SeedMovie,
 | 
							"seed-movie": SeedMovie,
 | 
				
			||||||
		"seed-movie-response": SeedMovieResponse,
 | 
							"seed-movie-response": SeedMovieResponse,
 | 
				
			||||||
 | 
							"seed-serie": SeedSerie,
 | 
				
			||||||
 | 
							"seed-serie-response": SeedSerieResponse,
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	.post(
 | 
						.post(
 | 
				
			||||||
		"/movies",
 | 
							"/movies",
 | 
				
			||||||
		async ({ body, error }) => {
 | 
							async ({ body, error }) => {
 | 
				
			||||||
			// needed due to https://github.com/elysiajs/elysia/issues/671
 | 
								// 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);
 | 
								const ret = await seedMovie(movie);
 | 
				
			||||||
			if (ret.status === 422) return error(422, ret);
 | 
								if ("status" in ret) return error(ret.status, ret as any);
 | 
				
			||||||
			return error(ret.status, ret);
 | 
								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: {
 | 
								response: {
 | 
				
			||||||
				200: {
 | 
									200: {
 | 
				
			||||||
					...SeedMovieResponse,
 | 
										...SeedMovieResponse,
 | 
				
			||||||
@ -31,18 +39,47 @@ export const seed = new Elysia()
 | 
				
			|||||||
				},
 | 
									},
 | 
				
			||||||
				201: { ...SeedMovieResponse, description: "Created a new movie." },
 | 
									201: { ...SeedMovieResponse, description: "Created a new movie." },
 | 
				
			||||||
				409: {
 | 
									409: {
 | 
				
			||||||
					...Resource,
 | 
										...Resource(),
 | 
				
			||||||
					description: comment`
 | 
										description: comment`
 | 
				
			||||||
						A movie with the same slug but a different air date already exists.
 | 
											A movie with the same slug but a different air date already exists.
 | 
				
			||||||
						Change the slug and re-run the request.
 | 
											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: {
 | 
								detail: {
 | 
				
			||||||
				tags: ["movies"],
 | 
									tags: ["series"],
 | 
				
			||||||
				description:
 | 
									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 { 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 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";
 | 
					import { guessNextRefresh } from "./refresh";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Show = typeof shows.$inferInsert;
 | 
					 | 
				
			||||||
type ShowTrans = typeof showTranslations.$inferInsert;
 | 
					 | 
				
			||||||
type Entry = typeof entries.$inferInsert;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const SeedMovieResponse = t.Object({
 | 
					export const SeedMovieResponse = t.Object({
 | 
				
			||||||
	id: t.String({ format: "uuid" }),
 | 
						id: t.String({ format: "uuid" }),
 | 
				
			||||||
	slug: t.String({ format: "slug", examples: ["bubble"] }),
 | 
						slug: t.String({ format: "slug", examples: ["bubble"] }),
 | 
				
			||||||
@ -30,7 +17,8 @@ export type SeedMovieResponse = typeof SeedMovieResponse.static;
 | 
				
			|||||||
export const seedMovie = async (
 | 
					export const seedMovie = async (
 | 
				
			||||||
	seed: SeedMovie,
 | 
						seed: SeedMovie,
 | 
				
			||||||
): Promise<
 | 
					): Promise<
 | 
				
			||||||
	| (SeedMovieResponse & { status: "Created" | "OK" | "Conflict" })
 | 
						| (SeedMovieResponse & { updated: boolean })
 | 
				
			||||||
 | 
						| { status: 409; id: string; slug: string }
 | 
				
			||||||
	| { status: 422; message: string }
 | 
						| { status: 422; message: string }
 | 
				
			||||||
> => {
 | 
					> => {
 | 
				
			||||||
	if (seed.slug === "random") {
 | 
						if (seed.slug === "random") {
 | 
				
			||||||
@ -43,152 +31,39 @@ export const seedMovie = async (
 | 
				
			|||||||
		seed.slug = `random-${getYear(seed.airDate)}`;
 | 
							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 show = await insertShow(
 | 
				
			||||||
		const movie: Show = {
 | 
							{
 | 
				
			||||||
			kind: "movie",
 | 
								kind: "movie",
 | 
				
			||||||
			startAir: bMovie.airDate,
 | 
								startAir: bMovie.airDate,
 | 
				
			||||||
			nextRefresh: guessNextRefresh(bMovie.airDate ?? new Date()),
 | 
								nextRefresh,
 | 
				
			||||||
			...bMovie,
 | 
								...bMovie,
 | 
				
			||||||
		};
 | 
							},
 | 
				
			||||||
 | 
							translations,
 | 
				
			||||||
		const insert = () =>
 | 
						);
 | 
				
			||||||
			tx
 | 
						if ("status" in show) return show;
 | 
				
			||||||
				.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.
 | 
						// even if never shown to the user, a movie still has an entry.
 | 
				
			||||||
		const movieEntry: Entry = { type: "movie", ...bMovie };
 | 
						const [entry] = await insertEntries(show, [
 | 
				
			||||||
		const [entry] = await tx
 | 
							{
 | 
				
			||||||
			.insert(entries)
 | 
								...bMovie,
 | 
				
			||||||
			.values(movieEntry)
 | 
								kind: "movie",
 | 
				
			||||||
			.onConflictDoUpdate({
 | 
								order: 1,
 | 
				
			||||||
				target: entries.slug,
 | 
								thumbnail: (bMovie.originalLanguage
 | 
				
			||||||
				set: conflictUpdateAllExcept(entries, [
 | 
									? translations[bMovie.originalLanguage]
 | 
				
			||||||
					"pk",
 | 
									: Object.values(translations)[0]
 | 
				
			||||||
					"id",
 | 
								)?.thumbnail,
 | 
				
			||||||
					"slug",
 | 
								translations,
 | 
				
			||||||
					"createdAt",
 | 
								videos,
 | 
				
			||||||
				]),
 | 
							},
 | 
				
			||||||
			})
 | 
						]);
 | 
				
			||||||
			.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 });
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return {
 | 
						return {
 | 
				
			||||||
		status: ret.updated ? "OK" : "Created",
 | 
							updated: show.updated,
 | 
				
			||||||
		id: ret.id,
 | 
							id: show.id,
 | 
				
			||||||
		slug: ret.slug,
 | 
							slug: show.slug,
 | 
				
			||||||
		videos: retVideos,
 | 
							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 { Elysia, t } from "elysia";
 | 
				
			||||||
import { db } from "~/db";
 | 
					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 { bubbleVideo } from "~/models/examples";
 | 
				
			||||||
import { SeedVideo, Video } from "~/models/video";
 | 
					import { SeedVideo, Video } from "~/models/video";
 | 
				
			||||||
import { comment } from "~/utils";
 | 
					import { comment } from "~/utils";
 | 
				
			||||||
 | 
					import { computeVideoSlug } from "./seed/insert/entries";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const CreatedVideo = t.Object({
 | 
					const CreatedVideo = t.Object({
 | 
				
			||||||
	id: t.String({ format: "uuid" }),
 | 
						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({
 | 
						.model({
 | 
				
			||||||
		video: Video,
 | 
							video: Video,
 | 
				
			||||||
		"created-videos": t.Array(CreatedVideo),
 | 
							"created-videos": t.Array(CreatedVideo),
 | 
				
			||||||
@ -20,21 +27,104 @@ export const videos = new Elysia({ prefix: "/videos", tags: ["videos"] })
 | 
				
			|||||||
		response: { 200: "video" },
 | 
							response: { 200: "video" },
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	.post(
 | 
						.post(
 | 
				
			||||||
		"/",
 | 
							"",
 | 
				
			||||||
		async ({ body }) => {
 | 
							async ({ body, error }) => {
 | 
				
			||||||
			return await db
 | 
								const oldRet = await db
 | 
				
			||||||
				.insert(videosT)
 | 
									.insert(videos)
 | 
				
			||||||
				.values(body)
 | 
									.values(body)
 | 
				
			||||||
				.onConflictDoNothing()
 | 
									.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),
 | 
								body: t.Array(SeedVideo),
 | 
				
			||||||
			response: { 201: "created-videos" },
 | 
								response: { 201: t.Array(CreatedVideo) },
 | 
				
			||||||
			detail: {
 | 
								detail: {
 | 
				
			||||||
				description: comment`
 | 
									description: comment`
 | 
				
			||||||
					Create videos in bulk.
 | 
										Create videos in bulk.
 | 
				
			||||||
					Duplicated videos will simply be ignored.
 | 
										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 {
 | 
					import {
 | 
				
			||||||
	check,
 | 
						check,
 | 
				
			||||||
	date,
 | 
						date,
 | 
				
			||||||
@ -14,6 +14,7 @@ import {
 | 
				
			|||||||
} from "drizzle-orm/pg-core";
 | 
					} from "drizzle-orm/pg-core";
 | 
				
			||||||
import { shows } from "./shows";
 | 
					import { shows } from "./shows";
 | 
				
			||||||
import { image, language, schema } from "./utils";
 | 
					import { image, language, schema } from "./utils";
 | 
				
			||||||
 | 
					import { entryVideoJoin } from "./videos";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const entryType = schema.enum("entry_type", [
 | 
					export const entryType = schema.enum("entry_type", [
 | 
				
			||||||
	"unknown",
 | 
						"unknown",
 | 
				
			||||||
@ -51,14 +52,18 @@ export const entries = schema.table(
 | 
				
			|||||||
		pk: integer().primaryKey().generatedAlwaysAsIdentity(),
 | 
							pk: integer().primaryKey().generatedAlwaysAsIdentity(),
 | 
				
			||||||
		id: uuid().notNull().unique().defaultRandom(),
 | 
							id: uuid().notNull().unique().defaultRandom(),
 | 
				
			||||||
		slug: varchar({ length: 255 }).notNull().unique(),
 | 
							slug: varchar({ length: 255 }).notNull().unique(),
 | 
				
			||||||
		showPk: integer().references(() => shows.pk, { onDelete: "cascade" }),
 | 
							showPk: integer()
 | 
				
			||||||
 | 
								.notNull()
 | 
				
			||||||
 | 
								.references(() => shows.pk, { onDelete: "cascade" }),
 | 
				
			||||||
		order: real(),
 | 
							order: real(),
 | 
				
			||||||
		seasonNumber: integer(),
 | 
							seasonNumber: integer(),
 | 
				
			||||||
		episodeNumber: integer(),
 | 
							episodeNumber: integer(),
 | 
				
			||||||
		type: entryType().notNull(),
 | 
							kind: entryType().notNull(),
 | 
				
			||||||
 | 
							// only when kind=extra
 | 
				
			||||||
 | 
							extraKind: text(),
 | 
				
			||||||
		airDate: date(),
 | 
							airDate: date(),
 | 
				
			||||||
		runtime: integer(),
 | 
							runtime: integer(),
 | 
				
			||||||
		thumbnails: image(),
 | 
							thumbnail: image(),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		externalId: entry_extid(),
 | 
							externalId: entry_extid(),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -88,3 +93,21 @@ export const entryTranslations = schema.table(
 | 
				
			|||||||
	},
 | 
						},
 | 
				
			||||||
	(t) => [primaryKey({ columns: [t.pk, t.language] })],
 | 
						(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 {
 | 
					import {
 | 
				
			||||||
	date,
 | 
						date,
 | 
				
			||||||
	index,
 | 
						index,
 | 
				
			||||||
@ -67,3 +68,22 @@ export const seasonTranslations = schema.table(
 | 
				
			|||||||
	},
 | 
						},
 | 
				
			||||||
	(t) => [primaryKey({ columns: [t.pk, t.language] })],
 | 
						(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,
 | 
						uuid,
 | 
				
			||||||
	varchar,
 | 
						varchar,
 | 
				
			||||||
} from "drizzle-orm/pg-core";
 | 
					} from "drizzle-orm/pg-core";
 | 
				
			||||||
 | 
					import { entries } from "./entries";
 | 
				
			||||||
 | 
					import { seasons } from "./seasons";
 | 
				
			||||||
import { image, language, schema } from "./utils";
 | 
					import { image, language, schema } from "./utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const showKind = schema.enum("show_kind", ["serie", "movie"]);
 | 
					export const showKind = schema.enum("show_kind", ["serie", "movie"]);
 | 
				
			||||||
@ -120,28 +122,30 @@ export const showTranslations = schema.table(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const showsRelations = relations(shows, ({ many, one }) => ({
 | 
					export const showsRelations = relations(shows, ({ many, one }) => ({
 | 
				
			||||||
	selectedTranslation: many(showTranslations, {
 | 
						selectedTranslation: many(showTranslations, {
 | 
				
			||||||
		relationName: "selectedTranslation",
 | 
							relationName: "selected_translation",
 | 
				
			||||||
	}),
 | 
						}),
 | 
				
			||||||
	translations: many(showTranslations, { relationName: "showTranslations" }),
 | 
						translations: many(showTranslations, { relationName: "show_translations" }),
 | 
				
			||||||
	originalTranslation: one(showTranslations, {
 | 
						originalTranslation: one(showTranslations, {
 | 
				
			||||||
		relationName: "originalTranslation",
 | 
							relationName: "original_translation",
 | 
				
			||||||
		fields: [shows.pk, shows.originalLanguage],
 | 
							fields: [shows.pk, shows.originalLanguage],
 | 
				
			||||||
		references: [showTranslations.pk, showTranslations.language],
 | 
							references: [showTranslations.pk, showTranslations.language],
 | 
				
			||||||
	}),
 | 
						}),
 | 
				
			||||||
 | 
						entries: many(entries, { relationName: "show_entries" }),
 | 
				
			||||||
 | 
						seasons: many(seasons, { relationName: "show_seasons" }),
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
export const showsTrRelations = relations(showTranslations, ({ one }) => ({
 | 
					export const showsTrRelations = relations(showTranslations, ({ one }) => ({
 | 
				
			||||||
	show: one(shows, {
 | 
						show: one(shows, {
 | 
				
			||||||
		relationName: "showTranslations",
 | 
							relationName: "show_translations",
 | 
				
			||||||
		fields: [showTranslations.pk],
 | 
							fields: [showTranslations.pk],
 | 
				
			||||||
		references: [shows.pk],
 | 
							references: [shows.pk],
 | 
				
			||||||
	}),
 | 
						}),
 | 
				
			||||||
	selectedTranslation: one(shows, {
 | 
						selectedTranslation: one(shows, {
 | 
				
			||||||
		relationName: "selectedTranslation",
 | 
							relationName: "selected_translation",
 | 
				
			||||||
		fields: [showTranslations.pk],
 | 
							fields: [showTranslations.pk],
 | 
				
			||||||
		references: [shows.pk],
 | 
							references: [shows.pk],
 | 
				
			||||||
	}),
 | 
						}),
 | 
				
			||||||
	originalTranslation: one(shows, {
 | 
						originalTranslation: one(shows, {
 | 
				
			||||||
		relationName: "originalTranslation",
 | 
							relationName: "original_translation",
 | 
				
			||||||
		fields: [showTranslations.pk, showTranslations.language],
 | 
							fields: [showTranslations.pk, showTranslations.language],
 | 
				
			||||||
		references: [shows.pk, shows.originalLanguage],
 | 
							references: [shows.pk, shows.originalLanguage],
 | 
				
			||||||
	}),
 | 
						}),
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import { sql } from "drizzle-orm";
 | 
					import { relations, sql } from "drizzle-orm";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
	check,
 | 
						check,
 | 
				
			||||||
	integer,
 | 
						integer,
 | 
				
			||||||
@ -33,8 +33,8 @@ export const videos = schema.table(
 | 
				
			|||||||
	],
 | 
						],
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const entryVideoJointure = schema.table(
 | 
					export const entryVideoJoin = schema.table(
 | 
				
			||||||
	"entry_video_jointure",
 | 
						"entry_video_join",
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
		entry: integer()
 | 
							entry: integer()
 | 
				
			||||||
			.notNull()
 | 
								.notNull()
 | 
				
			||||||
@ -46,3 +46,22 @@ export const entryVideoJointure = schema.table(
 | 
				
			|||||||
	},
 | 
						},
 | 
				
			||||||
	(t) => [primaryKey({ columns: [t.entry, t.video] })],
 | 
						(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[]) {
 | 
					export function sqlarr(array: unknown[]) {
 | 
				
			||||||
	return `{${array.map((item) => `"${item}"`).join(",")}}`;
 | 
						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";
 | 
					import type { KError } from "./models/error";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const base = new Elysia({ name: "base" })
 | 
					export const base = new Elysia({ name: "base" })
 | 
				
			||||||
@ -30,3 +37,12 @@ export const base = new Elysia({ name: "base" })
 | 
				
			|||||||
		return error;
 | 
							return error;
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
	.as("plugin");
 | 
						.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 jwt from "@elysiajs/jwt";
 | 
				
			||||||
import { swagger } from "@elysiajs/swagger";
 | 
					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 { migrate } from "./db";
 | 
				
			||||||
import { Image } from "./models/utils";
 | 
					import { app } from "./elysia";
 | 
				
			||||||
import { comment } from "./utils";
 | 
					import { comment } from "./utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
await migrate();
 | 
					await migrate();
 | 
				
			||||||
@ -31,8 +23,7 @@ if (!secret) {
 | 
				
			|||||||
	process.exit(1);
 | 
						process.exit(1);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const app = new Elysia()
 | 
					app
 | 
				
			||||||
	.use(base)
 | 
					 | 
				
			||||||
	.use(jwt({ secret }))
 | 
						.use(jwt({ secret }))
 | 
				
			||||||
	.use(
 | 
						.use(
 | 
				
			||||||
		swagger({
 | 
							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);
 | 
						.listen(3000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
console.log(`Api running at ${app.server?.hostname}:${app.server?.port}`);
 | 
					console.log(`Api running at ${app.server?.hostname}:${app.server?.port}`);
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ import { t } from "elysia";
 | 
				
			|||||||
import { Image } from "../utils/image";
 | 
					import { Image } from "../utils/image";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const BaseEntry = t.Object({
 | 
					export const BaseEntry = t.Object({
 | 
				
			||||||
	airDate: t.Nullable(t.String({ format: "data" })),
 | 
						airDate: t.Nullable(t.String({ format: "date" })),
 | 
				
			||||||
	runtime: t.Nullable(
 | 
						runtime: t.Nullable(
 | 
				
			||||||
		t.Number({ minimum: 0, description: "Runtime of the episode in minutes" }),
 | 
							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()),
 | 
						name: t.Nullable(t.String()),
 | 
				
			||||||
	description: 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 { t } from "elysia";
 | 
				
			||||||
import { EpisodeId } from "../utils/external-id";
 | 
					import { EpisodeId, Resource, SeedImage, TranslationRecord } from "../utils";
 | 
				
			||||||
import { Resource } from "../utils/resource";
 | 
					 | 
				
			||||||
import { BaseEntry, EntryTranslation } from "./base-entry";
 | 
					import { BaseEntry, EntryTranslation } from "./base-entry";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const BaseEpisode = t.Intersect([
 | 
					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 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 { t } from "elysia";
 | 
				
			||||||
import { comment } from "../../utils";
 | 
					import { comment } from "~/utils";
 | 
				
			||||||
import { EpisodeId } from "../utils/external-id";
 | 
					import { SeedImage } from "../utils";
 | 
				
			||||||
import { Resource } from "../utils/resource";
 | 
					import { Resource } from "../utils/resource";
 | 
				
			||||||
import { BaseEntry, EntryTranslation } from "./base-entry";
 | 
					import { BaseEntry } from "./base-entry";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ExtraType = t.UnionEnum([
 | 
					export const ExtraType = t.UnionEnum([
 | 
				
			||||||
	"other",
 | 
						"other",
 | 
				
			||||||
	"trailers",
 | 
						"trailer",
 | 
				
			||||||
	"interview",
 | 
						"interview",
 | 
				
			||||||
	"behind-the-scenes",
 | 
						"behind-the-scene",
 | 
				
			||||||
	"deleted-scenes",
 | 
						"deleted-scene",
 | 
				
			||||||
	"bloopers",
 | 
						"blooper",
 | 
				
			||||||
]);
 | 
					]);
 | 
				
			||||||
export type ExtraType = typeof ExtraType.static;
 | 
					export type ExtraType = typeof ExtraType.static;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const BaseExtra = t.Intersect(
 | 
					export const BaseExtra = t.Intersect(
 | 
				
			||||||
	[
 | 
						[
 | 
				
			||||||
		BaseEntry,
 | 
							t.Omit(BaseEntry, ["nextRefresh", "airDate"]),
 | 
				
			||||||
		t.Object({
 | 
							t.Object({
 | 
				
			||||||
			kind: ExtraType,
 | 
								kind: ExtraType,
 | 
				
			||||||
			// not sure about this id type
 | 
								name: t.String(),
 | 
				
			||||||
			externalId: EpisodeId,
 | 
					 | 
				
			||||||
		}),
 | 
							}),
 | 
				
			||||||
	],
 | 
						],
 | 
				
			||||||
	{
 | 
						{
 | 
				
			||||||
@ -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 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 { 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 const Entry = t.Union([Episode, MovieEntry, Special]);
 | 
				
			||||||
export type Entry = typeof Entry.static;
 | 
					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 "./episode";
 | 
				
			||||||
export * from "./movie-entry";
 | 
					export * from "./movie-entry";
 | 
				
			||||||
export * from "./special";
 | 
					export * from "./special";
 | 
				
			||||||
 | 
				
			|||||||
@ -1,13 +1,17 @@
 | 
				
			|||||||
import { t } from "elysia";
 | 
					import { t } from "elysia";
 | 
				
			||||||
import { comment } from "../../utils";
 | 
					import { comment } from "../../utils";
 | 
				
			||||||
import { ExternalId } from "../utils/external-id";
 | 
					import {
 | 
				
			||||||
import { Image } from "../utils/image";
 | 
						ExternalId,
 | 
				
			||||||
import { Resource } from "../utils/resource";
 | 
						Image,
 | 
				
			||||||
 | 
						Resource,
 | 
				
			||||||
 | 
						SeedImage,
 | 
				
			||||||
 | 
						TranslationRecord,
 | 
				
			||||||
 | 
					} from "../utils";
 | 
				
			||||||
import { BaseEntry, EntryTranslation } from "./base-entry";
 | 
					import { BaseEntry, EntryTranslation } from "./base-entry";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const BaseMovieEntry = t.Intersect(
 | 
					export const BaseMovieEntry = t.Intersect(
 | 
				
			||||||
	[
 | 
						[
 | 
				
			||||||
		t.Omit(BaseEntry, ["thumbnail"]),
 | 
							BaseEntry,
 | 
				
			||||||
		t.Object({
 | 
							t.Object({
 | 
				
			||||||
			kind: t.Literal("movie"),
 | 
								kind: t.Literal("movie"),
 | 
				
			||||||
			order: t.Number({
 | 
								order: t.Number({
 | 
				
			||||||
@ -29,13 +33,29 @@ export const MovieEntryTranslation = t.Intersect([
 | 
				
			|||||||
	EntryTranslation,
 | 
						EntryTranslation,
 | 
				
			||||||
	t.Object({
 | 
						t.Object({
 | 
				
			||||||
		tagline: t.Nullable(t.String()),
 | 
							tagline: t.Nullable(t.String()),
 | 
				
			||||||
		thumbnail: t.Nullable(Image),
 | 
							poster: t.Nullable(Image),
 | 
				
			||||||
	}),
 | 
						}),
 | 
				
			||||||
]);
 | 
					]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const MovieEntry = t.Intersect([
 | 
					export const MovieEntry = t.Intersect([
 | 
				
			||||||
	Resource,
 | 
						Resource(),
 | 
				
			||||||
	BaseMovieEntry,
 | 
						BaseMovieEntry,
 | 
				
			||||||
	MovieEntryTranslation,
 | 
						MovieEntryTranslation,
 | 
				
			||||||
]);
 | 
					]);
 | 
				
			||||||
export type MovieEntry = typeof MovieEntry.static;
 | 
					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 { t } from "elysia";
 | 
				
			||||||
import { comment } from "../../utils";
 | 
					import { comment } from "../../utils";
 | 
				
			||||||
import { EpisodeId } from "../utils/external-id";
 | 
					import { EpisodeId, Resource, SeedImage, TranslationRecord } from "../utils";
 | 
				
			||||||
import { Resource } from "../utils/resource";
 | 
					 | 
				
			||||||
import { BaseEntry, EntryTranslation } from "./base-entry";
 | 
					import { BaseEntry, EntryTranslation } from "./base-entry";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const BaseSpecial = t.Intersect(
 | 
					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 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([
 | 
					export const UnknownEntry = t.Intersect([
 | 
				
			||||||
	Resource,
 | 
						Resource(),
 | 
				
			||||||
	BaseUnknownEntry,
 | 
						BaseUnknownEntry,
 | 
				
			||||||
	UnknownEntryTranslation,
 | 
						UnknownEntryTranslation,
 | 
				
			||||||
]);
 | 
					]);
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
import type { SeedMovie } from "../movie";
 | 
					import type { SeedMovie } from "~/models/movie";
 | 
				
			||||||
import type { Video } from "../video";
 | 
					import type { Video } from "~/models/video";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const bubbleVideo: Video = {
 | 
					export const bubbleVideo: Video = {
 | 
				
			||||||
	id: "3cd436ee-01ff-4f45-ba98-62aabeb22f25",
 | 
						id: "3cd436ee-01ff-4f45-ba98-62aabeb22f25",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,22 @@
 | 
				
			|||||||
import type { SeedSerie } from "~/models/serie";
 | 
					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 = {
 | 
					export const madeInAbyss = {
 | 
				
			||||||
	slug: "made-in-abyss",
 | 
						slug: "made-in-abyss",
 | 
				
			||||||
@ -117,22 +135,20 @@ export const madeInAbyss = {
 | 
				
			|||||||
	entries: [
 | 
						entries: [
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			kind: "episode",
 | 
								kind: "episode",
 | 
				
			||||||
			id: "ab912364-61c8-4752-ac93-5802212467d8",
 | 
					 | 
				
			||||||
			slug: "made-in-abyss-s1e13",
 | 
					 | 
				
			||||||
			order: 13,
 | 
								order: 13,
 | 
				
			||||||
			seasonNumber: 1,
 | 
								seasonNumber: 1,
 | 
				
			||||||
			episodeNumber: 13,
 | 
								episodeNumber: 13,
 | 
				
			||||||
 | 
								translations: {
 | 
				
			||||||
 | 
									en: {
 | 
				
			||||||
					name: "The Challengers",
 | 
										name: "The Challengers",
 | 
				
			||||||
					description:
 | 
										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.",
 | 
											"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,
 | 
								runtime: 47,
 | 
				
			||||||
			airDate: "2017-09-29",
 | 
								airDate: "2017-09-29",
 | 
				
			||||||
			thumbnail: {
 | 
								thumbnail:
 | 
				
			||||||
				id: "c2bfd626-bfdb-dee8-caa6-b6a7e7cb74ad",
 | 
					 | 
				
			||||||
				source:
 | 
					 | 
				
			||||||
				"https://image.tmdb.org/t/p/original/j9t1quh24suXxBetV7Q77YngID6.jpg",
 | 
									"https://image.tmdb.org/t/p/original/j9t1quh24suXxBetV7Q77YngID6.jpg",
 | 
				
			||||||
				blurhash: "L370#nD*^jEN}r$$$%J8i_-URkNc",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
			externalId: {
 | 
								externalId: {
 | 
				
			||||||
				themoviedatabase: {
 | 
									themoviedatabase: {
 | 
				
			||||||
					serieId: "72636",
 | 
										serieId: "72636",
 | 
				
			||||||
@ -141,39 +157,23 @@ export const madeInAbyss = {
 | 
				
			|||||||
					link: "https://www.themoviedb.org/tv/72636/season/1/episode/13",
 | 
										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",
 | 
								kind: "special",
 | 
				
			||||||
			id: "1a83288a-3089-447f-9710-94297d614c51",
 | 
								// between s1e13 & movie (which has 13.5 for the `order field`)
 | 
				
			||||||
			slug: "made-in-abyss-ova3",
 | 
					 | 
				
			||||||
			// beween s1e13 & movie (which has 13.5 for the `order field`)
 | 
					 | 
				
			||||||
			order: 13.25,
 | 
								order: 13.25,
 | 
				
			||||||
			number: 3,
 | 
								number: 3,
 | 
				
			||||||
 | 
								translations: {
 | 
				
			||||||
 | 
									en: {
 | 
				
			||||||
					name: "Maruruk's Everday 3 - Cleaning",
 | 
										name: "Maruruk's Everday 3 - Cleaning",
 | 
				
			||||||
					description:
 | 
										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.",
 | 
											"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,
 | 
								runtime: 3,
 | 
				
			||||||
			airDate: "2020-01-31",
 | 
								airDate: "2020-01-31",
 | 
				
			||||||
			thumbnail: {
 | 
								thumbnail:
 | 
				
			||||||
				id: "f4ac4b0a-c857-ea95-4042-601314a26e71",
 | 
					 | 
				
			||||||
				source:
 | 
					 | 
				
			||||||
				"https://image.tmdb.org/t/p/original/4cMeg2ihvACsGVaSUcQJJZd96Je.jpg",
 | 
									"https://image.tmdb.org/t/p/original/4cMeg2ihvACsGVaSUcQJJZd96Je.jpg",
 | 
				
			||||||
				blurhash: "LAD,Pg%dc}tPDQfk.7kBo|ayR7WC",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
			externalId: {
 | 
								externalId: {
 | 
				
			||||||
				themoviedatabase: {
 | 
									themoviedatabase: {
 | 
				
			||||||
					serieId: "72636",
 | 
										serieId: "72636",
 | 
				
			||||||
@ -182,77 +182,48 @@ export const madeInAbyss = {
 | 
				
			|||||||
					link: "https://www.themoviedb.org/tv/72636/season/0/episode/3",
 | 
										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",
 | 
								kind: "movie",
 | 
				
			||||||
			id: "59312db0-df8c-446e-be26-2b2107d0cbde",
 | 
					 | 
				
			||||||
			slug: "made-in-abyss-dawn-of-the-deep-soul",
 | 
								slug: "made-in-abyss-dawn-of-the-deep-soul",
 | 
				
			||||||
			order: 13.5,
 | 
								order: 13.5,
 | 
				
			||||||
 | 
								translations: {
 | 
				
			||||||
 | 
									en: {
 | 
				
			||||||
					name: "Made in Abyss: Dawn of the Deep Soul",
 | 
										name: "Made in Abyss: Dawn of the Deep Soul",
 | 
				
			||||||
					tagline: "Defy the darkness",
 | 
										tagline: "Defy the darkness",
 | 
				
			||||||
					description:
 | 
										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.",
 | 
											"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,
 | 
								runtime: 105,
 | 
				
			||||||
			airDate: "2020-01-17",
 | 
								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: {
 | 
								externalId: {
 | 
				
			||||||
				themoviedatabase: {
 | 
									themoviedatabase: {
 | 
				
			||||||
					dataId: "72636",
 | 
										dataId: "72636",
 | 
				
			||||||
					link: "https://www.themoviedb.org/tv/72636/season/0/episode/3",
 | 
										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",
 | 
								kind: "episode",
 | 
				
			||||||
			id: "bd155be3-39d0-4253-bb29-a60bedb62943",
 | 
					 | 
				
			||||||
			slug: "made-in-abyss-s2e1",
 | 
					 | 
				
			||||||
			order: 14,
 | 
								order: 14,
 | 
				
			||||||
			seasonNumber: 2,
 | 
								seasonNumber: 2,
 | 
				
			||||||
			episodeNumber: 1,
 | 
								episodeNumber: 1,
 | 
				
			||||||
 | 
								translations: {
 | 
				
			||||||
 | 
									en: {
 | 
				
			||||||
					name: "The Compass Pointed to the Darkness",
 | 
										name: "The Compass Pointed to the Darkness",
 | 
				
			||||||
					description:
 | 
										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.",
 | 
											"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,
 | 
								runtime: 23,
 | 
				
			||||||
			airDate: "2022-07-06",
 | 
								airDate: "2022-07-06",
 | 
				
			||||||
			thumbnail: {
 | 
								thumbnail:
 | 
				
			||||||
				id: "072da617-f349-4a68-eb27-d097624b373c",
 | 
					 | 
				
			||||||
				source:
 | 
					 | 
				
			||||||
				"https://image.tmdb.org/t/p/original/Tgu6E3aMf7sFHFbEIMEjetnpMi.jpg",
 | 
									"https://image.tmdb.org/t/p/original/Tgu6E3aMf7sFHFbEIMEjetnpMi.jpg",
 | 
				
			||||||
				blurhash: "LOI#x]yE01xtE2D*kWt7NGjENGM|",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
			externalId: {
 | 
								externalId: {
 | 
				
			||||||
				themoviedatabase: {
 | 
									themoviedatabase: {
 | 
				
			||||||
					serieId: "72636",
 | 
										serieId: "72636",
 | 
				
			||||||
@ -261,62 +232,16 @@ export const madeInAbyss = {
 | 
				
			|||||||
					link: "https://www.themoviedb.org/tv/72636/season/2/episode/1",
 | 
										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: [
 | 
						extras: [
 | 
				
			||||||
		{
 | 
							{
 | 
				
			||||||
			kind: "behind-the-scenes",
 | 
								kind: "behind-the-scene",
 | 
				
			||||||
			id: "a9b27fcc-9423-44ad-b875-d35a7a25b613",
 | 
								slug: "made-in-abyss-making-of",
 | 
				
			||||||
			slug: "made-in-abyss-the-making-of-01",
 | 
					 | 
				
			||||||
			name: "The Making of MADE IN ABYSS 01",
 | 
								name: "The Making of MADE IN ABYSS 01",
 | 
				
			||||||
			description: null,
 | 
					 | 
				
			||||||
			runtime: 17,
 | 
								runtime: 17,
 | 
				
			||||||
			airDate: "2017-10-25",
 | 
					 | 
				
			||||||
			thumbnail: null,
 | 
								thumbnail: null,
 | 
				
			||||||
			externalId: {
 | 
								video: "3cd436ee-01ff-4f45-ba98-654282531234",
 | 
				
			||||||
				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",
 | 
					 | 
				
			||||||
			},
 | 
					 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
	],
 | 
						],
 | 
				
			||||||
} satisfies SeedSerie;
 | 
					} satisfies SeedSerie;
 | 
				
			||||||
 | 
				
			|||||||
@ -52,7 +52,7 @@ export const MovieTranslation = t.Object({
 | 
				
			|||||||
export type MovieTranslation = typeof MovieTranslation.static;
 | 
					export type MovieTranslation = typeof MovieTranslation.static;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Movie = t.Intersect([
 | 
					export const Movie = t.Intersect([
 | 
				
			||||||
	Resource,
 | 
						Resource(),
 | 
				
			||||||
	MovieTranslation,
 | 
						MovieTranslation,
 | 
				
			||||||
	BaseMovie,
 | 
						BaseMovie,
 | 
				
			||||||
	t.Object({ isAvailable: t.Boolean() }),
 | 
						t.Object({ isAvailable: t.Boolean() }),
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
import { t } from "elysia";
 | 
					import { t } from "elysia";
 | 
				
			||||||
 | 
					import { bubbleImages, madeInAbyss, registerExamples } from "./examples";
 | 
				
			||||||
import { SeasonId } from "./utils/external-id";
 | 
					import { SeasonId } from "./utils/external-id";
 | 
				
			||||||
import { Image, SeedImage } from "./utils/image";
 | 
					import { Image, SeedImage } from "./utils/image";
 | 
				
			||||||
import { TranslationRecord } from "./utils/language";
 | 
					import { TranslationRecord } from "./utils/language";
 | 
				
			||||||
@ -25,13 +26,13 @@ export const SeasonTranslation = t.Object({
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
export type SeasonTranslation = typeof SeasonTranslation.static;
 | 
					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 type Season = typeof Season.static;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const SeedSeason = t.Intersect([
 | 
					export const SeedSeason = t.Intersect([
 | 
				
			||||||
	t.Omit(BaseSeason, ["createdAt", "nextRefresh"]),
 | 
						t.Omit(BaseSeason, ["createdAt", "nextRefresh"]),
 | 
				
			||||||
	t.Object({
 | 
						t.Object({
 | 
				
			||||||
		slug: t.String({ format: "slug" }),
 | 
							slug: t.String({ format: "slug", examples: ["made-in-abyss-s1"] }),
 | 
				
			||||||
		translations: TranslationRecord(
 | 
							translations: TranslationRecord(
 | 
				
			||||||
			t.Intersect([
 | 
								t.Intersect([
 | 
				
			||||||
				t.Omit(SeasonTranslation, ["poster", "thumbnail", "banner"]),
 | 
									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 { t } from "elysia";
 | 
				
			||||||
 | 
					import { SeedEntry, SeedExtra } from "./entry";
 | 
				
			||||||
import { bubbleImages, madeInAbyss, registerExamples } from "./examples";
 | 
					import { bubbleImages, madeInAbyss, registerExamples } from "./examples";
 | 
				
			||||||
import { SeedSeason } from "./season";
 | 
					import { SeedSeason } from "./season";
 | 
				
			||||||
import { ExternalId } from "./utils/external-id";
 | 
					import { ExternalId } from "./utils/external-id";
 | 
				
			||||||
@ -55,7 +56,7 @@ export const SerieTranslation = t.Object({
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
export type SerieTranslation = typeof SerieTranslation.static;
 | 
					export type SerieTranslation = typeof SerieTranslation.static;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Serie = t.Intersect([Resource, SerieTranslation, BaseSerie]);
 | 
					export const Serie = t.Intersect([Resource(), SerieTranslation, BaseSerie]);
 | 
				
			||||||
export type Serie = typeof Serie.static;
 | 
					export type Serie = typeof Serie.static;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const SeedSerie = t.Intersect([
 | 
					export const SeedSerie = t.Intersect([
 | 
				
			||||||
@ -74,8 +75,8 @@ export const SeedSerie = t.Intersect([
 | 
				
			|||||||
			]),
 | 
								]),
 | 
				
			||||||
		),
 | 
							),
 | 
				
			||||||
		seasons: t.Array(SeedSeason),
 | 
							seasons: t.Array(SeedSeason),
 | 
				
			||||||
		// entries: t.Array(SeedEntry),
 | 
							entries: t.Array(SeedEntry),
 | 
				
			||||||
		// extras: t.Optional(t.Array(SeedExtra)),
 | 
							extras: t.Optional(t.Array(SeedExtra)),
 | 
				
			||||||
	}),
 | 
						}),
 | 
				
			||||||
]);
 | 
					]);
 | 
				
			||||||
export type SeedSerie = typeof SeedSerie.static;
 | 
					export type SeedSerie = typeof SeedSerie.static;
 | 
				
			||||||
@ -85,4 +86,3 @@ registerExamples(Serie, {
 | 
				
			|||||||
	...madeInAbyss.translations.en,
 | 
						...madeInAbyss.translations.en,
 | 
				
			||||||
	...bubbleImages,
 | 
						...bubbleImages,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
registerExamples(SeedSerie, madeInAbyss);
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,8 @@ FormatRegistry.Set("slug", (slug) => {
 | 
				
			|||||||
	return /^[a-z0-9-]+$/g.test(slug);
 | 
						return /^[a-z0-9-]+$/g.test(slug);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const Resource = t.Object({
 | 
					export const Resource = () =>
 | 
				
			||||||
 | 
						t.Object({
 | 
				
			||||||
		id: t.String({ format: "uuid" }),
 | 
							id: t.String({ format: "uuid" }),
 | 
				
			||||||
		slug: t.String({ format: "slug" }),
 | 
							slug: t.String({ format: "slug" }),
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import { t } from "elysia";
 | 
					import { type TSchema, t } from "elysia";
 | 
				
			||||||
import { comment } from "../utils";
 | 
					import { comment } from "../utils";
 | 
				
			||||||
import { bubbleVideo, registerExamples } from "./examples";
 | 
					import { bubbleVideo, registerExamples } from "./examples";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -31,8 +31,46 @@ export const Video = t.Object({
 | 
				
			|||||||
	}),
 | 
						}),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	createdAt: t.String({ format: "date-time" }),
 | 
						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;
 | 
					export type Video = typeof Video.static;
 | 
				
			||||||
registerExamples(Video, bubbleVideo);
 | 
					registerExamples(Video, bubbleVideo);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const SeedVideo = t.Omit(Video, ["id", "slug", "createdAt"]);
 | 
					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(/^[ \t]+/gm, "") // leading spaces
 | 
				
			||||||
		.replace(/([^\n])\n([^\n])/g, "$1 $2") // two lines to space separated line
 | 
							.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
 | 
							.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 { buildUrl } from "tests/utils";
 | 
				
			||||||
import { base } from "~/base";
 | 
					import { app } from "~/elysia";
 | 
				
			||||||
import { movies } from "~/controllers/movies";
 | 
					 | 
				
			||||||
import { seed } from "~/controllers/seed";
 | 
					 | 
				
			||||||
import type { SeedMovie } from "~/models/movie";
 | 
					import type { SeedMovie } from "~/models/movie";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const movieApp = new Elysia().use(base).use(movies).use(seed);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const getMovie = async (
 | 
					export const getMovie = async (
 | 
				
			||||||
	id: string,
 | 
						id: string,
 | 
				
			||||||
	{ langs, ...query }: { langs?: string; preferOriginal?: boolean },
 | 
						{ langs, ...query }: { langs?: string; preferOriginal?: boolean },
 | 
				
			||||||
) => {
 | 
					) => {
 | 
				
			||||||
	const resp = await movieApp.handle(
 | 
						const resp = await app.handle(
 | 
				
			||||||
		new Request(buildUrl(`movies/${id}`, query), {
 | 
							new Request(buildUrl(`movies/${id}`, query), {
 | 
				
			||||||
			method: "GET",
 | 
								method: "GET",
 | 
				
			||||||
			headers: langs
 | 
								headers: langs
 | 
				
			||||||
@ -37,7 +32,7 @@ export const getMovies = async ({
 | 
				
			|||||||
	langs?: string;
 | 
						langs?: string;
 | 
				
			||||||
	preferOriginal?: boolean;
 | 
						preferOriginal?: boolean;
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
	const resp = await movieApp.handle(
 | 
						const resp = await app.handle(
 | 
				
			||||||
		new Request(buildUrl("movies", query), {
 | 
							new Request(buildUrl("movies", query), {
 | 
				
			||||||
			method: "GET",
 | 
								method: "GET",
 | 
				
			||||||
			headers: langs
 | 
								headers: langs
 | 
				
			||||||
@ -52,7 +47,7 @@ export const getMovies = async ({
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const createMovie = async (movie: SeedMovie) => {
 | 
					export const createMovie = async (movie: SeedMovie) => {
 | 
				
			||||||
	const resp = await movieApp.handle(
 | 
						const resp = await app.handle(
 | 
				
			||||||
		new Request(buildUrl("movies"), {
 | 
							new Request(buildUrl("movies"), {
 | 
				
			||||||
			method: "POST",
 | 
								method: "POST",
 | 
				
			||||||
			body: JSON.stringify(movie),
 | 
								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 { beforeAll, describe, expect, it } from "bun:test";
 | 
				
			||||||
import { eq } from "drizzle-orm";
 | 
					 | 
				
			||||||
import { expectStatus } from "tests/utils";
 | 
					import { expectStatus } from "tests/utils";
 | 
				
			||||||
import { seedMovie } from "~/controllers/seed/movies";
 | 
					import { seedMovie } from "~/controllers/seed/movies";
 | 
				
			||||||
import { db } from "~/db";
 | 
					import { db } from "~/db";
 | 
				
			||||||
@ -7,18 +6,15 @@ import { shows } from "~/db/schema";
 | 
				
			|||||||
import { bubble } from "~/models/examples";
 | 
					import { bubble } from "~/models/examples";
 | 
				
			||||||
import { dune1984 } from "~/models/examples/dune-1984";
 | 
					import { dune1984 } from "~/models/examples/dune-1984";
 | 
				
			||||||
import { dune } from "~/models/examples/dune-2021";
 | 
					import { dune } from "~/models/examples/dune-2021";
 | 
				
			||||||
import { createMovie, getMovies, movieApp } from "./movies-helper";
 | 
					import { app, createMovie, getMovies } from "../helpers";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
beforeAll(async () => {
 | 
					beforeAll(async () => {
 | 
				
			||||||
	await db.delete(shows);
 | 
						await db.delete(shows);
 | 
				
			||||||
	for (const movie of [bubble, dune1984, dune]) await seedMovie(movie);
 | 
						for (const movie of [bubble, dune1984, dune]) await seedMovie(movie);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
afterAll(async () => {
 | 
					 | 
				
			||||||
	await db.delete(shows);
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe("with a null value", () => {
 | 
					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
 | 
						// instead we just make a new file for those /shrug
 | 
				
			||||||
	// see: https://github.com/oven-sh/bun/issues/5738
 | 
						// see: https://github.com/oven-sh/bun/issues/5738
 | 
				
			||||||
	beforeAll(async () => {
 | 
						beforeAll(async () => {
 | 
				
			||||||
@ -47,9 +43,6 @@ describe("with a null value", () => {
 | 
				
			|||||||
			externalId: {},
 | 
								externalId: {},
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
	afterAll(async () => {
 | 
					 | 
				
			||||||
		await db.delete(shows).where(eq(shows.slug, "no-air-date"));
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	it("sort by dates desc with a null value", async () => {
 | 
						it("sort by dates desc with a null value", async () => {
 | 
				
			||||||
		let [resp, body] = await getMovies({
 | 
							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();
 | 
							body = await resp.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		expectStatus(resp, body).toBe(200);
 | 
							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();
 | 
							body = await resp.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		expectStatus(resp, body).toBe(200);
 | 
							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 { expectStatus } from "tests/utils";
 | 
				
			||||||
import { seedMovie } from "~/controllers/seed/movies";
 | 
					import { seedMovie } from "~/controllers/seed/movies";
 | 
				
			||||||
import { db } from "~/db";
 | 
					import { db } from "~/db";
 | 
				
			||||||
@ -8,15 +8,12 @@ import { dune1984 } from "~/models/examples/dune-1984";
 | 
				
			|||||||
import { dune } from "~/models/examples/dune-2021";
 | 
					import { dune } from "~/models/examples/dune-2021";
 | 
				
			||||||
import type { Movie } from "~/models/movie";
 | 
					import type { Movie } from "~/models/movie";
 | 
				
			||||||
import { isUuid } from "~/models/utils";
 | 
					import { isUuid } from "~/models/utils";
 | 
				
			||||||
import { getMovies, movieApp } from "./movies-helper";
 | 
					import { app, getMovies } from "../helpers";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
beforeAll(async () => {
 | 
					beforeAll(async () => {
 | 
				
			||||||
	await db.delete(shows);
 | 
						await db.delete(shows);
 | 
				
			||||||
	for (const movie of [bubble, dune1984, dune]) await seedMovie(movie);
 | 
						for (const movie of [bubble, dune1984, dune]) await seedMovie(movie);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
afterAll(async () => {
 | 
					 | 
				
			||||||
	await db.delete(shows);
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe("Get all movies", () => {
 | 
					describe("Get all movies", () => {
 | 
				
			||||||
	it("Invalid filter params", async () => {
 | 
						it("Invalid filter params", async () => {
 | 
				
			||||||
@ -73,7 +70,7 @@ describe("Get all movies", () => {
 | 
				
			|||||||
		});
 | 
							});
 | 
				
			||||||
		expectStatus(resp, body).toBe(200);
 | 
							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();
 | 
							body = await resp.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		expectStatus(resp, body).toBe(200);
 | 
							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();
 | 
							body = await resp.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		expectStatus(resp, body).toBe(200);
 | 
							expectStatus(resp, body).toBe(200);
 | 
				
			||||||
@ -162,7 +159,7 @@ describe("Get all movies", () => {
 | 
				
			|||||||
			expect(items.length).toBe(1);
 | 
								expect(items.length).toBe(1);
 | 
				
			||||||
			expect(items[0].id).toBe(expectedIds[0]);
 | 
								expect(items[0].id).toBe(expectedIds[0]);
 | 
				
			||||||
			// Get Second Page
 | 
								// Get Second Page
 | 
				
			||||||
			resp = await movieApp.handle(new Request(body.next));
 | 
								resp = await app.handle(new Request(body.next));
 | 
				
			||||||
			body = await resp.json();
 | 
								body = await resp.json();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			expectStatus(resp, body).toBe(200);
 | 
								expectStatus(resp, body).toBe(200);
 | 
				
			||||||
@ -177,7 +174,7 @@ describe("Get all movies", () => {
 | 
				
			|||||||
			});
 | 
								});
 | 
				
			||||||
			expectStatus(resp, body).toBe(200);
 | 
								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();
 | 
								const body2 = await resp2.json();
 | 
				
			||||||
			expectStatus(resp2, body).toBe(200);
 | 
								expectStatus(resp2, body).toBe(200);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -188,7 +185,7 @@ describe("Get all movies", () => {
 | 
				
			|||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		it("Get /random", async () => {
 | 
							it("Get /random", async () => {
 | 
				
			||||||
			const resp = await movieApp.handle(
 | 
								const resp = await app.handle(
 | 
				
			||||||
				new Request("http://localhost/movies/random"),
 | 
									new Request("http://localhost/movies/random"),
 | 
				
			||||||
			);
 | 
								);
 | 
				
			||||||
			expect(resp.status).toBe(302);
 | 
								expect(resp.status).toBe(302);
 | 
				
			||||||
 | 
				
			|||||||
@ -2,13 +2,13 @@ import { beforeAll, describe, expect, it } from "bun:test";
 | 
				
			|||||||
import { expectStatus } from "tests/utils";
 | 
					import { expectStatus } from "tests/utils";
 | 
				
			||||||
import { seedMovie } from "~/controllers/seed/movies";
 | 
					import { seedMovie } from "~/controllers/seed/movies";
 | 
				
			||||||
import { bubble } from "~/models/examples";
 | 
					import { bubble } from "~/models/examples";
 | 
				
			||||||
import { getMovie } from "./movies-helper";
 | 
					import { getMovie } from "../helpers";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let bubbleId = "";
 | 
					let bubbleId = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
beforeAll(async () => {
 | 
					beforeAll(async () => {
 | 
				
			||||||
	const ret = await seedMovie(bubble);
 | 
						const ret = await seedMovie(bubble);
 | 
				
			||||||
	if (ret.status !== 422) bubbleId = ret.id;
 | 
						if (!("status" in ret)) bubbleId = ret.id;
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe("Get movie", () => {
 | 
					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 { eq } from "drizzle-orm";
 | 
				
			||||||
import { expectStatus } from "tests/utils";
 | 
					import { expectStatus } from "tests/utils";
 | 
				
			||||||
import { db } from "~/db";
 | 
					import { db } from "~/db";
 | 
				
			||||||
import { showTranslations, shows, videos } from "~/db/schema";
 | 
					import { showTranslations, shows, videos } from "~/db/schema";
 | 
				
			||||||
import { bubble } from "~/models/examples";
 | 
					import { bubble } from "~/models/examples";
 | 
				
			||||||
import { dune, duneVideo } from "~/models/examples/dune-2021";
 | 
					import { dune, duneVideo } from "~/models/examples/dune-2021";
 | 
				
			||||||
import { createMovie } from "./movies-helper";
 | 
					import { createMovie, createVideo } from "../helpers";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe("Movie seeding", () => {
 | 
					describe("Movie seeding", () => {
 | 
				
			||||||
	it("Can create a movie", async () => {
 | 
						it("Can create a movie", async () => {
 | 
				
			||||||
@ -293,9 +293,116 @@ describe("Movie seeding", () => {
 | 
				
			|||||||
		);
 | 
							);
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	test.todo("Create correct video slug (version)", async () => {});
 | 
						it("Create correct video slug", async () => {
 | 
				
			||||||
	test.todo("Create correct video slug (part)", async () => {});
 | 
							const [vresp, video] = await createVideo({
 | 
				
			||||||
	test.todo("Create correct video slug (rendering)", async () => {});
 | 
								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 () => {
 | 
					const cleanup = async () => {
 | 
				
			||||||
@ -304,4 +411,3 @@ const cleanup = async () => {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
// cleanup db beforehand to unsure tests are consistent
 | 
					// cleanup db beforehand to unsure tests are consistent
 | 
				
			||||||
beforeAll(cleanup);
 | 
					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",
 | 
						"$schema": "https://biomejs.dev/schemas/1.8.1/schema.json",
 | 
				
			||||||
	"formatter": {
 | 
						"formatter": {
 | 
				
			||||||
		"enabled": true,
 | 
							"enabled": true,
 | 
				
			||||||
		"formatWithErrors": false,
 | 
							"formatWithErrors": true,
 | 
				
			||||||
		"indentStyle": "tab",
 | 
							"indentStyle": "tab",
 | 
				
			||||||
		"indentWidth": 2,
 | 
							"indentWidth": 2,
 | 
				
			||||||
		"lineEnding": "lf",
 | 
							"lineEnding": "lf",
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user