mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-10-31 10:37:13 -04:00 
			
		
		
		
	Add studios (#824)
This commit is contained in:
		
						commit
						250c9c8ff9
					
				
							
								
								
									
										293
									
								
								api/README.md
									
									
									
									
									
								
							
							
						
						
									
										293
									
								
								api/README.md
									
									
									
									
									
								
							| @ -6,159 +6,168 @@ The many-to-many relation between entries (episodes/movies) & videos is NOT a mi | |||||||
| 
 | 
 | ||||||
| ```mermaid | ```mermaid | ||||||
| erDiagram | erDiagram | ||||||
|     shows { | 	shows { | ||||||
|         guid id PK | 		guid id PK | ||||||
|         kind kind "serie|movie|collection" | 		kind kind "serie|movie|collection" | ||||||
|         string(128) slug UK | 		string(128) slug UK | ||||||
|         genre[] genres | 		genre[] genres | ||||||
|         int rating "From 0 to 100" | 		int rating "From 0 to 100" | ||||||
|         status status "NN" | 		status status "NN" | ||||||
|         datetime added_date | 		datetime added_date | ||||||
|         date start_air | 		date start_air | ||||||
|         date end_air "null for movies" | 		date end_air "null for movies" | ||||||
|         datetime next_refresh | 		datetime next_refresh | ||||||
|         jsonb external_id | 		jsonb external_id | ||||||
|         guid studio_id FK | 		guid studio_id FK | ||||||
|         string original_language | 		string original_language | ||||||
| 		guid collection_id FK | 		guid collection_id FK | ||||||
|     } | 	} | ||||||
|     show_translations { | 	show_translations { | ||||||
|         guid id PK, FK | 		guid id PK, FK | ||||||
|         string language PK | 		string language PK | ||||||
|         string name "NN" | 		string name "NN" | ||||||
|         string tagline | 		string tagline | ||||||
|         string[] aliases | 		string[] aliases | ||||||
|         string description | 		string description | ||||||
|         string[] tags | 		string[] tags | ||||||
|         string trailerUrl | 		string trailerUrl | ||||||
|         jsonb poster | 		jsonb poster | ||||||
|         jsonb banner | 		jsonb banner | ||||||
|         jsonb logo | 		jsonb logo | ||||||
|         jsonb thumbnail | 		jsonb thumbnail | ||||||
|     } | 	} | ||||||
|     shows ||--|{ show_translations : has | 	shows ||--|{ show_translations : has | ||||||
|     shows |o--|| entries : has | 	shows |o--|| entries : has | ||||||
|     shows |o--|| shows : has_collection | 	shows |o--|| shows : has_collection | ||||||
| 
 | 
 | ||||||
|     entries { | 	entries { | ||||||
|         guid id PK | 		guid id PK | ||||||
|         string(256) slug UK | 		string(256) slug UK | ||||||
|         guid show_id FK, UK | 		guid show_id FK, UK | ||||||
|         %% Order is absolute number. | 		%% Order is absolute number. | ||||||
|         uint order "NN" | 		uint order "NN" | ||||||
|         uint season_number UK | 		uint season_number UK | ||||||
|         uint episode_number UK "NN" | 		uint episode_number UK "NN" | ||||||
|         type type "episode|movie|special|extra" | 		type type "episode|movie|special|extra" | ||||||
|         date air_date | 		date air_date | ||||||
|         uint runtime | 		uint runtime | ||||||
|         jsonb thumbnail | 		jsonb thumbnail | ||||||
|         datetime next_refresh | 		datetime next_refresh | ||||||
|         jsonb external_id | 		jsonb external_id | ||||||
|     } | 	} | ||||||
|     entry_translations { | 	entry_translations { | ||||||
|         guid id PK, FK | 		guid id PK, FK | ||||||
|         string language PK | 		string language PK | ||||||
|         string name | 		string name | ||||||
|         string description | 		string description | ||||||
|     } | 	} | ||||||
|     entries ||--|{ entry_translations : has | 	entries ||--|{ entry_translations : has | ||||||
| 
 | 
 | ||||||
|     video { | 	video { | ||||||
|         guid id PK | 		guid id PK | ||||||
|         string path "NN" | 		string path "NN" | ||||||
|         uint rendering "dedup for duplicates part1/2" | 		uint rendering "dedup for duplicates part1/2" | ||||||
|         uint part | 		uint part | ||||||
|         uint version "max version is preferred rendering" | 		uint version "max version is preferred rendering" | ||||||
|     } | 	} | ||||||
|     video }|--|{ entries : for | 	video }|--|{ entries : for | ||||||
| 
 | 
 | ||||||
|     seasons { | 	seasons { | ||||||
|         guid id PK | 		guid id PK | ||||||
|         string(256) slug UK | 		string(256) slug UK | ||||||
|         guid show_id FK | 		guid show_id FK | ||||||
|         uint season_number "NN" | 		uint season_number "NN" | ||||||
|         datetime added_date | 		datetime added_date | ||||||
|         date start_air | 		date start_air | ||||||
|         date end_air | 		date end_air | ||||||
|         datetime next_refresh | 		datetime next_refresh | ||||||
|         jsonb external_id | 		jsonb external_id | ||||||
|     } | 	} | ||||||
| 
 | 
 | ||||||
|     season_translations { | 	season_translations { | ||||||
|         guid id PK,FK | 		guid id PK,FK | ||||||
|         string language PK | 		string language PK | ||||||
|         string name | 		string name | ||||||
|         string description | 		string description | ||||||
|         jsonb poster | 		jsonb poster | ||||||
|         jsonb banner | 		jsonb banner | ||||||
|         jsonb logo | 		jsonb logo | ||||||
|         jsonb thumbnail | 		jsonb thumbnail | ||||||
|     } | 	} | ||||||
|     seasons ||--|{ season_translations : has | 	seasons ||--|{ season_translations : has | ||||||
|     seasons ||--o{ entries : has | 	seasons ||--o{ entries : has | ||||||
|     shows ||--|{ seasons : has | 	shows ||--|{ seasons : has | ||||||
| 
 | 
 | ||||||
|     watched_shows { | 	users { | ||||||
|         guid show_id PK, FK | 		guid id PK | ||||||
|         guid user_id PK, FK | 	} | ||||||
|         status status "completed|watching|droped|planned" |  | ||||||
|         uint seen_entry_count "NN" |  | ||||||
|     } |  | ||||||
|     shows ||--|{ watched_shows : has |  | ||||||
| 
 | 
 | ||||||
|     watched_entries { | 	watched_shows { | ||||||
|         guid entry_id PK, FK | 		guid show_id PK, FK | ||||||
|         guid user_id PK, FK | 		guid user_id PK, FK | ||||||
|         uint time "in seconds, null of finished" | 		status status "completed|watching|dropped|planned" | ||||||
|         uint progress "NN, from 0 to 100" | 		uint seen_entry_count "NN" | ||||||
|         datetime played_date | 		guid next_entry FK | ||||||
|     } | 	} | ||||||
|     entries ||--|{ watched_entries : has | 	shows ||--|{ watched_shows : has | ||||||
|  | 	users ||--|{ watched_shows : has | ||||||
|  | 	watched_shows ||--|o entries : next_entry | ||||||
| 
 | 
 | ||||||
|     roles { | 	history { | ||||||
|         guid show_id PK, FK | 		int id PK | ||||||
|         guid staff_id PK, FK | 		guid entry_id FK | ||||||
|         uint order | 		guid user_id FK | ||||||
|         type type "actor|director|writer|producer|music|other" | 		uint time "in seconds, null of finished" | ||||||
|         jsonb character_image | 		uint progress "NN, from 0 to 100" | ||||||
|     } | 		datetime played_date | ||||||
|  | 	} | ||||||
|  | 	entries ||--|{ history : part_of | ||||||
|  | 	users ||--|{ history : has | ||||||
| 
 | 
 | ||||||
|     role_translations { | 	roles { | ||||||
|         string language PK | 		guid show_id PK, FK | ||||||
|         string character_name | 		guid staff_id PK, FK | ||||||
|     } | 		uint order | ||||||
|     roles||--o{ role_translations : has | 		type type "actor|director|writer|producer|music|other" | ||||||
|     shows ||--|{ roles : has | 		jsonb character_image | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
|     staff { | 	role_translations { | ||||||
|         guid id PK | 		string language PK | ||||||
|         string(256) slug UK | 		string character_name | ||||||
|         jsonb image | 	} | ||||||
|         datetime next_refresh | 	roles||--o{ role_translations : has | ||||||
|         jsonb external_id | 	shows ||--|{ roles : has | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     staff_translations { | 	staff { | ||||||
|         guid id PK,FK | 		guid id PK | ||||||
|         string language PK | 		string(256) slug UK | ||||||
|         string name "NN" | 		jsonb image | ||||||
|     } | 		datetime next_refresh | ||||||
|     staff ||--|{ staff_translations : has | 		jsonb external_id | ||||||
|     staff ||--|{ roles : has | 	} | ||||||
| 
 | 
 | ||||||
|     studios { | 	staff_translations { | ||||||
|       guid id PK | 		guid id PK,FK | ||||||
|       string(128) slug UK | 		string language PK | ||||||
|       jsonb logo | 		string name "NN" | ||||||
|       datetime next_refresh | 	} | ||||||
|       jsonb external_id | 	staff ||--|{ staff_translations : has | ||||||
|     } | 	staff ||--|{ roles : has | ||||||
| 
 | 
 | ||||||
|     studio_translations { | 	studios { | ||||||
|       guid id PK,FK | 		guid id PK | ||||||
|       string language PK | 		string(128) slug UK | ||||||
|       string name | 		jsonb logo | ||||||
|     } | 		datetime next_refresh | ||||||
|     studios ||--|{ studio_translations : has | 		jsonb external_id | ||||||
|     shows ||--|{ studios : has | 	} | ||||||
|  | 
 | ||||||
|  | 	studio_translations { | ||||||
|  | 		guid id PK,FK | ||||||
|  | 		string language PK | ||||||
|  | 		string name | ||||||
|  | 	} | ||||||
|  | 	studios ||--|{ studio_translations : has | ||||||
|  | 	shows }|--|{ studios : has | ||||||
| ``` | ``` | ||||||
|  | |||||||
							
								
								
									
										38
									
								
								api/drizzle/0010_studios.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								api/drizzle/0010_studios.sql
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | |||||||
|  | CREATE TABLE "kyoo"."show_studio_join" ( | ||||||
|  | 	"show" integer NOT NULL, | ||||||
|  | 	"studio" integer NOT NULL, | ||||||
|  | 	CONSTRAINT "show_studio_join_show_studio_pk" PRIMARY KEY("show","studio") | ||||||
|  | ); | ||||||
|  | --> statement-breakpoint | ||||||
|  | CREATE TABLE "kyoo"."studio_translations" ( | ||||||
|  | 	"pk" integer NOT NULL, | ||||||
|  | 	"language" varchar(255) NOT NULL, | ||||||
|  | 	"name" text NOT NULL, | ||||||
|  | 	"logo" jsonb, | ||||||
|  | 	CONSTRAINT "studio_translations_pk_language_pk" PRIMARY KEY("pk","language") | ||||||
|  | ); | ||||||
|  | --> statement-breakpoint | ||||||
|  | CREATE TABLE "kyoo"."studios" ( | ||||||
|  | 	"pk" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "kyoo"."studios_pk_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1), | ||||||
|  | 	"id" uuid DEFAULT gen_random_uuid() NOT NULL, | ||||||
|  | 	"slug" varchar(255) NOT NULL, | ||||||
|  | 	"external_id" jsonb DEFAULT '{}'::jsonb NOT NULL, | ||||||
|  | 	"created_at" timestamp with time zone DEFAULT now() NOT NULL, | ||||||
|  | 	"updated_at" timestamp with time zone NOT NULL, | ||||||
|  | 	CONSTRAINT "studios_id_unique" UNIQUE("id"), | ||||||
|  | 	CONSTRAINT "studios_slug_unique" UNIQUE("slug") | ||||||
|  | ); | ||||||
|  | --> statement-breakpoint | ||||||
|  | ALTER TABLE "kyoo"."entries" ADD COLUMN "updated_at" timestamp with time zone NOT NULL;--> statement-breakpoint | ||||||
|  | ALTER TABLE "kyoo"."seasons" ADD COLUMN "updated_at" timestamp with time zone NOT NULL;--> statement-breakpoint | ||||||
|  | ALTER TABLE "kyoo"."shows" ADD COLUMN "updated_at" timestamp with time zone NOT NULL;--> statement-breakpoint | ||||||
|  | ALTER TABLE "kyoo"."videos" ADD COLUMN "updated_at" timestamp with time zone NOT NULL;--> statement-breakpoint | ||||||
|  | ALTER TABLE "kyoo"."show_studio_join" ADD CONSTRAINT "show_studio_join_show_shows_pk_fk" FOREIGN KEY ("show") REFERENCES "kyoo"."shows"("pk") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint | ||||||
|  | ALTER TABLE "kyoo"."show_studio_join" ADD CONSTRAINT "show_studio_join_studio_studios_pk_fk" FOREIGN KEY ("studio") REFERENCES "kyoo"."studios"("pk") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint | ||||||
|  | ALTER TABLE "kyoo"."studio_translations" ADD CONSTRAINT "studio_translations_pk_studios_pk_fk" FOREIGN KEY ("pk") REFERENCES "kyoo"."studios"("pk") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint | ||||||
|  | CREATE INDEX "studio_name_trgm" ON "kyoo"."studio_translations" USING gin ("name" gin_trgm_ops);--> statement-breakpoint | ||||||
|  | CREATE INDEX "entry_kind" ON "kyoo"."entries" USING hash ("kind");--> statement-breakpoint | ||||||
|  | CREATE INDEX "entry_order" ON "kyoo"."entries" USING btree ("order");--> statement-breakpoint | ||||||
|  | CREATE INDEX "entry_name_trgm" ON "kyoo"."entry_translations" USING gin ("name" gin_trgm_ops);--> statement-breakpoint | ||||||
|  | CREATE INDEX "season_name_trgm" ON "kyoo"."season_translations" USING gin ("name" gin_trgm_ops);--> statement-breakpoint | ||||||
|  | CREATE INDEX "season_nbr" ON "kyoo"."seasons" USING btree ("season_number"); | ||||||
							
								
								
									
										20
									
								
								api/drizzle/0011_join_rename.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								api/drizzle/0011_join_rename.sql
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | |||||||
|  | ALTER TABLE "kyoo"."show_studio_join" RENAME COLUMN "show" TO "show_pk";--> statement-breakpoint | ||||||
|  | ALTER TABLE "kyoo"."show_studio_join" RENAME COLUMN "studio" TO "studio_pk";--> statement-breakpoint | ||||||
|  | ALTER TABLE "kyoo"."entry_video_join" RENAME COLUMN "entry" TO "entry_pk";--> statement-breakpoint | ||||||
|  | ALTER TABLE "kyoo"."entry_video_join" RENAME COLUMN "video" TO "video_pk";--> statement-breakpoint | ||||||
|  | ALTER TABLE "kyoo"."show_studio_join" DROP CONSTRAINT "show_studio_join_show_shows_pk_fk"; | ||||||
|  | --> statement-breakpoint | ||||||
|  | ALTER TABLE "kyoo"."show_studio_join" DROP CONSTRAINT "show_studio_join_studio_studios_pk_fk"; | ||||||
|  | --> statement-breakpoint | ||||||
|  | ALTER TABLE "kyoo"."entry_video_join" DROP CONSTRAINT "entry_video_join_entry_entries_pk_fk"; | ||||||
|  | --> statement-breakpoint | ||||||
|  | ALTER TABLE "kyoo"."entry_video_join" DROP CONSTRAINT "entry_video_join_video_videos_pk_fk"; | ||||||
|  | --> statement-breakpoint | ||||||
|  | ALTER TABLE "kyoo"."show_studio_join" DROP CONSTRAINT "show_studio_join_show_studio_pk";--> statement-breakpoint | ||||||
|  | ALTER TABLE "kyoo"."entry_video_join" DROP CONSTRAINT "entry_video_join_entry_video_pk";--> statement-breakpoint | ||||||
|  | ALTER TABLE "kyoo"."show_studio_join" ADD CONSTRAINT "show_studio_join_show_pk_studio_pk_pk" PRIMARY KEY("show_pk","studio_pk");--> statement-breakpoint | ||||||
|  | ALTER TABLE "kyoo"."entry_video_join" ADD CONSTRAINT "entry_video_join_entry_pk_video_pk_pk" PRIMARY KEY("entry_pk","video_pk");--> statement-breakpoint | ||||||
|  | ALTER TABLE "kyoo"."show_studio_join" ADD CONSTRAINT "show_studio_join_show_pk_shows_pk_fk" FOREIGN KEY ("show_pk") REFERENCES "kyoo"."shows"("pk") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint | ||||||
|  | ALTER TABLE "kyoo"."show_studio_join" ADD CONSTRAINT "show_studio_join_studio_pk_studios_pk_fk" FOREIGN KEY ("studio_pk") REFERENCES "kyoo"."studios"("pk") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint | ||||||
|  | ALTER TABLE "kyoo"."entry_video_join" ADD CONSTRAINT "entry_video_join_entry_pk_entries_pk_fk" FOREIGN KEY ("entry_pk") 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_pk_videos_pk_fk" FOREIGN KEY ("video_pk") REFERENCES "kyoo"."videos"("pk") ON DELETE cascade ON UPDATE no action; | ||||||
							
								
								
									
										1265
									
								
								api/drizzle/meta/0010_snapshot.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1265
									
								
								api/drizzle/meta/0010_snapshot.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1265
									
								
								api/drizzle/meta/0011_snapshot.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1265
									
								
								api/drizzle/meta/0011_snapshot.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -71,6 +71,20 @@ | |||||||
| 			"when": 1740872363604, | 			"when": 1740872363604, | ||||||
| 			"tag": "0009_collections", | 			"tag": "0009_collections", | ||||||
| 			"breakpoints": true | 			"breakpoints": true | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"idx": 10, | ||||||
|  | 			"version": "7", | ||||||
|  | 			"when": 1740950531468, | ||||||
|  | 			"tag": "0010_studios", | ||||||
|  | 			"breakpoints": true | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"idx": 11, | ||||||
|  | 			"version": "7", | ||||||
|  | 			"when": 1741014917375, | ||||||
|  | 			"tag": "0011_join_rename", | ||||||
|  | 			"breakpoints": true | ||||||
| 		} | 		} | ||||||
| 	] | 	] | ||||||
| } | } | ||||||
|  | |||||||
| @ -141,8 +141,8 @@ export const insertEntries = async ( | |||||||
| 		.select( | 		.select( | ||||||
| 			db | 			db | ||||||
| 				.select({ | 				.select({ | ||||||
| 					entry: sql<number>`vids.entryPk::integer`.as("entry"), | 					entryPk: sql<number>`vids.entryPk::integer`.as("entry"), | ||||||
| 					video: sql`${videos.pk}`.as("video"), | 					videoPk: sql`${videos.pk}`.as("video"), | ||||||
| 					slug: computeVideoSlug( | 					slug: computeVideoSlug( | ||||||
| 						sql`${show.slug}::text`, | 						sql`${show.slug}::text`, | ||||||
| 						sql`vids.needRendering::boolean`, | 						sql`vids.needRendering::boolean`, | ||||||
| @ -154,7 +154,7 @@ export const insertEntries = async ( | |||||||
| 		.onConflictDoNothing() | 		.onConflictDoNothing() | ||||||
| 		.returning({ | 		.returning({ | ||||||
| 			slug: entryVideoJoin.slug, | 			slug: entryVideoJoin.slug, | ||||||
| 			entryPk: entryVideoJoin.entry, | 			entryPk: entryVideoJoin.entryPk, | ||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
| 	return retEntries.map((entry) => ({ | 	return retEntries.map((entry) => ({ | ||||||
|  | |||||||
							
								
								
									
										55
									
								
								api/src/controllers/seed/insert/studios.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								api/src/controllers/seed/insert/studios.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,55 @@ | |||||||
|  | import { db } from "~/db"; | ||||||
|  | import { showStudioJoin, studioTranslations, studios } from "~/db/schema"; | ||||||
|  | import { conflictUpdateAllExcept } from "~/db/utils"; | ||||||
|  | import type { SeedStudio } from "~/models/studio"; | ||||||
|  | import { processOptImage } from "../images"; | ||||||
|  | 
 | ||||||
|  | type StudioI = typeof studios.$inferInsert; | ||||||
|  | type StudioTransI = typeof studioTranslations.$inferInsert; | ||||||
|  | 
 | ||||||
|  | export const insertStudios = async (seed: SeedStudio[], showPk: number) => { | ||||||
|  | 	if (!seed.length) return []; | ||||||
|  | 
 | ||||||
|  | 	return await db.transaction(async (tx) => { | ||||||
|  | 		const vals: StudioI[] = seed.map((x) => { | ||||||
|  | 			const { translations, ...item } = x; | ||||||
|  | 			return item; | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		const ret = await tx | ||||||
|  | 			.insert(studios) | ||||||
|  | 			.values(vals) | ||||||
|  | 			.onConflictDoUpdate({ | ||||||
|  | 				target: studios.slug, | ||||||
|  | 				set: conflictUpdateAllExcept(studios, [ | ||||||
|  | 					"pk", | ||||||
|  | 					"id", | ||||||
|  | 					"slug", | ||||||
|  | 					"createdAt", | ||||||
|  | 				]), | ||||||
|  | 			}) | ||||||
|  | 			.returning({ pk: studios.pk, id: studios.id, slug: studios.slug }); | ||||||
|  | 
 | ||||||
|  | 		const trans: StudioTransI[] = seed.flatMap((x, i) => | ||||||
|  | 			Object.entries(x.translations).map(([lang, tr]) => ({ | ||||||
|  | 				pk: ret[i].pk, | ||||||
|  | 				language: lang, | ||||||
|  | 				name: tr.name, | ||||||
|  | 				logo: processOptImage(tr.logo), | ||||||
|  | 			})), | ||||||
|  | 		); | ||||||
|  | 		await tx | ||||||
|  | 			.insert(studioTranslations) | ||||||
|  | 			.values(trans) | ||||||
|  | 			.onConflictDoUpdate({ | ||||||
|  | 				target: [studioTranslations.pk, studioTranslations.language], | ||||||
|  | 				set: conflictUpdateAllExcept(studioTranslations, ["pk", "language"]), | ||||||
|  | 			}); | ||||||
|  | 
 | ||||||
|  | 		await tx | ||||||
|  | 			.insert(showStudioJoin) | ||||||
|  | 			.values(ret.map((studio) => ({ showPk: showPk, studioPk: studio.pk }))) | ||||||
|  | 			.onConflictDoNothing(); | ||||||
|  | 		return ret; | ||||||
|  | 	}); | ||||||
|  | }; | ||||||
| @ -4,6 +4,7 @@ import { getYear } from "~/utils"; | |||||||
| import { insertCollection } from "./insert/collection"; | import { insertCollection } from "./insert/collection"; | ||||||
| import { insertEntries } from "./insert/entries"; | import { insertEntries } from "./insert/entries"; | ||||||
| import { insertShow } from "./insert/shows"; | import { insertShow } from "./insert/shows"; | ||||||
|  | import { insertStudios } from "./insert/studios"; | ||||||
| import { guessNextRefresh } from "./refresh"; | import { guessNextRefresh } from "./refresh"; | ||||||
| 
 | 
 | ||||||
| export const SeedMovieResponse = t.Object({ | export const SeedMovieResponse = t.Object({ | ||||||
| @ -18,6 +19,12 @@ export const SeedMovieResponse = t.Object({ | |||||||
| 			slug: t.String({ format: "slug", examples: ["sawano-collection"] }), | 			slug: t.String({ format: "slug", examples: ["sawano-collection"] }), | ||||||
| 		}), | 		}), | ||||||
| 	), | 	), | ||||||
|  | 	studios: t.Array( | ||||||
|  | 		t.Object({ | ||||||
|  | 			id: t.String({ format: "uuid" }), | ||||||
|  | 			slug: t.String({ format: "slug", examples: ["disney"] }), | ||||||
|  | 		}), | ||||||
|  | 	), | ||||||
| }); | }); | ||||||
| export type SeedMovieResponse = typeof SeedMovieResponse.static; | export type SeedMovieResponse = typeof SeedMovieResponse.static; | ||||||
| 
 | 
 | ||||||
| @ -38,7 +45,7 @@ export const seedMovie = async ( | |||||||
| 		seed.slug = `random-${getYear(seed.airDate)}`; | 		seed.slug = `random-${getYear(seed.airDate)}`; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	const { translations, videos, collection, ...bMovie } = seed; | 	const { translations, videos, collection, studios, ...bMovie } = seed; | ||||||
| 	const nextRefresh = guessNextRefresh(bMovie.airDate ?? new Date()); | 	const nextRefresh = guessNextRefresh(bMovie.airDate ?? new Date()); | ||||||
| 
 | 
 | ||||||
| 	const col = await insertCollection(collection, { | 	const col = await insertCollection(collection, { | ||||||
| @ -74,11 +81,14 @@ export const seedMovie = async ( | |||||||
| 		}, | 		}, | ||||||
| 	]); | 	]); | ||||||
| 
 | 
 | ||||||
|  | 	const retStudios = await insertStudios(studios, show.pk); | ||||||
|  | 
 | ||||||
| 	return { | 	return { | ||||||
| 		updated: show.updated, | 		updated: show.updated, | ||||||
| 		id: show.id, | 		id: show.id, | ||||||
| 		slug: show.slug, | 		slug: show.slug, | ||||||
| 		videos: entry.videos, | 		videos: entry.videos, | ||||||
| 		collection: col, | 		collection: col, | ||||||
|  | 		studios: retStudios, | ||||||
| 	}; | 	}; | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ import { insertCollection } from "./insert/collection"; | |||||||
| import { insertEntries } from "./insert/entries"; | import { insertEntries } from "./insert/entries"; | ||||||
| import { insertSeasons } from "./insert/seasons"; | import { insertSeasons } from "./insert/seasons"; | ||||||
| import { insertShow } from "./insert/shows"; | import { insertShow } from "./insert/shows"; | ||||||
|  | import { insertStudios } from "./insert/studios"; | ||||||
| import { guessNextRefresh } from "./refresh"; | import { guessNextRefresh } from "./refresh"; | ||||||
| 
 | 
 | ||||||
| export const SeedSerieResponse = t.Object({ | export const SeedSerieResponse = t.Object({ | ||||||
| @ -45,6 +46,12 @@ export const SeedSerieResponse = t.Object({ | |||||||
| 			}), | 			}), | ||||||
| 		}), | 		}), | ||||||
| 	), | 	), | ||||||
|  | 	studios: t.Array( | ||||||
|  | 		t.Object({ | ||||||
|  | 			id: t.String({ format: "uuid" }), | ||||||
|  | 			slug: t.String({ format: "slug", examples: ["mappa"] }), | ||||||
|  | 		}), | ||||||
|  | 	), | ||||||
| }); | }); | ||||||
| export type SeedSerieResponse = typeof SeedSerieResponse.static; | export type SeedSerieResponse = typeof SeedSerieResponse.static; | ||||||
| 
 | 
 | ||||||
| @ -65,7 +72,15 @@ export const seedSerie = async ( | |||||||
| 		seed.slug = `random-${getYear(seed.startAir)}`; | 		seed.slug = `random-${getYear(seed.startAir)}`; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	const { translations, seasons, entries, extras, collection, ...serie } = seed; | 	const { | ||||||
|  | 		translations, | ||||||
|  | 		seasons, | ||||||
|  | 		entries, | ||||||
|  | 		extras, | ||||||
|  | 		collection, | ||||||
|  | 		studios, | ||||||
|  | 		...serie | ||||||
|  | 	} = seed; | ||||||
| 	const nextRefresh = guessNextRefresh(serie.startAir ?? new Date()); | 	const nextRefresh = guessNextRefresh(serie.startAir ?? new Date()); | ||||||
| 
 | 
 | ||||||
| 	const col = await insertCollection(collection, { | 	const col = await insertCollection(collection, { | ||||||
| @ -92,6 +107,8 @@ export const seedSerie = async ( | |||||||
| 		(extras ?? []).map((x) => ({ ...x, kind: "extra", extraKind: x.kind })), | 		(extras ?? []).map((x) => ({ ...x, kind: "extra", extraKind: x.kind })), | ||||||
| 	); | 	); | ||||||
| 
 | 
 | ||||||
|  | 	const retStudios = await insertStudios(studios, show.pk); | ||||||
|  | 
 | ||||||
| 	return { | 	return { | ||||||
| 		updated: show.updated, | 		updated: show.updated, | ||||||
| 		id: show.id, | 		id: show.id, | ||||||
| @ -100,5 +117,6 @@ export const seedSerie = async ( | |||||||
| 		entries: retEntries, | 		entries: retEntries, | ||||||
| 		extras: retExtras, | 		extras: retExtras, | ||||||
| 		collection: col, | 		collection: col, | ||||||
|  | 		studios: retStudios, | ||||||
| 	}; | 	}; | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -1,11 +0,0 @@ | |||||||
| import { Elysia, t } from "elysia"; |  | ||||||
| import { Serie } from "~/models/serie"; |  | ||||||
| 
 |  | ||||||
| export const series = new Elysia({ prefix: "/series" }) |  | ||||||
| 	.model({ |  | ||||||
| 		serie: Serie, |  | ||||||
| 		error: t.Object({}), |  | ||||||
| 	}) |  | ||||||
| 	.get("/:id", () => "hello" as unknown as Serie, { |  | ||||||
| 		response: { 200: "serie" }, |  | ||||||
| 	}); |  | ||||||
| @ -10,6 +10,8 @@ import { | |||||||
| import { KError } from "~/models/error"; | import { KError } from "~/models/error"; | ||||||
| import { duneCollection } from "~/models/examples"; | import { duneCollection } from "~/models/examples"; | ||||||
| import { Movie } from "~/models/movie"; | import { Movie } from "~/models/movie"; | ||||||
|  | import { Serie } from "~/models/serie"; | ||||||
|  | import { Show } from "~/models/show"; | ||||||
| import { | import { | ||||||
| 	AcceptLanguage, | 	AcceptLanguage, | ||||||
| 	Filter, | 	Filter, | ||||||
| @ -171,6 +173,34 @@ export const collections = new Elysia({ | |||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 	) | 	) | ||||||
|  | 	.guard({ | ||||||
|  | 		params: t.Object({ | ||||||
|  | 			id: t.String({ | ||||||
|  | 				description: "The id or slug of the collection.", | ||||||
|  | 				example: duneCollection.slug, | ||||||
|  | 			}), | ||||||
|  | 		}), | ||||||
|  | 		query: t.Object({ | ||||||
|  | 			sort: showSort, | ||||||
|  | 			filter: t.Optional(Filter({ def: showFilters })), | ||||||
|  | 			query: t.Optional(t.String({ description: desc.query })), | ||||||
|  | 			limit: t.Integer({ | ||||||
|  | 				minimum: 1, | ||||||
|  | 				maximum: 250, | ||||||
|  | 				default: 50, | ||||||
|  | 				description: "Max page size.", | ||||||
|  | 			}), | ||||||
|  | 			after: t.Optional(t.String({ description: desc.after })), | ||||||
|  | 			preferOriginal: t.Optional( | ||||||
|  | 				t.Boolean({ | ||||||
|  | 					description: desc.preferOriginal, | ||||||
|  | 				}), | ||||||
|  | 			), | ||||||
|  | 		}), | ||||||
|  | 		headers: t.Object({ | ||||||
|  | 			"accept-language": AcceptLanguage({ autoFallback: true }), | ||||||
|  | 		}), | ||||||
|  | 	}) | ||||||
| 	.get( | 	.get( | ||||||
| 		"/:id/movies", | 		"/:id/movies", | ||||||
| 		async ({ | 		async ({ | ||||||
| @ -216,32 +246,6 @@ export const collections = new Elysia({ | |||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			detail: { description: "Get all movies in a collection" }, | 			detail: { description: "Get all movies in a collection" }, | ||||||
| 			params: t.Object({ |  | ||||||
| 				id: t.String({ |  | ||||||
| 					description: "The id or slug of the collection.", |  | ||||||
| 					example: duneCollection.slug, |  | ||||||
| 				}), |  | ||||||
| 			}), |  | ||||||
| 			query: t.Object({ |  | ||||||
| 				sort: showSort, |  | ||||||
| 				filter: t.Optional(Filter({ def: showFilters })), |  | ||||||
| 				query: t.Optional(t.String({ description: desc.query })), |  | ||||||
| 				limit: t.Integer({ |  | ||||||
| 					minimum: 1, |  | ||||||
| 					maximum: 250, |  | ||||||
| 					default: 50, |  | ||||||
| 					description: "Max page size.", |  | ||||||
| 				}), |  | ||||||
| 				after: t.Optional(t.String({ description: desc.after })), |  | ||||||
| 				preferOriginal: t.Optional( |  | ||||||
| 					t.Boolean({ |  | ||||||
| 						description: desc.preferOriginal, |  | ||||||
| 					}), |  | ||||||
| 				), |  | ||||||
| 			}), |  | ||||||
| 			headers: t.Object({ |  | ||||||
| 				"accept-language": AcceptLanguage({ autoFallback: true }), |  | ||||||
| 			}), |  | ||||||
| 			response: { | 			response: { | ||||||
| 				200: Page(Movie), | 				200: Page(Movie), | ||||||
| 				404: { | 				404: { | ||||||
| @ -297,34 +301,8 @@ export const collections = new Elysia({ | |||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			detail: { description: "Get all series in a collection" }, | 			detail: { description: "Get all series in a collection" }, | ||||||
| 			params: t.Object({ |  | ||||||
| 				id: t.String({ |  | ||||||
| 					description: "The id or slug of the collection.", |  | ||||||
| 					example: duneCollection.slug, |  | ||||||
| 				}), |  | ||||||
| 			}), |  | ||||||
| 			query: t.Object({ |  | ||||||
| 				sort: showSort, |  | ||||||
| 				filter: t.Optional(Filter({ def: showFilters })), |  | ||||||
| 				query: t.Optional(t.String({ description: desc.query })), |  | ||||||
| 				limit: t.Integer({ |  | ||||||
| 					minimum: 1, |  | ||||||
| 					maximum: 250, |  | ||||||
| 					default: 50, |  | ||||||
| 					description: "Max page size.", |  | ||||||
| 				}), |  | ||||||
| 				after: t.Optional(t.String({ description: desc.after })), |  | ||||||
| 				preferOriginal: t.Optional( |  | ||||||
| 					t.Boolean({ |  | ||||||
| 						description: desc.preferOriginal, |  | ||||||
| 					}), |  | ||||||
| 				), |  | ||||||
| 			}), |  | ||||||
| 			headers: t.Object({ |  | ||||||
| 				"accept-language": AcceptLanguage({ autoFallback: true }), |  | ||||||
| 			}), |  | ||||||
| 			response: { | 			response: { | ||||||
| 				200: Page(Movie), | 				200: Page(Serie), | ||||||
| 				404: { | 				404: { | ||||||
| 					...KError, | 					...KError, | ||||||
| 					description: "No collection found with the given id or slug.", | 					description: "No collection found with the given id or slug.", | ||||||
| @ -374,34 +352,8 @@ export const collections = new Elysia({ | |||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			detail: { description: "Get all series & movies in a collection" }, | 			detail: { description: "Get all series & movies in a collection" }, | ||||||
| 			params: t.Object({ |  | ||||||
| 				id: t.String({ |  | ||||||
| 					description: "The id or slug of the collection.", |  | ||||||
| 					example: duneCollection.slug, |  | ||||||
| 				}), |  | ||||||
| 			}), |  | ||||||
| 			query: t.Object({ |  | ||||||
| 				sort: showSort, |  | ||||||
| 				filter: t.Optional(Filter({ def: showFilters })), |  | ||||||
| 				query: t.Optional(t.String({ description: desc.query })), |  | ||||||
| 				limit: t.Integer({ |  | ||||||
| 					minimum: 1, |  | ||||||
| 					maximum: 250, |  | ||||||
| 					default: 50, |  | ||||||
| 					description: "Max page size.", |  | ||||||
| 				}), |  | ||||||
| 				after: t.Optional(t.String({ description: desc.after })), |  | ||||||
| 				preferOriginal: t.Optional( |  | ||||||
| 					t.Boolean({ |  | ||||||
| 						description: desc.preferOriginal, |  | ||||||
| 					}), |  | ||||||
| 				), |  | ||||||
| 			}), |  | ||||||
| 			headers: t.Object({ |  | ||||||
| 				"accept-language": AcceptLanguage({ autoFallback: true }), |  | ||||||
| 			}), |  | ||||||
| 			response: { | 			response: { | ||||||
| 				200: Page(Movie), | 				200: Page(Show), | ||||||
| 				404: { | 				404: { | ||||||
| 					...KError, | 					...KError, | ||||||
| 					description: "No collection found with the given id or slug.", | 					description: "No collection found with the given id or slug.", | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| import type { StaticDecode } from "@sinclair/typebox"; | import type { StaticDecode } from "@sinclair/typebox"; | ||||||
| import { type SQL, and, eq, sql } from "drizzle-orm"; | import { type SQL, and, eq, sql } from "drizzle-orm"; | ||||||
| import { db } from "~/db"; | import { db } from "~/db"; | ||||||
| import { showTranslations, shows } from "~/db/schema"; | import { showTranslations, shows, studioTranslations } from "~/db/schema"; | ||||||
| import { getColumns, sqlarr } from "~/db/utils"; | import { getColumns, sqlarr } from "~/db/utils"; | ||||||
| import type { MovieStatus } from "~/models/movie"; | import type { MovieStatus } from "~/models/movie"; | ||||||
| import { SerieStatus } from "~/models/serie"; | import { SerieStatus } from "~/models/serie"; | ||||||
| @ -12,6 +12,7 @@ import { | |||||||
| 	Sort, | 	Sort, | ||||||
| 	isUuid, | 	isUuid, | ||||||
| 	keysetPaginate, | 	keysetPaginate, | ||||||
|  | 	selectTranslationQuery, | ||||||
| 	sortToSql, | 	sortToSql, | ||||||
| } from "~/models/utils"; | } from "~/models/utils"; | ||||||
| 
 | 
 | ||||||
| @ -130,7 +131,7 @@ export async function getShow( | |||||||
| 	}: { | 	}: { | ||||||
| 		languages: string[]; | 		languages: string[]; | ||||||
| 		preferOriginal: boolean | undefined; | 		preferOriginal: boolean | undefined; | ||||||
| 		relations: ("translations" | "videos")[]; | 		relations: ("translations" | "studios" | "videos")[]; | ||||||
| 		filters: SQL | undefined; | 		filters: SQL | undefined; | ||||||
| 	}, | 	}, | ||||||
| ) { | ) { | ||||||
| @ -141,18 +142,7 @@ export async function getShow( | |||||||
| 		}, | 		}, | ||||||
| 		where: and(isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id), filters), | 		where: and(isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id), filters), | ||||||
| 		with: { | 		with: { | ||||||
| 			selectedTranslation: { | 			selectedTranslation: selectTranslationQuery(showTranslations, languages), | ||||||
| 				columns: { |  | ||||||
| 					pk: false, |  | ||||||
| 				}, |  | ||||||
| 				where: !languages.includes("*") |  | ||||||
| 					? eq(showTranslations.language, sql`any(${sqlarr(languages)})`) |  | ||||||
| 					: undefined, |  | ||||||
| 				orderBy: [ |  | ||||||
| 					sql`array_position(${sqlarr(languages)}, ${showTranslations.language})`, |  | ||||||
| 				], |  | ||||||
| 				limit: 1, |  | ||||||
| 			}, |  | ||||||
| 			originalTranslation: { | 			originalTranslation: { | ||||||
| 				columns: { | 				columns: { | ||||||
| 					poster: true, | 					poster: true, | ||||||
| @ -175,6 +165,23 @@ export async function getShow( | |||||||
| 					}, | 					}, | ||||||
| 				}, | 				}, | ||||||
| 			}), | 			}), | ||||||
|  | 			...(relations.includes("studios") && { | ||||||
|  | 				studios: { | ||||||
|  | 					with: { | ||||||
|  | 						studio: { | ||||||
|  | 							columns: { | ||||||
|  | 								pk: false, | ||||||
|  | 							}, | ||||||
|  | 							with: { | ||||||
|  | 								selectedTranslation: selectTranslationQuery( | ||||||
|  | 									studioTranslations, | ||||||
|  | 									languages, | ||||||
|  | 								), | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}, | ||||||
|  | 				}, | ||||||
|  | 			}), | ||||||
| 		}, | 		}, | ||||||
| 	}); | 	}); | ||||||
| 	if (!ret) return null; | 	if (!ret) return null; | ||||||
| @ -184,6 +191,7 @@ export async function getShow( | |||||||
| 	const show = { | 	const show = { | ||||||
| 		...ret, | 		...ret, | ||||||
| 		...translation, | 		...translation, | ||||||
|  | 		kind: ret.kind as any, | ||||||
| 		...(ot?.preferOriginal && { | 		...(ot?.preferOriginal && { | ||||||
| 			...(ot.poster && { poster: ot.poster }), | 			...(ot.poster && { poster: ot.poster }), | ||||||
| 			...(ot.thumbnail && { thumbnail: ot.thumbnail }), | 			...(ot.thumbnail && { thumbnail: ot.thumbnail }), | ||||||
| @ -197,6 +205,12 @@ export async function getShow( | |||||||
| 				), | 				), | ||||||
| 			), | 			), | ||||||
| 		}), | 		}), | ||||||
|  | 		...(ret.studios && { | ||||||
|  | 			studios: ret.studios.map((x: any) => ({ | ||||||
|  | 				...x.studio, | ||||||
|  | 				...x.studio.selectedTranslation[0], | ||||||
|  | 			})), | ||||||
|  | 		}), | ||||||
| 	}; | 	}; | ||||||
| 	return { show, language: translation.language }; | 	return { show, language: translation.language }; | ||||||
| } | } | ||||||
|  | |||||||
| @ -65,7 +65,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) | |||||||
| 				preferOriginal: t.Optional( | 				preferOriginal: t.Optional( | ||||||
| 					t.Boolean({ description: desc.preferOriginal }), | 					t.Boolean({ description: desc.preferOriginal }), | ||||||
| 				), | 				), | ||||||
| 				with: t.Array(t.UnionEnum(["translations", "videos"]), { | 				with: t.Array(t.UnionEnum(["translations", "studios", "videos"]), { | ||||||
| 					default: [], | 					default: [], | ||||||
| 					description: "Include related resources in the response.", | 					description: "Include related resources in the response.", | ||||||
| 				}), | 				}), | ||||||
|  | |||||||
| @ -65,7 +65,7 @@ export const series = new Elysia({ prefix: "/series", tags: ["series"] }) | |||||||
| 				preferOriginal: t.Optional( | 				preferOriginal: t.Optional( | ||||||
| 					t.Boolean({ description: desc.preferOriginal }), | 					t.Boolean({ description: desc.preferOriginal }), | ||||||
| 				), | 				), | ||||||
| 				with: t.Array(t.UnionEnum(["translations"]), { | 				with: t.Array(t.UnionEnum(["translations", "studios"]), { | ||||||
| 					default: [], | 					default: [], | ||||||
| 					description: "Include related resources in the response.", | 					description: "Include related resources in the response.", | ||||||
| 				}), | 				}), | ||||||
|  | |||||||
| @ -2,10 +2,8 @@ import { and, isNull, sql } from "drizzle-orm"; | |||||||
| import { Elysia, t } from "elysia"; | import { Elysia, t } from "elysia"; | ||||||
| import { db } from "~/db"; | import { db } from "~/db"; | ||||||
| import { shows } from "~/db/schema"; | import { shows } from "~/db/schema"; | ||||||
| import { Collection } from "~/models/collections"; |  | ||||||
| import { KError } from "~/models/error"; | import { KError } from "~/models/error"; | ||||||
| import { Movie } from "~/models/movie"; | import { Show } from "~/models/show"; | ||||||
| import { Serie } from "~/models/serie"; |  | ||||||
| import { | import { | ||||||
| 	AcceptLanguage, | 	AcceptLanguage, | ||||||
| 	Filter, | 	Filter, | ||||||
| @ -16,8 +14,6 @@ import { | |||||||
| import { desc } from "~/models/utils/descriptions"; | import { desc } from "~/models/utils/descriptions"; | ||||||
| import { getShows, showFilters, showSort } from "./logic"; | import { getShows, showFilters, showSort } from "./logic"; | ||||||
| 
 | 
 | ||||||
| const Show = t.Union([Movie, Serie, Collection]); |  | ||||||
| 
 |  | ||||||
| export const showsH = new Elysia({ prefix: "/shows", tags: ["shows"] }) | export const showsH = new Elysia({ prefix: "/shows", tags: ["shows"] }) | ||||||
| 	.model({ | 	.model({ | ||||||
| 		show: Show, | 		show: Show, | ||||||
|  | |||||||
							
								
								
									
										412
									
								
								api/src/controllers/studios.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										412
									
								
								api/src/controllers/studios.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,412 @@ | |||||||
|  | import { and, eq, exists, sql } from "drizzle-orm"; | ||||||
|  | import Elysia, { t } from "elysia"; | ||||||
|  | import { db } from "~/db"; | ||||||
|  | import { | ||||||
|  | 	showStudioJoin, | ||||||
|  | 	shows, | ||||||
|  | 	studioTranslations, | ||||||
|  | 	studios, | ||||||
|  | } from "~/db/schema"; | ||||||
|  | import { getColumns, sqlarr } from "~/db/utils"; | ||||||
|  | import { KError } from "~/models/error"; | ||||||
|  | import { Movie } from "~/models/movie"; | ||||||
|  | import { Serie } from "~/models/serie"; | ||||||
|  | import { Show } from "~/models/show"; | ||||||
|  | import { Studio, StudioTranslation } from "~/models/studio"; | ||||||
|  | import { | ||||||
|  | 	AcceptLanguage, | ||||||
|  | 	Filter, | ||||||
|  | 	Page, | ||||||
|  | 	Sort, | ||||||
|  | 	createPage, | ||||||
|  | 	isUuid, | ||||||
|  | 	keysetPaginate, | ||||||
|  | 	processLanguages, | ||||||
|  | 	selectTranslationQuery, | ||||||
|  | 	sortToSql, | ||||||
|  | } from "~/models/utils"; | ||||||
|  | import { desc } from "~/models/utils/descriptions"; | ||||||
|  | import { getShows, showFilters, showSort } from "./shows/logic"; | ||||||
|  | 
 | ||||||
|  | const studioSort = Sort(["slug", "createdAt"], { default: ["slug"] }); | ||||||
|  | 
 | ||||||
|  | export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] }) | ||||||
|  | 	.model({ | ||||||
|  | 		studio: Studio, | ||||||
|  | 		"studio-translation": StudioTranslation, | ||||||
|  | 	}) | ||||||
|  | 	.get( | ||||||
|  | 		"/:id", | ||||||
|  | 		async ({ | ||||||
|  | 			params: { id }, | ||||||
|  | 			headers: { "accept-language": languages }, | ||||||
|  | 			query: { with: relations }, | ||||||
|  | 			error, | ||||||
|  | 			set, | ||||||
|  | 		}) => { | ||||||
|  | 			const langs = processLanguages(languages); | ||||||
|  | 			const ret = await db.query.studios.findFirst({ | ||||||
|  | 				where: isUuid(id) ? eq(studios.id, id) : eq(studios.slug, id), | ||||||
|  | 				with: { | ||||||
|  | 					selectedTranslation: selectTranslationQuery( | ||||||
|  | 						studioTranslations, | ||||||
|  | 						langs, | ||||||
|  | 					), | ||||||
|  | 					...(relations.includes("translations") && { | ||||||
|  | 						translations: { | ||||||
|  | 							columns: { | ||||||
|  | 								pk: false, | ||||||
|  | 							}, | ||||||
|  | 						}, | ||||||
|  | 					}), | ||||||
|  | 				}, | ||||||
|  | 			}); | ||||||
|  | 			if (!ret) { | ||||||
|  | 				return error(404, { | ||||||
|  | 					status: 404, | ||||||
|  | 					message: `No studio with the id or slug: '${id}'`, | ||||||
|  | 				}); | ||||||
|  | 			} | ||||||
|  | 			const tr = ret.selectedTranslation[0]; | ||||||
|  | 			set.headers["content-language"] = tr.language; | ||||||
|  | 			return { | ||||||
|  | 				...ret, | ||||||
|  | 				...tr, | ||||||
|  | 				...(ret.translations && { | ||||||
|  | 					translations: Object.fromEntries( | ||||||
|  | 						ret.translations.map( | ||||||
|  | 							({ language, ...translation }) => | ||||||
|  | 								[language, translation] as const, | ||||||
|  | 						), | ||||||
|  | 					), | ||||||
|  | 				}), | ||||||
|  | 			}; | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			detail: { | ||||||
|  | 				description: "Get a studio by id or slug", | ||||||
|  | 			}, | ||||||
|  | 			params: t.Object({ | ||||||
|  | 				id: t.String({ | ||||||
|  | 					description: "The id or slug of the collection to retrieve.", | ||||||
|  | 					example: "mappa", | ||||||
|  | 				}), | ||||||
|  | 			}), | ||||||
|  | 			query: t.Object({ | ||||||
|  | 				with: t.Array(t.UnionEnum(["translations"]), { | ||||||
|  | 					default: [], | ||||||
|  | 					description: "Include related resources in the response.", | ||||||
|  | 				}), | ||||||
|  | 			}), | ||||||
|  | 			headers: t.Object({ | ||||||
|  | 				"accept-language": AcceptLanguage(), | ||||||
|  | 			}), | ||||||
|  | 			response: { | ||||||
|  | 				200: "studio", | ||||||
|  | 				404: { | ||||||
|  | 					...KError, | ||||||
|  | 					description: "No collection found with the given id or slug.", | ||||||
|  | 				}, | ||||||
|  | 				422: KError, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	.get( | ||||||
|  | 		"random", | ||||||
|  | 		async ({ error, redirect }) => { | ||||||
|  | 			const [studio] = await db | ||||||
|  | 				.select({ slug: studios.slug }) | ||||||
|  | 				.from(studios) | ||||||
|  | 				.orderBy(sql`random()`) | ||||||
|  | 				.limit(1); | ||||||
|  | 			if (!studio) | ||||||
|  | 				return error(404, { | ||||||
|  | 					status: 404, | ||||||
|  | 					message: "No studios in the database.", | ||||||
|  | 				}); | ||||||
|  | 			return redirect(`/studios/${studio.slug}`); | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			detail: { | ||||||
|  | 				description: "Get a random studio.", | ||||||
|  | 			}, | ||||||
|  | 			response: { | ||||||
|  | 				302: t.Void({ | ||||||
|  | 					description: | ||||||
|  | 						"Redirected to the [/studios/{id}](#tag/studios/GET/studios/{id}) route.", | ||||||
|  | 				}), | ||||||
|  | 				404: { | ||||||
|  | 					...KError, | ||||||
|  | 					description: "No studios in the database.", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	.get( | ||||||
|  | 		"", | ||||||
|  | 		async ({ | ||||||
|  | 			query: { limit, after, query, sort }, | ||||||
|  | 			headers: { "accept-language": languages }, | ||||||
|  | 			request: { url }, | ||||||
|  | 		}) => { | ||||||
|  | 			const langs = processLanguages(languages); | ||||||
|  | 			const transQ = db | ||||||
|  | 				.selectDistinctOn([studioTranslations.pk]) | ||||||
|  | 				.from(studioTranslations) | ||||||
|  | 				.orderBy( | ||||||
|  | 					studioTranslations.pk, | ||||||
|  | 					sql`array_position(${sqlarr(langs)}, ${studioTranslations.language}`, | ||||||
|  | 				) | ||||||
|  | 				.as("t"); | ||||||
|  | 			const { pk, ...transCol } = getColumns(transQ); | ||||||
|  | 
 | ||||||
|  | 			const items = await db | ||||||
|  | 				.select({ | ||||||
|  | 					...getColumns(studios), | ||||||
|  | 					...transCol, | ||||||
|  | 				}) | ||||||
|  | 				.from(studios) | ||||||
|  | 				.where( | ||||||
|  | 					and( | ||||||
|  | 						query ? sql`${transQ.name} %> ${query}::text` : undefined, | ||||||
|  | 						keysetPaginate({ table: studios, after, sort }), | ||||||
|  | 					), | ||||||
|  | 				) | ||||||
|  | 				.orderBy( | ||||||
|  | 					...(query | ||||||
|  | 						? [sql`word_similarity(${query}::text, ${transQ.name})`] | ||||||
|  | 						: sortToSql(sort, studios)), | ||||||
|  | 					studios.pk, | ||||||
|  | 				) | ||||||
|  | 				.limit(limit); | ||||||
|  | 			return createPage(items, { url, sort, limit }); | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			detail: { description: "Get all studios" }, | ||||||
|  | 			query: t.Object({ | ||||||
|  | 				sort: studioSort, | ||||||
|  | 				query: t.Optional(t.String({ description: desc.query })), | ||||||
|  | 				limit: t.Integer({ | ||||||
|  | 					minimum: 1, | ||||||
|  | 					maximum: 250, | ||||||
|  | 					default: 50, | ||||||
|  | 					description: "Max page size.", | ||||||
|  | 				}), | ||||||
|  | 				after: t.Optional(t.String({ description: desc.after })), | ||||||
|  | 			}), | ||||||
|  | 			headers: t.Object({ | ||||||
|  | 				"accept-language": AcceptLanguage({ autoFallback: true }), | ||||||
|  | 			}), | ||||||
|  | 			response: { | ||||||
|  | 				200: Page(Studio), | ||||||
|  | 				422: KError, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	.guard({ | ||||||
|  | 		params: t.Object({ | ||||||
|  | 			id: t.String({ | ||||||
|  | 				description: "The id or slug of the studio.", | ||||||
|  | 				example: "mappa", | ||||||
|  | 			}), | ||||||
|  | 		}), | ||||||
|  | 		query: t.Object({ | ||||||
|  | 			sort: showSort, | ||||||
|  | 			filter: t.Optional(Filter({ def: showFilters })), | ||||||
|  | 			query: t.Optional(t.String({ description: desc.query })), | ||||||
|  | 			limit: t.Integer({ | ||||||
|  | 				minimum: 1, | ||||||
|  | 				maximum: 250, | ||||||
|  | 				default: 50, | ||||||
|  | 				description: "Max page size.", | ||||||
|  | 			}), | ||||||
|  | 			after: t.Optional(t.String({ description: desc.after })), | ||||||
|  | 			preferOriginal: t.Optional( | ||||||
|  | 				t.Boolean({ | ||||||
|  | 					description: desc.preferOriginal, | ||||||
|  | 				}), | ||||||
|  | 			), | ||||||
|  | 		}), | ||||||
|  | 		headers: t.Object({ | ||||||
|  | 			"accept-language": AcceptLanguage({ autoFallback: true }), | ||||||
|  | 		}), | ||||||
|  | 	}) | ||||||
|  | 	.get( | ||||||
|  | 		"/:id/shows", | ||||||
|  | 		async ({ | ||||||
|  | 			params: { id }, | ||||||
|  | 			query: { limit, after, query, sort, filter, preferOriginal }, | ||||||
|  | 			headers: { "accept-language": languages }, | ||||||
|  | 			request: { url }, | ||||||
|  | 			error, | ||||||
|  | 		}) => { | ||||||
|  | 			const [studio] = await db | ||||||
|  | 				.select({ pk: studios.pk }) | ||||||
|  | 				.from(studios) | ||||||
|  | 				.where(isUuid(id) ? eq(studios.id, id) : eq(studios.slug, id)) | ||||||
|  | 				.limit(1); | ||||||
|  | 
 | ||||||
|  | 			if (!studio) { | ||||||
|  | 				return error(404, { | ||||||
|  | 					status: 404, | ||||||
|  | 					message: `No studios with the id or slug: '${id}'.`, | ||||||
|  | 				}); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			const langs = processLanguages(languages); | ||||||
|  | 			const items = await getShows({ | ||||||
|  | 				limit, | ||||||
|  | 				after, | ||||||
|  | 				query, | ||||||
|  | 				sort, | ||||||
|  | 				filter: and( | ||||||
|  | 					exists( | ||||||
|  | 						db | ||||||
|  | 							.select() | ||||||
|  | 							.from(showStudioJoin) | ||||||
|  | 							.where( | ||||||
|  | 								and( | ||||||
|  | 									eq(showStudioJoin.studioPk, studio.pk), | ||||||
|  | 									eq(showStudioJoin.showPk, shows.pk), | ||||||
|  | 								), | ||||||
|  | 							), | ||||||
|  | 					), | ||||||
|  | 					filter, | ||||||
|  | 				), | ||||||
|  | 				languages: langs, | ||||||
|  | 				preferOriginal, | ||||||
|  | 			}); | ||||||
|  | 			return createPage(items, { url, sort, limit }); | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			detail: { description: "Get all series & movies made by a studio." }, | ||||||
|  | 			response: { | ||||||
|  | 				200: Page(Show), | ||||||
|  | 				404: { | ||||||
|  | 					...KError, | ||||||
|  | 					description: "No collection found with the given id or slug.", | ||||||
|  | 				}, | ||||||
|  | 				422: KError, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	.get( | ||||||
|  | 		"/:id/movies", | ||||||
|  | 		async ({ | ||||||
|  | 			params: { id }, | ||||||
|  | 			query: { limit, after, query, sort, filter, preferOriginal }, | ||||||
|  | 			headers: { "accept-language": languages }, | ||||||
|  | 			request: { url }, | ||||||
|  | 			error, | ||||||
|  | 		}) => { | ||||||
|  | 			const [studio] = await db | ||||||
|  | 				.select({ pk: studios.pk }) | ||||||
|  | 				.from(studios) | ||||||
|  | 				.where(isUuid(id) ? eq(studios.id, id) : eq(studios.slug, id)) | ||||||
|  | 				.limit(1); | ||||||
|  | 
 | ||||||
|  | 			if (!studio) { | ||||||
|  | 				return error(404, { | ||||||
|  | 					status: 404, | ||||||
|  | 					message: `No studios with the id or slug: '${id}'.`, | ||||||
|  | 				}); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			const langs = processLanguages(languages); | ||||||
|  | 			const items = await getShows({ | ||||||
|  | 				limit, | ||||||
|  | 				after, | ||||||
|  | 				query, | ||||||
|  | 				sort, | ||||||
|  | 				filter: and( | ||||||
|  | 					eq(shows.kind, "movie"), | ||||||
|  | 					exists( | ||||||
|  | 						db | ||||||
|  | 							.select() | ||||||
|  | 							.from(showStudioJoin) | ||||||
|  | 							.where( | ||||||
|  | 								and( | ||||||
|  | 									eq(showStudioJoin.studioPk, studio.pk), | ||||||
|  | 									eq(showStudioJoin.showPk, shows.pk), | ||||||
|  | 								), | ||||||
|  | 							), | ||||||
|  | 					), | ||||||
|  | 					filter, | ||||||
|  | 				), | ||||||
|  | 				languages: langs, | ||||||
|  | 				preferOriginal, | ||||||
|  | 			}); | ||||||
|  | 			return createPage(items, { url, sort, limit }); | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			detail: { description: "Get all movies made by a studio." }, | ||||||
|  | 			response: { | ||||||
|  | 				200: Page(Movie), | ||||||
|  | 				404: { | ||||||
|  | 					...KError, | ||||||
|  | 					description: "No collection found with the given id or slug.", | ||||||
|  | 				}, | ||||||
|  | 				422: KError, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 	.get( | ||||||
|  | 		"/:id/series", | ||||||
|  | 		async ({ | ||||||
|  | 			params: { id }, | ||||||
|  | 			query: { limit, after, query, sort, filter, preferOriginal }, | ||||||
|  | 			headers: { "accept-language": languages }, | ||||||
|  | 			request: { url }, | ||||||
|  | 			error, | ||||||
|  | 		}) => { | ||||||
|  | 			const [studio] = await db | ||||||
|  | 				.select({ pk: studios.pk }) | ||||||
|  | 				.from(studios) | ||||||
|  | 				.where(isUuid(id) ? eq(studios.id, id) : eq(studios.slug, id)) | ||||||
|  | 				.limit(1); | ||||||
|  | 
 | ||||||
|  | 			if (!studio) { | ||||||
|  | 				return error(404, { | ||||||
|  | 					status: 404, | ||||||
|  | 					message: `No studios with the id or slug: '${id}'.`, | ||||||
|  | 				}); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			const langs = processLanguages(languages); | ||||||
|  | 			const items = await getShows({ | ||||||
|  | 				limit, | ||||||
|  | 				after, | ||||||
|  | 				query, | ||||||
|  | 				sort, | ||||||
|  | 				filter: and( | ||||||
|  | 					eq(shows.kind, "serie"), | ||||||
|  | 					exists( | ||||||
|  | 						db | ||||||
|  | 							.select() | ||||||
|  | 							.from(showStudioJoin) | ||||||
|  | 							.where( | ||||||
|  | 								and( | ||||||
|  | 									eq(showStudioJoin.studioPk, studio.pk), | ||||||
|  | 									eq(showStudioJoin.showPk, shows.pk), | ||||||
|  | 								), | ||||||
|  | 							), | ||||||
|  | 					), | ||||||
|  | 					filter, | ||||||
|  | 				), | ||||||
|  | 				languages: langs, | ||||||
|  | 				preferOriginal, | ||||||
|  | 			}); | ||||||
|  | 			return createPage(items, { url, sort, limit }); | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			detail: { description: "Get all series made by a studio." }, | ||||||
|  | 			response: { | ||||||
|  | 				200: Page(Serie), | ||||||
|  | 				404: { | ||||||
|  | 					...KError, | ||||||
|  | 					description: "No collection found with the given id or slug.", | ||||||
|  | 				}, | ||||||
|  | 				422: KError, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	); | ||||||
| @ -42,78 +42,77 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] }) | |||||||
| 			return error(201, oldRet); | 			return error(201, oldRet); | ||||||
| 
 | 
 | ||||||
| 			// TODO: this is a huge untested wip
 | 			// TODO: this is a huge untested wip
 | ||||||
| 			// biome-ignore lint/correctness/noUnreachable: leave me alone
 | 			// const vidsI = db.$with("vidsI").as(
 | ||||||
| 			const vidsI = db.$with("vidsI").as( | 			// 	db.insert(videos).values(body).onConflictDoNothing().returning({
 | ||||||
| 				db.insert(videos).values(body).onConflictDoNothing().returning({ | 			// 		pk: videos.pk,
 | ||||||
| 					pk: videos.pk, | 			// 		id: videos.id,
 | ||||||
| 					id: videos.id, | 			// 		path: videos.path,
 | ||||||
| 					path: videos.path, | 			// 		guess: videos.guess,
 | ||||||
| 					guess: videos.guess, | 			// 	}),
 | ||||||
| 				}), | 			// );
 | ||||||
| 			); | 			//
 | ||||||
| 
 | 			// const findEntriesQ = db
 | ||||||
| 			const findEntriesQ = db | 			// 	.select({
 | ||||||
| 				.select({ | 			// 		guess: videos.guess,
 | ||||||
| 					guess: videos.guess, | 			// 		entryPk: entries.pk,
 | ||||||
| 					entryPk: entries.pk, | 			// 		showSlug: shows.slug,
 | ||||||
| 					showSlug: shows.slug, | 			// 		// TODO: handle extras here
 | ||||||
| 					// TODO: handle extras here
 | 			// 		// guessit can't know if an episode is a special or not. treat specials like a normal episode.
 | ||||||
| 					// guessit can't know if an episode is a special or not. treat specials like a normal episode.
 | 			// 		kind: sql`
 | ||||||
| 					kind: sql` | 			// 			case when ${entries.kind} = 'movie' then 'movie' else 'episode' end
 | ||||||
| 						case when ${entries.kind} = 'movie' then 'movie' else 'episode' end | 			// 		`.as("kind"),
 | ||||||
| 					`.as("kind"),
 | 			// 		season: entries.seasonNumber,
 | ||||||
| 					season: entries.seasonNumber, | 			// 		episode: entries.episodeNumber,
 | ||||||
| 					episode: entries.episodeNumber, | 			// 	})
 | ||||||
| 				}) | 			// 	.from(entries)
 | ||||||
| 				.from(entries) | 			// 	.leftJoin(entryVideoJoin, eq(entryVideoJoin.entry, entries.pk))
 | ||||||
| 				.leftJoin(entryVideoJoin, eq(entryVideoJoin.entry, entries.pk)) | 			// 	.leftJoin(videos, eq(videos.pk, entryVideoJoin.video))
 | ||||||
| 				.leftJoin(videos, eq(videos.pk, entryVideoJoin.video)) | 			// 	.leftJoin(shows, eq(shows.pk, entries.showPk))
 | ||||||
| 				.leftJoin(shows, eq(shows.pk, entries.showPk)) | 			// 	.as("find_entries");
 | ||||||
| 				.as("find_entries"); | 			//
 | ||||||
| 
 | 			// const hasRenderingQ = db
 | ||||||
| 			const hasRenderingQ = db | 			// 	.select()
 | ||||||
| 				.select() | 			// 	.from(entryVideoJoin)
 | ||||||
| 				.from(entryVideoJoin) | 			// 	.where(eq(entryVideoJoin.entry, findEntriesQ.entryPk));
 | ||||||
| 				.where(eq(entryVideoJoin.entry, findEntriesQ.entryPk)); | 			//
 | ||||||
| 
 | 			// const ret = await db
 | ||||||
| 			const ret = await db | 			// 	.with(vidsI)
 | ||||||
| 				.with(vidsI) | 			// 	.insert(entryVideoJoin)
 | ||||||
| 				.insert(entryVideoJoin) | 			// 	.select(
 | ||||||
| 				.select( | 			// 		db
 | ||||||
| 					db | 			// 			.select({
 | ||||||
| 						.select({ | 			// 				entry: findEntriesQ.entryPk,
 | ||||||
| 							entry: findEntriesQ.entryPk, | 			// 				video: vidsI.pk,
 | ||||||
| 							video: vidsI.pk, | 			// 				slug: computeVideoSlug(
 | ||||||
| 							slug: computeVideoSlug( | 			// 					findEntriesQ.showSlug,
 | ||||||
| 								findEntriesQ.showSlug, | 			// 					sql`exists(${hasRenderingQ})`,
 | ||||||
| 								sql`exists(${hasRenderingQ})`, | 			// 				),
 | ||||||
| 							), | 			// 			})
 | ||||||
| 						}) | 			// 			.from(vidsI)
 | ||||||
| 						.from(vidsI) | 			// 			.leftJoin(
 | ||||||
| 						.leftJoin( | 			// 				findEntriesQ,
 | ||||||
| 							findEntriesQ, | 			// 				and(
 | ||||||
| 							and( | 			// 					eq(
 | ||||||
| 								eq( | 			// 						sql`${findEntriesQ.guess}->'title'`,
 | ||||||
| 									sql`${findEntriesQ.guess}->'title'`, | 			// 						sql`${vidsI.guess}->'title'`,
 | ||||||
| 									sql`${vidsI.guess}->'title'`, | 			// 					),
 | ||||||
| 								), | 			// 					// TODO: find if @> with a jsonb created on the fly is
 | ||||||
| 								// TODO: find if @> with a jsonb created on the fly is
 | 			// 					// better than multiples checks
 | ||||||
| 								// better than multiples checks
 | 			// 					sql`${vidsI.guess} @> {"kind": }::jsonb`,
 | ||||||
| 								sql`${vidsI.guess} @> {"kind": }::jsonb`, | 			// 					inArray(findEntriesQ.kind, sql`${vidsI.guess}->'type'`),
 | ||||||
| 								inArray(findEntriesQ.kind, sql`${vidsI.guess}->'type'`), | 			// 					inArray(findEntriesQ.episode, sql`${vidsI.guess}->'episode'`),
 | ||||||
| 								inArray(findEntriesQ.episode, sql`${vidsI.guess}->'episode'`), | 			// 					inArray(findEntriesQ.season, sql`${vidsI.guess}->'season'`),
 | ||||||
| 								inArray(findEntriesQ.season, sql`${vidsI.guess}->'season'`), | 			// 				),
 | ||||||
| 							), | 			// 			),
 | ||||||
| 						), | 			// 	)
 | ||||||
| 				) | 			// 	.onConflictDoNothing()
 | ||||||
| 				.onConflictDoNothing() | 			// 	.returning({
 | ||||||
| 				.returning({ | 			// 		slug: entryVideoJoin.slug,
 | ||||||
| 					slug: entryVideoJoin.slug, | 			// 		entryPk: entryVideoJoin.entry,
 | ||||||
| 					entryPk: entryVideoJoin.entry, | 			// 		id: vidsI.id,
 | ||||||
| 					id: vidsI.id, | 			// 		path: vidsI.path,
 | ||||||
| 					path: vidsI.path, | 			// 	});
 | ||||||
| 				}); | 			// return error(201, ret as any);
 | ||||||
| 			return error(201, ret as any); |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			body: t.Array(SeedVideo), | 			body: t.Array(SeedVideo), | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ import { relations, sql } from "drizzle-orm"; | |||||||
| import { | import { | ||||||
| 	check, | 	check, | ||||||
| 	date, | 	date, | ||||||
|  | 	index, | ||||||
| 	integer, | 	integer, | ||||||
| 	jsonb, | 	jsonb, | ||||||
| 	primaryKey, | 	primaryKey, | ||||||
| @ -70,11 +71,17 @@ export const entries = schema.table( | |||||||
| 		createdAt: timestamp({ withTimezone: true, mode: "string" }) | 		createdAt: timestamp({ withTimezone: true, mode: "string" }) | ||||||
| 			.notNull() | 			.notNull() | ||||||
| 			.defaultNow(), | 			.defaultNow(), | ||||||
|  | 		updatedAt: timestamp({ withTimezone: true, mode: "string" }) | ||||||
|  | 			.notNull() | ||||||
|  | 			.$onUpdate(() => sql`now()`), | ||||||
| 		nextRefresh: timestamp({ withTimezone: true, mode: "string" }).notNull(), | 		nextRefresh: timestamp({ withTimezone: true, mode: "string" }).notNull(), | ||||||
| 	}, | 	}, | ||||||
| 	(t) => [ | 	(t) => [ | ||||||
| 		unique().on(t.showPk, t.seasonNumber, t.episodeNumber), | 		unique().on(t.showPk, t.seasonNumber, t.episodeNumber), | ||||||
| 		check("order_positive", sql`${t.order} >= 0`), | 		check("order_positive", sql`${t.order} >= 0`), | ||||||
|  | 
 | ||||||
|  | 		index("entry_kind").using("hash", t.kind), | ||||||
|  | 		index("entry_order").on(t.order), | ||||||
| 	], | 	], | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| @ -91,7 +98,10 @@ export const entryTranslations = schema.table( | |||||||
| 		tagline: text(), | 		tagline: text(), | ||||||
| 		poster: image(), | 		poster: image(), | ||||||
| 	}, | 	}, | ||||||
| 	(t) => [primaryKey({ columns: [t.pk, t.language] })], | 	(t) => [ | ||||||
|  | 		primaryKey({ columns: [t.pk, t.language] }), | ||||||
|  | 		index("entry_name_trgm").using("gin", sql`${t.name} gin_trgm_ops`), | ||||||
|  | 	], | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| export const entryRelations = relations(entries, ({ one, many }) => ({ | export const entryRelations = relations(entries, ({ one, many }) => ({ | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| export * from "./entries"; | export * from "./entries"; | ||||||
| export * from "./seasons"; | export * from "./seasons"; | ||||||
| export * from "./shows"; | export * from "./shows"; | ||||||
|  | export * from "./studios"; | ||||||
| export * from "./videos"; | export * from "./videos"; | ||||||
|  | |||||||
| @ -1,4 +1,4 @@ | |||||||
| import { relations } from "drizzle-orm"; | import { relations, sql } from "drizzle-orm"; | ||||||
| import { | import { | ||||||
| 	date, | 	date, | ||||||
| 	index, | 	index, | ||||||
| @ -45,11 +45,15 @@ export const seasons = schema.table( | |||||||
| 		createdAt: timestamp({ withTimezone: true, mode: "string" }) | 		createdAt: timestamp({ withTimezone: true, mode: "string" }) | ||||||
| 			.notNull() | 			.notNull() | ||||||
| 			.defaultNow(), | 			.defaultNow(), | ||||||
|  | 		updatedAt: timestamp({ withTimezone: true, mode: "string" }) | ||||||
|  | 			.notNull() | ||||||
|  | 			.$onUpdate(() => sql`now()`), | ||||||
| 		nextRefresh: timestamp({ withTimezone: true, mode: "string" }).notNull(), | 		nextRefresh: timestamp({ withTimezone: true, mode: "string" }).notNull(), | ||||||
| 	}, | 	}, | ||||||
| 	(t) => [ | 	(t) => [ | ||||||
| 		unique().on(t.showPk, t.seasonNumber), | 		unique().on(t.showPk, t.seasonNumber), | ||||||
| 		index("show_fk").using("hash", t.showPk), | 		index("show_fk").using("hash", t.showPk), | ||||||
|  | 		index("season_nbr").on(t.seasonNumber), | ||||||
| 	], | 	], | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| @ -66,7 +70,10 @@ export const seasonTranslations = schema.table( | |||||||
| 		thumbnail: image(), | 		thumbnail: image(), | ||||||
| 		banner: image(), | 		banner: image(), | ||||||
| 	}, | 	}, | ||||||
| 	(t) => [primaryKey({ columns: [t.pk, t.language] })], | 	(t) => [ | ||||||
|  | 		primaryKey({ columns: [t.pk, t.language] }), | ||||||
|  | 		index("season_name_trgm").using("gin", sql`${t.name} gin_trgm_ops`), | ||||||
|  | 	], | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| export const seasonRelations = relations(seasons, ({ one, many }) => ({ | export const seasonRelations = relations(seasons, ({ one, many }) => ({ | ||||||
|  | |||||||
| @ -5,7 +5,6 @@ import { | |||||||
| 	date, | 	date, | ||||||
| 	index, | 	index, | ||||||
| 	integer, | 	integer, | ||||||
| 	jsonb, |  | ||||||
| 	primaryKey, | 	primaryKey, | ||||||
| 	smallint, | 	smallint, | ||||||
| 	text, | 	text, | ||||||
| @ -15,7 +14,8 @@ import { | |||||||
| } from "drizzle-orm/pg-core"; | } from "drizzle-orm/pg-core"; | ||||||
| import { entries } from "./entries"; | import { entries } from "./entries"; | ||||||
| import { seasons } from "./seasons"; | import { seasons } from "./seasons"; | ||||||
| import { image, language, schema } from "./utils"; | import { showStudioJoin } from "./studios"; | ||||||
|  | import { externalid, image, language, schema } from "./utils"; | ||||||
| 
 | 
 | ||||||
| export const showKind = schema.enum("show_kind", [ | export const showKind = schema.enum("show_kind", [ | ||||||
| 	"serie", | 	"serie", | ||||||
| @ -54,20 +54,6 @@ export const genres = schema.enum("genres", [ | |||||||
| 	"talk", | 	"talk", | ||||||
| ]); | ]); | ||||||
| 
 | 
 | ||||||
| export const externalid = () => |  | ||||||
| 	jsonb() |  | ||||||
| 		.$type< |  | ||||||
| 			Record< |  | ||||||
| 				string, |  | ||||||
| 				{ |  | ||||||
| 					dataId: string; |  | ||||||
| 					link: string | null; |  | ||||||
| 				} |  | ||||||
| 			> |  | ||||||
| 		>() |  | ||||||
| 		.notNull() |  | ||||||
| 		.default({}); |  | ||||||
| 
 |  | ||||||
| export const shows = schema.table( | export const shows = schema.table( | ||||||
| 	"shows", | 	"shows", | ||||||
| 	{ | 	{ | ||||||
| @ -92,6 +78,9 @@ export const shows = schema.table( | |||||||
| 		createdAt: timestamp({ withTimezone: true, mode: "string" }) | 		createdAt: timestamp({ withTimezone: true, mode: "string" }) | ||||||
| 			.notNull() | 			.notNull() | ||||||
| 			.defaultNow(), | 			.defaultNow(), | ||||||
|  | 		updatedAt: timestamp({ withTimezone: true, mode: "string" }) | ||||||
|  | 			.notNull() | ||||||
|  | 			.$onUpdate(() => sql`now()`), | ||||||
| 		nextRefresh: timestamp({ withTimezone: true, mode: "string" }).notNull(), | 		nextRefresh: timestamp({ withTimezone: true, mode: "string" }).notNull(), | ||||||
| 	}, | 	}, | ||||||
| 	(t) => [ | 	(t) => [ | ||||||
| @ -141,6 +130,7 @@ export const showsRelations = relations(shows, ({ many, one }) => ({ | |||||||
| 	}), | 	}), | ||||||
| 	entries: many(entries, { relationName: "show_entries" }), | 	entries: many(entries, { relationName: "show_entries" }), | ||||||
| 	seasons: many(seasons, { relationName: "show_seasons" }), | 	seasons: many(seasons, { relationName: "show_seasons" }), | ||||||
|  | 	studios: many(showStudioJoin, { relationName: "ssj_show" }), | ||||||
| })); | })); | ||||||
| export const showsTrRelations = relations(showTranslations, ({ one }) => ({ | export const showsTrRelations = relations(showTranslations, ({ one }) => ({ | ||||||
| 	show: one(shows, { | 	show: one(shows, { | ||||||
|  | |||||||
							
								
								
									
										89
									
								
								api/src/db/schema/studios.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								api/src/db/schema/studios.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,89 @@ | |||||||
|  | import { relations, sql } from "drizzle-orm"; | ||||||
|  | import { | ||||||
|  | 	index, | ||||||
|  | 	integer, | ||||||
|  | 	primaryKey, | ||||||
|  | 	text, | ||||||
|  | 	timestamp, | ||||||
|  | 	uuid, | ||||||
|  | 	varchar, | ||||||
|  | } from "drizzle-orm/pg-core"; | ||||||
|  | import { shows } from "./shows"; | ||||||
|  | import { externalid, image, language, schema } from "./utils"; | ||||||
|  | 
 | ||||||
|  | export const studios = schema.table("studios", { | ||||||
|  | 	pk: integer().primaryKey().generatedAlwaysAsIdentity(), | ||||||
|  | 	id: uuid().notNull().unique().defaultRandom(), | ||||||
|  | 	slug: varchar({ length: 255 }).notNull().unique(), | ||||||
|  | 	externalId: externalid(), | ||||||
|  | 
 | ||||||
|  | 	createdAt: timestamp({ withTimezone: true, mode: "string" }) | ||||||
|  | 		.notNull() | ||||||
|  | 		.defaultNow(), | ||||||
|  | 	updatedAt: timestamp({ withTimezone: true, mode: "string" }) | ||||||
|  | 		.notNull() | ||||||
|  | 		.$onUpdate(() => sql`now()`), | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | export const studioTranslations = schema.table( | ||||||
|  | 	"studio_translations", | ||||||
|  | 	{ | ||||||
|  | 		pk: integer() | ||||||
|  | 			.notNull() | ||||||
|  | 			.references(() => studios.pk, { onDelete: "cascade" }), | ||||||
|  | 		language: language().notNull(), | ||||||
|  | 		name: text().notNull(), | ||||||
|  | 		logo: image(), | ||||||
|  | 	}, | ||||||
|  | 	(t) => [ | ||||||
|  | 		primaryKey({ columns: [t.pk, t.language] }), | ||||||
|  | 		index("studio_name_trgm").using("gin", sql`${t.name} gin_trgm_ops`), | ||||||
|  | 	], | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export const showStudioJoin = schema.table( | ||||||
|  | 	"show_studio_join", | ||||||
|  | 	{ | ||||||
|  | 		showPk: integer() | ||||||
|  | 			.notNull() | ||||||
|  | 			.references(() => shows.pk, { onDelete: "cascade" }), | ||||||
|  | 		studioPk: integer() | ||||||
|  | 			.notNull() | ||||||
|  | 			.references(() => studios.pk, { onDelete: "cascade" }), | ||||||
|  | 	}, | ||||||
|  | 	(t) => [primaryKey({ columns: [t.showPk, t.studioPk] })], | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | export const studioRelations = relations(studios, ({ many }) => ({ | ||||||
|  | 	translations: many(studioTranslations, { | ||||||
|  | 		relationName: "studio_translations", | ||||||
|  | 	}), | ||||||
|  | 	selectedTranslation: many(studioTranslations, { | ||||||
|  | 		relationName: "studio_selected_translation", | ||||||
|  | 	}), | ||||||
|  | 	showsJoin: many(showStudioJoin, { relationName: "ssj_studio" }), | ||||||
|  | })); | ||||||
|  | export const studioTrRelations = relations(studioTranslations, ({ one }) => ({ | ||||||
|  | 	studio: one(studios, { | ||||||
|  | 		relationName: "studio_translations", | ||||||
|  | 		fields: [studioTranslations.pk], | ||||||
|  | 		references: [studios.pk], | ||||||
|  | 	}), | ||||||
|  | 	selectedTranslation: one(studios, { | ||||||
|  | 		relationName: "studio_selected_translation", | ||||||
|  | 		fields: [studioTranslations.pk], | ||||||
|  | 		references: [studios.pk], | ||||||
|  | 	}), | ||||||
|  | })); | ||||||
|  | export const ssjRelations = relations(showStudioJoin, ({ one }) => ({ | ||||||
|  | 	show: one(shows, { | ||||||
|  | 		relationName: "ssj_show", | ||||||
|  | 		fields: [showStudioJoin.showPk], | ||||||
|  | 		references: [shows.pk], | ||||||
|  | 	}), | ||||||
|  | 	studio: one(studios, { | ||||||
|  | 		relationName: "ssj_studio", | ||||||
|  | 		fields: [showStudioJoin.studioPk], | ||||||
|  | 		references: [studios.pk], | ||||||
|  | 	}), | ||||||
|  | })); | ||||||
| @ -6,3 +6,17 @@ export const language = () => varchar({ length: 255 }); | |||||||
| 
 | 
 | ||||||
| export const image = () => | export const image = () => | ||||||
| 	jsonb().$type<{ id: string; source: string; blurhash: string }>(); | 	jsonb().$type<{ id: string; source: string; blurhash: string }>(); | ||||||
|  | 
 | ||||||
|  | export const externalid = () => | ||||||
|  | 	jsonb() | ||||||
|  | 		.$type< | ||||||
|  | 			Record< | ||||||
|  | 				string, | ||||||
|  | 				{ | ||||||
|  | 					dataId: string; | ||||||
|  | 					link: string | null; | ||||||
|  | 				} | ||||||
|  | 			> | ||||||
|  | 		>() | ||||||
|  | 		.notNull() | ||||||
|  | 		.default({}); | ||||||
|  | |||||||
| @ -26,6 +26,9 @@ export const videos = schema.table( | |||||||
| 		createdAt: timestamp({ withTimezone: true, mode: "string" }) | 		createdAt: timestamp({ withTimezone: true, mode: "string" }) | ||||||
| 			.notNull() | 			.notNull() | ||||||
| 			.defaultNow(), | 			.defaultNow(), | ||||||
|  | 		updatedAt: timestamp({ withTimezone: true, mode: "string" }) | ||||||
|  | 			.notNull() | ||||||
|  | 			.$onUpdate(() => sql`now()`), | ||||||
| 	}, | 	}, | ||||||
| 	(t) => [ | 	(t) => [ | ||||||
| 		check("part_pos", sql`${t.part} >= 0`), | 		check("part_pos", sql`${t.part} >= 0`), | ||||||
| @ -36,15 +39,15 @@ export const videos = schema.table( | |||||||
| export const entryVideoJoin = schema.table( | export const entryVideoJoin = schema.table( | ||||||
| 	"entry_video_join", | 	"entry_video_join", | ||||||
| 	{ | 	{ | ||||||
| 		entry: integer() | 		entryPk: integer() | ||||||
| 			.notNull() | 			.notNull() | ||||||
| 			.references(() => entries.pk, { onDelete: "cascade" }), | 			.references(() => entries.pk, { onDelete: "cascade" }), | ||||||
| 		video: integer() | 		videoPk: integer() | ||||||
| 			.notNull() | 			.notNull() | ||||||
| 			.references(() => videos.pk, { onDelete: "cascade" }), | 			.references(() => videos.pk, { onDelete: "cascade" }), | ||||||
| 		slug: varchar({ length: 255 }).notNull().unique(), | 		slug: varchar({ length: 255 }).notNull().unique(), | ||||||
| 	}, | 	}, | ||||||
| 	(t) => [primaryKey({ columns: [t.entry, t.video] })], | 	(t) => [primaryKey({ columns: [t.entryPk, t.videoPk] })], | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| export const videosRelations = relations(videos, ({ many }) => ({ | export const videosRelations = relations(videos, ({ many }) => ({ | ||||||
| @ -56,12 +59,12 @@ export const videosRelations = relations(videos, ({ many }) => ({ | |||||||
| export const evjRelations = relations(entryVideoJoin, ({ one }) => ({ | export const evjRelations = relations(entryVideoJoin, ({ one }) => ({ | ||||||
| 	video: one(videos, { | 	video: one(videos, { | ||||||
| 		relationName: "evj_video", | 		relationName: "evj_video", | ||||||
| 		fields: [entryVideoJoin.video], | 		fields: [entryVideoJoin.videoPk], | ||||||
| 		references: [videos.pk], | 		references: [videos.pk], | ||||||
| 	}), | 	}), | ||||||
| 	entry: one(entries, { | 	entry: one(entries, { | ||||||
| 		relationName: "evj_entry", | 		relationName: "evj_entry", | ||||||
| 		fields: [entryVideoJoin.entry], | 		fields: [entryVideoJoin.entryPk], | ||||||
| 		references: [entries.pk], | 		references: [entries.pk], | ||||||
| 	}), | 	}), | ||||||
| })); | })); | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ import { collections } from "./controllers/shows/collections"; | |||||||
| import { movies } from "./controllers/shows/movies"; | import { movies } from "./controllers/shows/movies"; | ||||||
| import { series } from "./controllers/shows/series"; | import { series } from "./controllers/shows/series"; | ||||||
| import { showsH } from "./controllers/shows/shows"; | import { showsH } from "./controllers/shows/shows"; | ||||||
|  | import { studiosH } from "./controllers/studios"; | ||||||
| import { videosH } from "./controllers/videos"; | import { videosH } from "./controllers/videos"; | ||||||
| import type { KError } from "./models/error"; | import type { KError } from "./models/error"; | ||||||
| 
 | 
 | ||||||
| @ -48,4 +49,5 @@ export const app = new Elysia() | |||||||
| 	.use(entriesH) | 	.use(entriesH) | ||||||
| 	.use(seasonsH) | 	.use(seasonsH) | ||||||
| 	.use(videosH) | 	.use(videosH) | ||||||
|  | 	.use(studiosH) | ||||||
| 	.use(seed); | 	.use(seed); | ||||||
|  | |||||||
| @ -63,6 +63,7 @@ app | |||||||
| 							Can be used for administration or third party apps. | 							Can be used for administration or third party apps. | ||||||
| 						`,
 | 						`,
 | ||||||
| 					}, | 					}, | ||||||
|  | 					{ name: "studios", description: "Routes about studios" }, | ||||||
| 				], | 				], | ||||||
| 			}, | 			}, | ||||||
| 		}), | 		}), | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ import { t } from "elysia"; | |||||||
| import type { Prettify } from "elysia/dist/types"; | import type { Prettify } from "elysia/dist/types"; | ||||||
| import { bubbleImages, duneCollection, registerExamples } from "./examples"; | import { bubbleImages, duneCollection, registerExamples } from "./examples"; | ||||||
| import { | import { | ||||||
|  | 	DbMetadata, | ||||||
| 	ExternalId, | 	ExternalId, | ||||||
| 	Genre, | 	Genre, | ||||||
| 	Image, | 	Image, | ||||||
| @ -33,10 +34,9 @@ const BaseCollection = t.Object({ | |||||||
| 		}), | 		}), | ||||||
| 	), | 	), | ||||||
| 
 | 
 | ||||||
| 	createdAt: t.String({ format: "date-time" }), |  | ||||||
| 	nextRefresh: t.String({ format: "date-time" }), | 	nextRefresh: t.String({ format: "date-time" }), | ||||||
| 
 | 
 | ||||||
| 	externalId: ExternalId, | 	externalId: ExternalId(), | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| export const CollectionTranslation = t.Object({ | export const CollectionTranslation = t.Object({ | ||||||
| @ -56,6 +56,7 @@ export const Collection = t.Intersect([ | |||||||
| 	Resource(), | 	Resource(), | ||||||
| 	CollectionTranslation, | 	CollectionTranslation, | ||||||
| 	BaseCollection, | 	BaseCollection, | ||||||
|  | 	DbMetadata, | ||||||
| ]); | ]); | ||||||
| export type Collection = Prettify<typeof Collection.static>; | export type Collection = Prettify<typeof Collection.static>; | ||||||
| 
 | 
 | ||||||
| @ -68,13 +69,7 @@ export const FullCollection = t.Intersect([ | |||||||
| export type FullCollection = Prettify<typeof FullCollection.static>; | export type FullCollection = Prettify<typeof FullCollection.static>; | ||||||
| 
 | 
 | ||||||
| export const SeedCollection = t.Intersect([ | export const SeedCollection = t.Intersect([ | ||||||
| 	t.Omit(BaseCollection, [ | 	t.Omit(BaseCollection, ["kind", "startAir", "endAir", "nextRefresh"]), | ||||||
| 		"kind", |  | ||||||
| 		"startAir", |  | ||||||
| 		"endAir", |  | ||||||
| 		"createdAt", |  | ||||||
| 		"nextRefresh", |  | ||||||
| 	]), |  | ||||||
| 	t.Object({ | 	t.Object({ | ||||||
| 		slug: t.String({ format: "slug" }), | 		slug: t.String({ format: "slug" }), | ||||||
| 		translations: TranslationRecord( | 		translations: TranslationRecord( | ||||||
|  | |||||||
| @ -12,7 +12,6 @@ export const BaseEntry = () => | |||||||
| 		), | 		), | ||||||
| 		thumbnail: t.Nullable(Image), | 		thumbnail: t.Nullable(Image), | ||||||
| 
 | 
 | ||||||
| 		createdAt: t.String({ format: "date-time" }), |  | ||||||
| 		nextRefresh: t.String({ format: "date-time" }), | 		nextRefresh: t.String({ format: "date-time" }), | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,7 +1,13 @@ | |||||||
| import { t } from "elysia"; | import { t } from "elysia"; | ||||||
| import type { Prettify } from "~/utils"; | import type { Prettify } from "~/utils"; | ||||||
| import { bubbleImages, madeInAbyss, registerExamples } from "../examples"; | import { bubbleImages, madeInAbyss, registerExamples } from "../examples"; | ||||||
| import { EpisodeId, Resource, SeedImage, TranslationRecord } from "../utils"; | import { | ||||||
|  | 	DbMetadata, | ||||||
|  | 	EpisodeId, | ||||||
|  | 	Resource, | ||||||
|  | 	SeedImage, | ||||||
|  | 	TranslationRecord, | ||||||
|  | } from "../utils"; | ||||||
| import { BaseEntry, EntryTranslation } from "./base-entry"; | import { BaseEntry, EntryTranslation } from "./base-entry"; | ||||||
| 
 | 
 | ||||||
| export const BaseEpisode = t.Intersect([ | export const BaseEpisode = t.Intersect([ | ||||||
| @ -19,11 +25,12 @@ export const Episode = t.Intersect([ | |||||||
| 	Resource(), | 	Resource(), | ||||||
| 	EntryTranslation(), | 	EntryTranslation(), | ||||||
| 	BaseEpisode, | 	BaseEpisode, | ||||||
|  | 	DbMetadata, | ||||||
| ]); | ]); | ||||||
| export type Episode = Prettify<typeof Episode.static>; | export type Episode = Prettify<typeof Episode.static>; | ||||||
| 
 | 
 | ||||||
| export const SeedEpisode = t.Intersect([ | export const SeedEpisode = t.Intersect([ | ||||||
| 	t.Omit(BaseEpisode, ["thumbnail", "createdAt", "nextRefresh"]), | 	t.Omit(BaseEpisode, ["thumbnail", "nextRefresh"]), | ||||||
| 	t.Object({ | 	t.Object({ | ||||||
| 		thumbnail: t.Nullable(SeedImage), | 		thumbnail: t.Nullable(SeedImage), | ||||||
| 		translations: TranslationRecord(EntryTranslation()), | 		translations: TranslationRecord(EntryTranslation()), | ||||||
|  | |||||||
| @ -1,7 +1,7 @@ | |||||||
| import { t } from "elysia"; | import { t } from "elysia"; | ||||||
| import { type Prettify, comment } from "~/utils"; | import { type Prettify, comment } from "~/utils"; | ||||||
| import { madeInAbyss, registerExamples } from "../examples"; | import { madeInAbyss, registerExamples } from "../examples"; | ||||||
| import { SeedImage } from "../utils"; | import { DbMetadata, SeedImage } from "../utils"; | ||||||
| import { Resource } from "../utils/resource"; | import { Resource } from "../utils/resource"; | ||||||
| import { BaseEntry } from "./base-entry"; | import { BaseEntry } from "./base-entry"; | ||||||
| 
 | 
 | ||||||
| @ -31,11 +31,11 @@ export const BaseExtra = t.Intersect( | |||||||
| 	}, | 	}, | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| export const Extra = t.Intersect([Resource(), BaseExtra]); | export const Extra = t.Intersect([Resource(), BaseExtra, DbMetadata]); | ||||||
| export type Extra = Prettify<typeof Extra.static>; | export type Extra = Prettify<typeof Extra.static>; | ||||||
| 
 | 
 | ||||||
| export const SeedExtra = t.Intersect([ | export const SeedExtra = t.Intersect([ | ||||||
| 	t.Omit(BaseExtra, ["thumbnail", "createdAt"]), | 	t.Omit(BaseExtra, ["thumbnail"]), | ||||||
| 	t.Object({ | 	t.Object({ | ||||||
| 		slug: t.String({ format: "slug" }), | 		slug: t.String({ format: "slug" }), | ||||||
| 		thumbnail: t.Nullable(SeedImage), | 		thumbnail: t.Nullable(SeedImage), | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ import { t } from "elysia"; | |||||||
| import { type Prettify, comment } from "~/utils"; | import { type Prettify, comment } from "~/utils"; | ||||||
| import { bubbleImages, madeInAbyss, registerExamples } from "../examples"; | import { bubbleImages, madeInAbyss, registerExamples } from "../examples"; | ||||||
| import { | import { | ||||||
|  | 	DbMetadata, | ||||||
| 	ExternalId, | 	ExternalId, | ||||||
| 	Image, | 	Image, | ||||||
| 	Resource, | 	Resource, | ||||||
| @ -18,7 +19,7 @@ export const BaseMovieEntry = t.Intersect( | |||||||
| 				minimum: 1, | 				minimum: 1, | ||||||
| 				description: "Absolute playback order. Can be mixed with episodes.", | 				description: "Absolute playback order. Can be mixed with episodes.", | ||||||
| 			}), | 			}), | ||||||
| 			externalId: ExternalId, | 			externalId: ExternalId(), | ||||||
| 		}), | 		}), | ||||||
| 		BaseEntry(), | 		BaseEntry(), | ||||||
| 	], | 	], | ||||||
| @ -42,11 +43,12 @@ export const MovieEntry = t.Intersect([ | |||||||
| 	Resource(), | 	Resource(), | ||||||
| 	MovieEntryTranslation, | 	MovieEntryTranslation, | ||||||
| 	BaseMovieEntry, | 	BaseMovieEntry, | ||||||
|  | 	DbMetadata, | ||||||
| ]); | ]); | ||||||
| export type MovieEntry = Prettify<typeof MovieEntry.static>; | export type MovieEntry = Prettify<typeof MovieEntry.static>; | ||||||
| 
 | 
 | ||||||
| export const SeedMovieEntry = t.Intersect([ | export const SeedMovieEntry = t.Intersect([ | ||||||
| 	t.Omit(BaseMovieEntry, ["thumbnail", "createdAt", "nextRefresh"]), | 	t.Omit(BaseMovieEntry, ["thumbnail", "nextRefresh"]), | ||||||
| 	t.Object({ | 	t.Object({ | ||||||
| 		slug: t.Optional(t.String({ format: "slug" })), | 		slug: t.Optional(t.String({ format: "slug" })), | ||||||
| 		thumbnail: t.Nullable(SeedImage), | 		thumbnail: t.Nullable(SeedImage), | ||||||
|  | |||||||
| @ -1,7 +1,13 @@ | |||||||
| import { t } from "elysia"; | import { t } from "elysia"; | ||||||
| import { type Prettify, comment } from "~/utils"; | import { type Prettify, comment } from "~/utils"; | ||||||
| import { bubbleImages, madeInAbyss, registerExamples } from "../examples"; | import { bubbleImages, madeInAbyss, registerExamples } from "../examples"; | ||||||
| import { EpisodeId, Resource, SeedImage, TranslationRecord } from "../utils"; | import { | ||||||
|  | 	DbMetadata, | ||||||
|  | 	EpisodeId, | ||||||
|  | 	Resource, | ||||||
|  | 	SeedImage, | ||||||
|  | 	TranslationRecord, | ||||||
|  | } from "../utils"; | ||||||
| import { BaseEntry, EntryTranslation } from "./base-entry"; | import { BaseEntry, EntryTranslation } from "./base-entry"; | ||||||
| 
 | 
 | ||||||
| export const BaseSpecial = t.Intersect( | export const BaseSpecial = t.Intersect( | ||||||
| @ -29,11 +35,12 @@ export const Special = t.Intersect([ | |||||||
| 	Resource(), | 	Resource(), | ||||||
| 	EntryTranslation(), | 	EntryTranslation(), | ||||||
| 	BaseSpecial, | 	BaseSpecial, | ||||||
|  | 	DbMetadata, | ||||||
| ]); | ]); | ||||||
| export type Special = Prettify<typeof Special.static>; | export type Special = Prettify<typeof Special.static>; | ||||||
| 
 | 
 | ||||||
| export const SeedSpecial = t.Intersect([ | export const SeedSpecial = t.Intersect([ | ||||||
| 	t.Omit(BaseSpecial, ["thumbnail", "createdAt", "nextRefresh"]), | 	t.Omit(BaseSpecial, ["thumbnail", "nextRefresh"]), | ||||||
| 	t.Object({ | 	t.Object({ | ||||||
| 		thumbnail: t.Nullable(SeedImage), | 		thumbnail: t.Nullable(SeedImage), | ||||||
| 		translations: TranslationRecord(EntryTranslation()), | 		translations: TranslationRecord(EntryTranslation()), | ||||||
|  | |||||||
| @ -1,8 +1,7 @@ | |||||||
| import { t } from "elysia"; | import { t } from "elysia"; | ||||||
| import { type Prettify, comment } from "~/utils"; | import { type Prettify, comment } from "~/utils"; | ||||||
| import { bubbleImages, registerExamples } from "../examples"; | import { bubbleImages, registerExamples, youtubeExample } from "../examples"; | ||||||
| import { youtubeExample } from "../examples/others"; | import { DbMetadata, Resource } from "../utils"; | ||||||
| import { Resource } from "../utils/resource"; |  | ||||||
| import { BaseEntry, EntryTranslation } from "./base-entry"; | import { BaseEntry, EntryTranslation } from "./base-entry"; | ||||||
| 
 | 
 | ||||||
| export const BaseUnknownEntry = t.Intersect( | export const BaseUnknownEntry = t.Intersect( | ||||||
| @ -28,6 +27,7 @@ export const UnknownEntry = t.Intersect([ | |||||||
| 	Resource(), | 	Resource(), | ||||||
| 	UnknownEntryTranslation, | 	UnknownEntryTranslation, | ||||||
| 	BaseUnknownEntry, | 	BaseUnknownEntry, | ||||||
|  | 	DbMetadata, | ||||||
| ]); | ]); | ||||||
| export type UnknownEntry = Prettify<typeof UnknownEntry.static>; | export type UnknownEntry = Prettify<typeof UnknownEntry.static>; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ export const bubbleVideo: Video = { | |||||||
| 	part: null, | 	part: null, | ||||||
| 	version: 1, | 	version: 1, | ||||||
| 	createdAt: "2024-11-23T15:01:24.968Z", | 	createdAt: "2024-11-23T15:01:24.968Z", | ||||||
|  | 	updatedAt: "2024-11-23T15:01:24.968Z", | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const bubble: SeedMovie = { | export const bubble: SeedMovie = { | ||||||
| @ -60,6 +61,7 @@ export const bubble: SeedMovie = { | |||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
| 	videos: [bubbleVideo.id], | 	videos: [bubbleVideo.id], | ||||||
|  | 	studios: [], | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const bubbleImages = { | export const bubbleImages = { | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ export const dune1984Video: Video = { | |||||||
| 	part: null, | 	part: null, | ||||||
| 	version: 1, | 	version: 1, | ||||||
| 	createdAt: "2024-12-02T11:45:12.968Z", | 	createdAt: "2024-12-02T11:45:12.968Z", | ||||||
|  | 	updatedAt: "2024-12-02T11:45:12.968Z", | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const dune1984: SeedMovie = { | export const dune1984: SeedMovie = { | ||||||
| @ -47,6 +48,7 @@ export const dune1984: SeedMovie = { | |||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
| 	videos: [dune1984Video.id], | 	videos: [dune1984Video.id], | ||||||
|  | 	studios: [], | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const dune1984Images = { | export const dune1984Images = { | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ export const duneVideo: Video = { | |||||||
| 	part: null, | 	part: null, | ||||||
| 	version: 1, | 	version: 1, | ||||||
| 	createdAt: "2024-12-02T10:10:24.968Z", | 	createdAt: "2024-12-02T10:10:24.968Z", | ||||||
|  | 	updatedAt: "2024-12-02T10:10:24.968Z", | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const dune: SeedMovie = { | export const dune: SeedMovie = { | ||||||
| @ -47,6 +48,7 @@ export const dune: SeedMovie = { | |||||||
| 		}, | 		}, | ||||||
| 	}, | 	}, | ||||||
| 	videos: [duneVideo.id], | 	videos: [duneVideo.id], | ||||||
|  | 	studios: [], | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const duneImages = { | export const duneImages = { | ||||||
|  | |||||||
| @ -16,6 +16,7 @@ export const madeInAbyssVideo: Video = { | |||||||
| 		from: "guessit", | 		from: "guessit", | ||||||
| 	}, | 	}, | ||||||
| 	createdAt: "2024-11-23T15:01:24.968Z", | 	createdAt: "2024-11-23T15:01:24.968Z", | ||||||
|  | 	updatedAt: "2024-11-23T15:01:24.968Z", | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const madeInAbyss = { | export const madeInAbyss = { | ||||||
| @ -242,4 +243,21 @@ export const madeInAbyss = { | |||||||
| 			video: "3cd436ee-01ff-4f45-ba98-654282531234", | 			video: "3cd436ee-01ff-4f45-ba98-654282531234", | ||||||
| 		}, | 		}, | ||||||
| 	], | 	], | ||||||
|  | 	studios: [ | ||||||
|  | 		{ | ||||||
|  | 			slug: "kinema-citrus", | ||||||
|  | 			translations: { | ||||||
|  | 				en: { | ||||||
|  | 					name: "Kinema Citrus", | ||||||
|  | 					logo: "https://image.tmdb.org/t/p/original/Lf0udeB7OwHoFJ0XIxVwfyGOqE.png", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 			externalId: { | ||||||
|  | 				themoviedatabase: { | ||||||
|  | 					dataId: "16738", | ||||||
|  | 					link: "https://www.themoviedb.org/company/16738", | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
|  | 	], | ||||||
| } satisfies SeedSerie; | } satisfies SeedSerie; | ||||||
|  | |||||||
| @ -1,9 +1,10 @@ | |||||||
| import { t } from "elysia"; | import { t } from "elysia"; | ||||||
| import type { Prettify } from "~/utils"; | import type { Prettify } from "~/utils"; | ||||||
| import { SeedCollection } from "./collections"; | import { SeedCollection } from "./collections"; | ||||||
| import { bubble, registerExamples } from "./examples"; | import { bubble, bubbleImages, registerExamples } from "./examples"; | ||||||
| import { bubbleImages } from "./examples/bubble"; | import { SeedStudio, Studio } from "./studio"; | ||||||
| import { | import { | ||||||
|  | 	DbMetadata, | ||||||
| 	ExternalId, | 	ExternalId, | ||||||
| 	Genre, | 	Genre, | ||||||
| 	Image, | 	Image, | ||||||
| @ -33,10 +34,9 @@ const BaseMovie = t.Object({ | |||||||
| 		}), | 		}), | ||||||
| 	), | 	), | ||||||
| 
 | 
 | ||||||
| 	createdAt: t.String({ format: "date-time" }), |  | ||||||
| 	nextRefresh: t.String({ format: "date-time" }), | 	nextRefresh: t.String({ format: "date-time" }), | ||||||
| 
 | 
 | ||||||
| 	externalId: ExternalId, | 	externalId: ExternalId(), | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| export const MovieTranslation = t.Object({ | export const MovieTranslation = t.Object({ | ||||||
| @ -58,6 +58,7 @@ export const Movie = t.Intersect([ | |||||||
| 	Resource(), | 	Resource(), | ||||||
| 	MovieTranslation, | 	MovieTranslation, | ||||||
| 	BaseMovie, | 	BaseMovie, | ||||||
|  | 	DbMetadata, | ||||||
| 	// t.Object({ isAvailable: t.Boolean() }),
 | 	// t.Object({ isAvailable: t.Boolean() }),
 | ||||||
| ]); | ]); | ||||||
| export type Movie = Prettify<typeof Movie.static>; | export type Movie = Prettify<typeof Movie.static>; | ||||||
| @ -67,12 +68,13 @@ export const FullMovie = t.Intersect([ | |||||||
| 	t.Object({ | 	t.Object({ | ||||||
| 		translations: t.Optional(TranslationRecord(MovieTranslation)), | 		translations: t.Optional(TranslationRecord(MovieTranslation)), | ||||||
| 		videos: t.Optional(t.Array(Video)), | 		videos: t.Optional(t.Array(Video)), | ||||||
|  | 		studios: t.Optional(t.Array(Studio)), | ||||||
| 	}), | 	}), | ||||||
| ]); | ]); | ||||||
| export type FullMovie = Prettify<typeof FullMovie.static>; | export type FullMovie = Prettify<typeof FullMovie.static>; | ||||||
| 
 | 
 | ||||||
| export const SeedMovie = t.Intersect([ | export const SeedMovie = t.Intersect([ | ||||||
| 	t.Omit(BaseMovie, ["kind", "createdAt", "nextRefresh"]), | 	t.Omit(BaseMovie, ["kind", "nextRefresh"]), | ||||||
| 	t.Object({ | 	t.Object({ | ||||||
| 		slug: t.String({ format: "slug", examples: ["bubble"] }), | 		slug: t.String({ format: "slug", examples: ["bubble"] }), | ||||||
| 		translations: TranslationRecord( | 		translations: TranslationRecord( | ||||||
| @ -88,6 +90,7 @@ export const SeedMovie = t.Intersect([ | |||||||
| 		), | 		), | ||||||
| 		videos: t.Optional(t.Array(t.String({ format: "uuid" }))), | 		videos: t.Optional(t.Array(t.String({ format: "uuid" }))), | ||||||
| 		collection: t.Optional(SeedCollection), | 		collection: t.Optional(SeedCollection), | ||||||
|  | 		studios: t.Array(SeedStudio), | ||||||
| 	}), | 	}), | ||||||
| ]); | ]); | ||||||
| export type SeedMovie = Prettify<typeof SeedMovie.static>; | export type SeedMovie = Prettify<typeof SeedMovie.static>; | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| import { t } from "elysia"; | import { t } from "elysia"; | ||||||
| import type { Prettify } from "~/utils"; | import type { Prettify } from "~/utils"; | ||||||
| import { bubbleImages, madeInAbyss, registerExamples } from "./examples"; | import { bubbleImages, madeInAbyss, registerExamples } from "./examples"; | ||||||
|  | import { DbMetadata } from "./utils"; | ||||||
| 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"; | ||||||
| @ -11,7 +12,6 @@ export const BaseSeason = t.Object({ | |||||||
| 	startAir: t.Nullable(t.String({ format: "date" })), | 	startAir: t.Nullable(t.String({ format: "date" })), | ||||||
| 	endAir: t.Nullable(t.String({ format: "date" })), | 	endAir: t.Nullable(t.String({ format: "date" })), | ||||||
| 
 | 
 | ||||||
| 	createdAt: t.String({ format: "date-time" }), |  | ||||||
| 	nextRefresh: t.String({ format: "date-time" }), | 	nextRefresh: t.String({ format: "date-time" }), | ||||||
| 
 | 
 | ||||||
| 	externalId: SeasonId, | 	externalId: SeasonId, | ||||||
| @ -27,11 +27,16 @@ export const SeasonTranslation = t.Object({ | |||||||
| }); | }); | ||||||
| export type SeasonTranslation = typeof SeasonTranslation.static; | export type SeasonTranslation = typeof SeasonTranslation.static; | ||||||
| 
 | 
 | ||||||
| export const Season = t.Intersect([Resource(), SeasonTranslation, BaseSeason]); | export const Season = t.Intersect([ | ||||||
| export type Season = typeof Season.static; | 	Resource(), | ||||||
|  | 	SeasonTranslation, | ||||||
|  | 	BaseSeason, | ||||||
|  | 	DbMetadata, | ||||||
|  | ]); | ||||||
|  | export type Season = Prettify<typeof Season.static>; | ||||||
| 
 | 
 | ||||||
| export const SeedSeason = t.Intersect([ | export const SeedSeason = t.Intersect([ | ||||||
| 	t.Omit(BaseSeason, ["createdAt", "nextRefresh"]), | 	t.Omit(BaseSeason, ["nextRefresh"]), | ||||||
| 	t.Object({ | 	t.Object({ | ||||||
| 		translations: TranslationRecord( | 		translations: TranslationRecord( | ||||||
| 			t.Intersect([ | 			t.Intersect([ | ||||||
|  | |||||||
| @ -4,11 +4,17 @@ import { SeedCollection } from "./collections"; | |||||||
| import { SeedEntry, SeedExtra } from "./entry"; | import { SeedEntry, SeedExtra } from "./entry"; | ||||||
| import { bubbleImages, madeInAbyss, registerExamples } from "./examples"; | import { bubbleImages, madeInAbyss, registerExamples } from "./examples"; | ||||||
| import { SeedSeason } from "./season"; | import { SeedSeason } from "./season"; | ||||||
| import { ExternalId } from "./utils/external-id"; | import { SeedStudio, Studio } from "./studio"; | ||||||
| import { Genre } from "./utils/genres"; | import { | ||||||
| import { Image, SeedImage } from "./utils/image"; | 	DbMetadata, | ||||||
| import { Language, TranslationRecord } from "./utils/language"; | 	ExternalId, | ||||||
| import { Resource } from "./utils/resource"; | 	Genre, | ||||||
|  | 	Image, | ||||||
|  | 	Language, | ||||||
|  | 	Resource, | ||||||
|  | 	SeedImage, | ||||||
|  | 	TranslationRecord, | ||||||
|  | } from "./utils"; | ||||||
| 
 | 
 | ||||||
| export const SerieStatus = t.UnionEnum([ | export const SerieStatus = t.UnionEnum([ | ||||||
| 	"unknown", | 	"unknown", | ||||||
| @ -18,7 +24,7 @@ export const SerieStatus = t.UnionEnum([ | |||||||
| ]); | ]); | ||||||
| export type SerieStatus = typeof SerieStatus.static; | export type SerieStatus = typeof SerieStatus.static; | ||||||
| 
 | 
 | ||||||
| export const BaseSerie = t.Object({ | const BaseSerie = t.Object({ | ||||||
| 	kind: t.Literal("serie"), | 	kind: t.Literal("serie"), | ||||||
| 	genres: t.Array(Genre), | 	genres: t.Array(Genre), | ||||||
| 	rating: t.Nullable(t.Integer({ minimum: 0, maximum: 100 })), | 	rating: t.Nullable(t.Integer({ minimum: 0, maximum: 100 })), | ||||||
| @ -38,10 +44,9 @@ export const BaseSerie = t.Object({ | |||||||
| 		}), | 		}), | ||||||
| 	), | 	), | ||||||
| 
 | 
 | ||||||
| 	createdAt: t.String({ format: "date-time" }), |  | ||||||
| 	nextRefresh: t.String({ format: "date-time" }), | 	nextRefresh: t.String({ format: "date-time" }), | ||||||
| 
 | 
 | ||||||
| 	externalId: ExternalId, | 	externalId: ExternalId(), | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| export const SerieTranslation = t.Object({ | export const SerieTranslation = t.Object({ | ||||||
| @ -59,19 +64,25 @@ 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, | ||||||
|  | 	DbMetadata, | ||||||
|  | ]); | ||||||
| export type Serie = Prettify<typeof Serie.static>; | export type Serie = Prettify<typeof Serie.static>; | ||||||
| 
 | 
 | ||||||
| export const FullSerie = t.Intersect([ | export const FullSerie = t.Intersect([ | ||||||
| 	Serie, | 	Serie, | ||||||
| 	t.Object({ | 	t.Object({ | ||||||
| 		translations: t.Optional(TranslationRecord(SerieTranslation)), | 		translations: t.Optional(TranslationRecord(SerieTranslation)), | ||||||
|  | 		studios: t.Optional(t.Array(Studio)), | ||||||
| 	}), | 	}), | ||||||
| ]); | ]); | ||||||
| export type FullMovie = Prettify<typeof FullSerie.static>; | export type FullMovie = Prettify<typeof FullSerie.static>; | ||||||
| 
 | 
 | ||||||
| export const SeedSerie = t.Intersect([ | export const SeedSerie = t.Intersect([ | ||||||
| 	t.Omit(BaseSerie, ["kind", "createdAt", "nextRefresh"]), | 	t.Omit(BaseSerie, ["kind", "nextRefresh"]), | ||||||
| 	t.Object({ | 	t.Object({ | ||||||
| 		slug: t.String({ format: "slug" }), | 		slug: t.String({ format: "slug" }), | ||||||
| 		translations: TranslationRecord( | 		translations: TranslationRecord( | ||||||
| @ -89,6 +100,7 @@ export const SeedSerie = t.Intersect([ | |||||||
| 		entries: t.Array(SeedEntry), | 		entries: t.Array(SeedEntry), | ||||||
| 		extras: t.Optional(t.Array(SeedExtra)), | 		extras: t.Optional(t.Array(SeedExtra)), | ||||||
| 		collection: t.Optional(SeedCollection), | 		collection: t.Optional(SeedCollection), | ||||||
|  | 		studios: t.Array(SeedStudio), | ||||||
| 	}), | 	}), | ||||||
| ]); | ]); | ||||||
| export type SeedSerie = typeof SeedSerie.static; | export type SeedSerie = typeof SeedSerie.static; | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								api/src/models/show.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								api/src/models/show.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | |||||||
|  | import { t } from "elysia"; | ||||||
|  | import { Collection } from "./collections"; | ||||||
|  | import { Movie } from "./movie"; | ||||||
|  | import { Serie } from "./serie"; | ||||||
|  | 
 | ||||||
|  | export const Show = t.Union([Movie, Serie, Collection]); | ||||||
							
								
								
									
										41
									
								
								api/src/models/studio.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								api/src/models/studio.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | |||||||
|  | import { t } from "elysia"; | ||||||
|  | import type { Prettify } from "elysia/dist/types"; | ||||||
|  | import { bubbleImages, madeInAbyss, registerExamples } from "./examples"; | ||||||
|  | import { DbMetadata, ExternalId, Resource, TranslationRecord } from "./utils"; | ||||||
|  | import { Image, SeedImage } from "./utils/image"; | ||||||
|  | 
 | ||||||
|  | const BaseStudio = t.Object({ | ||||||
|  | 	externalId: ExternalId(), | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | export const StudioTranslation = t.Object({ | ||||||
|  | 	name: t.String(), | ||||||
|  | 	logo: t.Nullable(Image), | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | export const Studio = t.Intersect([ | ||||||
|  | 	Resource(), | ||||||
|  | 	StudioTranslation, | ||||||
|  | 	BaseStudio, | ||||||
|  | 	DbMetadata, | ||||||
|  | ]); | ||||||
|  | export type Studio = Prettify<typeof Studio.static>; | ||||||
|  | 
 | ||||||
|  | export const SeedStudio = t.Intersect([ | ||||||
|  | 	BaseStudio, | ||||||
|  | 	t.Object({ | ||||||
|  | 		slug: t.String({ format: "slug" }), | ||||||
|  | 		translations: TranslationRecord( | ||||||
|  | 			t.Intersect([ | ||||||
|  | 				t.Omit(StudioTranslation, ["logo"]), | ||||||
|  | 				t.Object({ | ||||||
|  | 					logo: t.Nullable(SeedImage), | ||||||
|  | 				}), | ||||||
|  | 			]), | ||||||
|  | 		), | ||||||
|  | 	}), | ||||||
|  | ]); | ||||||
|  | export type SeedStudio = Prettify<typeof SeedStudio.static>; | ||||||
|  | 
 | ||||||
|  | const ex = madeInAbyss.studios[0]; | ||||||
|  | registerExamples(Studio, { ...ex, ...ex.translations.en, ...bubbleImages }); | ||||||
							
								
								
									
										6
									
								
								api/src/models/utils/db-metadata.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								api/src/models/utils/db-metadata.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | |||||||
|  | import { t } from "elysia"; | ||||||
|  | 
 | ||||||
|  | export const DbMetadata = t.Object({ | ||||||
|  | 	createdAt: t.String({ format: "date-time" }), | ||||||
|  | 	updatedAt: t.String({ format: "date-time" }), | ||||||
|  | }); | ||||||
| @ -1,14 +1,14 @@ | |||||||
| import { t } from "elysia"; | import { t } from "elysia"; | ||||||
| import { comment } from "../../utils"; | import { comment } from "../../utils"; | ||||||
| 
 | 
 | ||||||
| export const ExternalId = t.Record( | export const ExternalId = () => | ||||||
| 	t.String(), | 	t.Record( | ||||||
| 	t.Object({ | 		t.String(), | ||||||
| 		dataId: t.String(), | 		t.Object({ | ||||||
| 		link: t.Nullable(t.String({ format: "uri" })), | 			dataId: t.String(), | ||||||
| 	}), | 			link: t.Nullable(t.String({ format: "uri" })), | ||||||
| ); | 		}), | ||||||
| export type ExternalId = typeof ExternalId.static; | 	); | ||||||
| 
 | 
 | ||||||
| export const EpisodeId = t.Record( | export const EpisodeId = t.Record( | ||||||
| 	t.String(), | 	t.String(), | ||||||
|  | |||||||
| @ -7,3 +7,4 @@ export * from "./filters"; | |||||||
| export * from "./page"; | export * from "./page"; | ||||||
| export * from "./sort"; | export * from "./sort"; | ||||||
| export * from "./keyset-paginate"; | export * from "./keyset-paginate"; | ||||||
|  | export * from "./db-metadata"; | ||||||
|  | |||||||
| @ -4,7 +4,9 @@ import { | |||||||
| 	type TSchema, | 	type TSchema, | ||||||
| 	type TString, | 	type TString, | ||||||
| } from "@sinclair/typebox"; | } from "@sinclair/typebox"; | ||||||
|  | import { type Column, type Table, eq, sql } from "drizzle-orm"; | ||||||
| import { t } from "elysia"; | import { t } from "elysia"; | ||||||
|  | import { sqlarr } from "~/db/utils"; | ||||||
| import { comment } from "../../utils"; | import { comment } from "../../utils"; | ||||||
| import { KErrorT } from "../error"; | import { KErrorT } from "../error"; | ||||||
| 
 | 
 | ||||||
| @ -106,3 +108,19 @@ export const AcceptLanguage = ({ | |||||||
| 		` | 		` | ||||||
| 				: ""), | 				: ""), | ||||||
| 	}); | 	}); | ||||||
|  | 
 | ||||||
|  | export const selectTranslationQuery = ( | ||||||
|  | 	translationTable: Table & { language: Column }, | ||||||
|  | 	languages: string[], | ||||||
|  | ) => ({ | ||||||
|  | 	columns: { | ||||||
|  | 		pk: false, | ||||||
|  | 	} as const, | ||||||
|  | 	where: !languages.includes("*") | ||||||
|  | 		? eq(translationTable.language, sql`any(${sqlarr(languages)})`) | ||||||
|  | 		: undefined, | ||||||
|  | 	orderBy: [ | ||||||
|  | 		sql`array_position(${sqlarr(languages)}, ${translationTable.language})`, | ||||||
|  | 	], | ||||||
|  | 	limit: 1, | ||||||
|  | }); | ||||||
|  | |||||||
| @ -1,10 +1,9 @@ | |||||||
| import { type TSchema, t } from "elysia"; | import { t } from "elysia"; | ||||||
| import { comment } from "../utils"; | import { type Prettify, comment } from "~/utils"; | ||||||
| import { bubbleVideo, registerExamples } from "./examples"; | import { bubbleVideo, registerExamples } from "./examples"; | ||||||
|  | import { DbMetadata, Resource } from "./utils"; | ||||||
| 
 | 
 | ||||||
| export const Video = t.Object({ | export const SeedVideo = t.Object({ | ||||||
| 	id: t.String({ format: "uuid" }), |  | ||||||
| 	slug: t.String({ format: "slug" }), |  | ||||||
| 	path: t.String(), | 	path: t.String(), | ||||||
| 	rendering: t.String({ | 	rendering: t.String({ | ||||||
| 		description: comment` | 		description: comment` | ||||||
| @ -30,8 +29,6 @@ export const Video = t.Object({ | |||||||
| 			"Kyoo will prefer playing back the highest `version` number if there are multiples rendering.", | 			"Kyoo will prefer playing back the highest `version` number if there are multiples rendering.", | ||||||
| 	}), | 	}), | ||||||
| 
 | 
 | ||||||
| 	createdAt: t.String({ format: "date-time" }), |  | ||||||
| 
 |  | ||||||
| 	guess: t.Optional( | 	guess: t.Optional( | ||||||
| 		t.Recursive((Self) => | 		t.Recursive((Self) => | ||||||
| 			t.Object( | 			t.Object( | ||||||
| @ -69,8 +66,9 @@ export const Video = t.Object({ | |||||||
| 		), | 		), | ||||||
| 	), | 	), | ||||||
| }); | }); | ||||||
| export type Video = typeof Video.static; |  | ||||||
| registerExamples(Video, bubbleVideo); |  | ||||||
| 
 |  | ||||||
| export const SeedVideo = t.Omit(Video, ["id", "slug", "createdAt"]); |  | ||||||
| export type SeedVideo = typeof SeedVideo.static; | export type SeedVideo = typeof SeedVideo.static; | ||||||
|  | 
 | ||||||
|  | export const Video = t.Intersect([Resource(), SeedVideo, DbMetadata]); | ||||||
|  | export type Video = Prettify<typeof Video.static>; | ||||||
|  | 
 | ||||||
|  | registerExamples(Video, bubbleVideo); | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| export * from "./movies-helper"; | export * from "./movies-helper"; | ||||||
| export * from "./series-helper"; | export * from "./series-helper"; | ||||||
|  | export * from "./studio-helper"; | ||||||
| export * from "./videos-helper"; | export * from "./videos-helper"; | ||||||
| 
 | 
 | ||||||
| export * from "~/elysia"; | export * from "~/elysia"; | ||||||
|  | |||||||
| @ -4,7 +4,10 @@ import type { SeedMovie } from "~/models/movie"; | |||||||
| 
 | 
 | ||||||
| export const getMovie = async ( | export const getMovie = async ( | ||||||
| 	id: string, | 	id: string, | ||||||
| 	{ langs, ...query }: { langs?: string; preferOriginal?: boolean }, | 	{ | ||||||
|  | 		langs, | ||||||
|  | 		...query | ||||||
|  | 	}: { langs?: string; preferOriginal?: boolean; with?: string[] }, | ||||||
| ) => { | ) => { | ||||||
| 	const resp = await app.handle( | 	const resp = await app.handle( | ||||||
| 		new Request(buildUrl(`movies/${id}`, query), { | 		new Request(buildUrl(`movies/${id}`, query), { | ||||||
|  | |||||||
| @ -16,6 +16,27 @@ export const createSerie = async (serie: SeedSerie) => { | |||||||
| 	return [resp, body] as const; | 	return [resp, body] as const; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | export const getSerie = async ( | ||||||
|  | 	id: string, | ||||||
|  | 	{ | ||||||
|  | 		langs, | ||||||
|  | 		...query | ||||||
|  | 	}: { langs?: string; preferOriginal?: boolean; with?: string[] }, | ||||||
|  | ) => { | ||||||
|  | 	const resp = await app.handle( | ||||||
|  | 		new Request(buildUrl(`series/${id}`, query), { | ||||||
|  | 			method: "GET", | ||||||
|  | 			headers: langs | ||||||
|  | 				? { | ||||||
|  | 						"Accept-Language": langs, | ||||||
|  | 					} | ||||||
|  | 				: {}, | ||||||
|  | 		}), | ||||||
|  | 	); | ||||||
|  | 	const body = await resp.json(); | ||||||
|  | 	return [resp, body] as const; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| export const getSeasons = async ( | export const getSeasons = async ( | ||||||
| 	serie: string, | 	serie: string, | ||||||
| 	{ | 	{ | ||||||
|  | |||||||
							
								
								
									
										49
									
								
								api/tests/helpers/studio-helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								api/tests/helpers/studio-helper.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,49 @@ | |||||||
|  | import { buildUrl } from "tests/utils"; | ||||||
|  | import { app } from "~/elysia"; | ||||||
|  | 
 | ||||||
|  | export const getStudio = async ( | ||||||
|  | 	id: string, | ||||||
|  | 	{ langs, ...query }: { langs?: string; preferOriginal?: boolean }, | ||||||
|  | ) => { | ||||||
|  | 	const resp = await app.handle( | ||||||
|  | 		new Request(buildUrl(`studios/${id}`, query), { | ||||||
|  | 			method: "GET", | ||||||
|  | 			headers: langs | ||||||
|  | 				? { | ||||||
|  | 						"Accept-Language": langs, | ||||||
|  | 					} | ||||||
|  | 				: {}, | ||||||
|  | 		}), | ||||||
|  | 	); | ||||||
|  | 	const body = await resp.json(); | ||||||
|  | 	return [resp, body] as const; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const getShowsByStudio = async ( | ||||||
|  | 	studio: string, | ||||||
|  | 	{ | ||||||
|  | 		langs, | ||||||
|  | 		...opts | ||||||
|  | 	}: { | ||||||
|  | 		filter?: string; | ||||||
|  | 		limit?: number; | ||||||
|  | 		after?: string; | ||||||
|  | 		sort?: string | string[]; | ||||||
|  | 		query?: string; | ||||||
|  | 		langs?: string; | ||||||
|  | 		preferOriginal?: boolean; | ||||||
|  | 	}, | ||||||
|  | ) => { | ||||||
|  | 	const resp = await app.handle( | ||||||
|  | 		new Request(buildUrl(`studios/${studio}/shows`, opts), { | ||||||
|  | 			method: "GET", | ||||||
|  | 			headers: langs | ||||||
|  | 				? { | ||||||
|  | 						"Accept-Language": langs, | ||||||
|  | 					} | ||||||
|  | 				: {}, | ||||||
|  | 		}), | ||||||
|  | 	); | ||||||
|  | 	const body = await resp.json(); | ||||||
|  | 	return [resp, body] as const; | ||||||
|  | }; | ||||||
| @ -41,6 +41,7 @@ describe("with a null value", () => { | |||||||
| 			airDate: null, | 			airDate: null, | ||||||
| 			originalLanguage: null, | 			originalLanguage: null, | ||||||
| 			externalId: {}, | 			externalId: {}, | ||||||
|  | 			studios: [], | ||||||
| 		}); | 		}); | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										64
									
								
								api/tests/series/studios.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								api/tests/series/studios.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | |||||||
|  | import { beforeAll, describe, expect, it } from "bun:test"; | ||||||
|  | import { getSerie, getShowsByStudio, getStudio } from "tests/helpers"; | ||||||
|  | import { expectStatus } from "tests/utils"; | ||||||
|  | import { seedSerie } from "~/controllers/seed/series"; | ||||||
|  | import { madeInAbyss } from "~/models/examples"; | ||||||
|  | 
 | ||||||
|  | beforeAll(async () => { | ||||||
|  | 	await seedSerie(madeInAbyss); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | describe("Get by studio", () => { | ||||||
|  | 	it("Invalid slug", async () => { | ||||||
|  | 		const [resp, body] = await getShowsByStudio("sotneuhn", { langs: "en" }); | ||||||
|  | 
 | ||||||
|  | 		expectStatus(resp, body).toBe(404); | ||||||
|  | 		expect(body).toMatchObject({ | ||||||
|  | 			status: 404, | ||||||
|  | 			message: expect.any(String), | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  | 	it("Get serie from studio", async () => { | ||||||
|  | 		const [resp, body] = await getShowsByStudio(madeInAbyss.studios[0].slug, { | ||||||
|  | 			langs: "en", | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		expectStatus(resp, body).toBe(200); | ||||||
|  | 		expect(body.items).toBeArrayOfSize(1); | ||||||
|  | 		expect(body.items[0].slug).toBe(madeInAbyss.slug); | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | describe("Get a studio", () => { | ||||||
|  | 	it("Invalid slug", async () => { | ||||||
|  | 		const [resp, body] = await getStudio("sotneuhn", { langs: "en" }); | ||||||
|  | 
 | ||||||
|  | 		expectStatus(resp, body).toBe(404); | ||||||
|  | 		expect(body).toMatchObject({ | ||||||
|  | 			status: 404, | ||||||
|  | 			message: expect.any(String), | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  | 	it("Get by id", async () => { | ||||||
|  | 		const slug = madeInAbyss.studios[0].slug; | ||||||
|  | 		const [resp, body] = await getStudio(slug, { langs: "en" }); | ||||||
|  | 
 | ||||||
|  | 		expectStatus(resp, body).toBe(200); | ||||||
|  | 		expect(body.slug).toBe(slug); | ||||||
|  | 	}); | ||||||
|  | 	it("Get using /shows?with=", async () => { | ||||||
|  | 		const [resp, body] = await getSerie(madeInAbyss.slug, { | ||||||
|  | 			langs: "en", | ||||||
|  | 			with: ["studios"], | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		expectStatus(resp, body).toBe(200); | ||||||
|  | 		expect(body.slug).toBe(madeInAbyss.slug); | ||||||
|  | 		expect(body.studios).toBeArrayOfSize(1); | ||||||
|  | 		const studio = madeInAbyss.studios[0]; | ||||||
|  | 		expect(body.studios[0]).toMatchObject({ | ||||||
|  | 			slug: studio.slug, | ||||||
|  | 			name: studio.translations.en.name, | ||||||
|  | 		}); | ||||||
|  | 	}); | ||||||
|  | }); | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user