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
					
				| @ -98,22 +98,31 @@ erDiagram | ||||
| 	seasons ||--o{ entries : has | ||||
| 	shows ||--|{ seasons : has | ||||
| 
 | ||||
| 	users { | ||||
| 		guid id PK | ||||
| 	} | ||||
| 
 | ||||
| 	watched_shows { | ||||
| 		guid show_id PK, FK | ||||
| 		guid user_id PK, FK | ||||
|         status status "completed|watching|droped|planned" | ||||
| 		status status "completed|watching|dropped|planned" | ||||
| 		uint seen_entry_count "NN" | ||||
| 		guid next_entry FK | ||||
| 	} | ||||
| 	shows ||--|{ watched_shows : has | ||||
| 	users ||--|{ watched_shows : has | ||||
| 	watched_shows ||--|o entries : next_entry | ||||
| 
 | ||||
|     watched_entries { | ||||
|         guid entry_id PK, FK | ||||
|         guid user_id PK, FK | ||||
| 	history { | ||||
| 		int id PK | ||||
| 		guid entry_id FK | ||||
| 		guid user_id FK | ||||
| 		uint time "in seconds, null of finished" | ||||
| 		uint progress "NN, from 0 to 100" | ||||
| 		datetime played_date | ||||
| 	} | ||||
|     entries ||--|{ watched_entries : has | ||||
| 	entries ||--|{ history : part_of | ||||
| 	users ||--|{ history : has | ||||
| 
 | ||||
| 	roles { | ||||
| 		guid show_id PK, FK | ||||
| @ -160,5 +169,5 @@ erDiagram | ||||
| 		string name | ||||
| 	} | ||||
| 	studios ||--|{ studio_translations : has | ||||
|     shows ||--|{ studios : 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, | ||||
| 			"tag": "0009_collections", | ||||
| 			"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( | ||||
| 			db | ||||
| 				.select({ | ||||
| 					entry: sql<number>`vids.entryPk::integer`.as("entry"), | ||||
| 					video: sql`${videos.pk}`.as("video"), | ||||
| 					entryPk: sql<number>`vids.entryPk::integer`.as("entry"), | ||||
| 					videoPk: sql`${videos.pk}`.as("video"), | ||||
| 					slug: computeVideoSlug( | ||||
| 						sql`${show.slug}::text`, | ||||
| 						sql`vids.needRendering::boolean`, | ||||
| @ -154,7 +154,7 @@ export const insertEntries = async ( | ||||
| 		.onConflictDoNothing() | ||||
| 		.returning({ | ||||
| 			slug: entryVideoJoin.slug, | ||||
| 			entryPk: entryVideoJoin.entry, | ||||
| 			entryPk: entryVideoJoin.entryPk, | ||||
| 		}); | ||||
| 
 | ||||
| 	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 { insertEntries } from "./insert/entries"; | ||||
| import { insertShow } from "./insert/shows"; | ||||
| import { insertStudios } from "./insert/studios"; | ||||
| import { guessNextRefresh } from "./refresh"; | ||||
| 
 | ||||
| export const SeedMovieResponse = t.Object({ | ||||
| @ -18,6 +19,12 @@ export const SeedMovieResponse = t.Object({ | ||||
| 			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; | ||||
| 
 | ||||
| @ -38,7 +45,7 @@ export const seedMovie = async ( | ||||
| 		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 col = await insertCollection(collection, { | ||||
| @ -74,11 +81,14 @@ export const seedMovie = async ( | ||||
| 		}, | ||||
| 	]); | ||||
| 
 | ||||
| 	const retStudios = await insertStudios(studios, show.pk); | ||||
| 
 | ||||
| 	return { | ||||
| 		updated: show.updated, | ||||
| 		id: show.id, | ||||
| 		slug: show.slug, | ||||
| 		videos: entry.videos, | ||||
| 		collection: col, | ||||
| 		studios: retStudios, | ||||
| 	}; | ||||
| }; | ||||
|  | ||||
| @ -5,6 +5,7 @@ import { insertCollection } from "./insert/collection"; | ||||
| import { insertEntries } from "./insert/entries"; | ||||
| import { insertSeasons } from "./insert/seasons"; | ||||
| import { insertShow } from "./insert/shows"; | ||||
| import { insertStudios } from "./insert/studios"; | ||||
| import { guessNextRefresh } from "./refresh"; | ||||
| 
 | ||||
| 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; | ||||
| 
 | ||||
| @ -65,7 +72,15 @@ export const seedSerie = async ( | ||||
| 		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 col = await insertCollection(collection, { | ||||
| @ -92,6 +107,8 @@ export const seedSerie = async ( | ||||
| 		(extras ?? []).map((x) => ({ ...x, kind: "extra", extraKind: x.kind })), | ||||
| 	); | ||||
| 
 | ||||
| 	const retStudios = await insertStudios(studios, show.pk); | ||||
| 
 | ||||
| 	return { | ||||
| 		updated: show.updated, | ||||
| 		id: show.id, | ||||
| @ -100,5 +117,6 @@ export const seedSerie = async ( | ||||
| 		entries: retEntries, | ||||
| 		extras: retExtras, | ||||
| 		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 { duneCollection } from "~/models/examples"; | ||||
| import { Movie } from "~/models/movie"; | ||||
| import { Serie } from "~/models/serie"; | ||||
| import { Show } from "~/models/show"; | ||||
| import { | ||||
| 	AcceptLanguage, | ||||
| 	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( | ||||
| 		"/:id/movies", | ||||
| 		async ({ | ||||
| @ -216,32 +246,6 @@ export const collections = new Elysia({ | ||||
| 		}, | ||||
| 		{ | ||||
| 			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: { | ||||
| 				200: Page(Movie), | ||||
| 				404: { | ||||
| @ -297,34 +301,8 @@ export const collections = new Elysia({ | ||||
| 		}, | ||||
| 		{ | ||||
| 			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: { | ||||
| 				200: Page(Movie), | ||||
| 				200: Page(Serie), | ||||
| 				404: { | ||||
| 					...KError, | ||||
| 					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" }, | ||||
| 			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: { | ||||
| 				200: Page(Movie), | ||||
| 				200: Page(Show), | ||||
| 				404: { | ||||
| 					...KError, | ||||
| 					description: "No collection found with the given id or slug.", | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import type { StaticDecode } from "@sinclair/typebox"; | ||||
| import { type SQL, and, eq, sql } from "drizzle-orm"; | ||||
| import { db } from "~/db"; | ||||
| import { showTranslations, shows } from "~/db/schema"; | ||||
| import { showTranslations, shows, studioTranslations } from "~/db/schema"; | ||||
| import { getColumns, sqlarr } from "~/db/utils"; | ||||
| import type { MovieStatus } from "~/models/movie"; | ||||
| import { SerieStatus } from "~/models/serie"; | ||||
| @ -12,6 +12,7 @@ import { | ||||
| 	Sort, | ||||
| 	isUuid, | ||||
| 	keysetPaginate, | ||||
| 	selectTranslationQuery, | ||||
| 	sortToSql, | ||||
| } from "~/models/utils"; | ||||
| 
 | ||||
| @ -130,7 +131,7 @@ export async function getShow( | ||||
| 	}: { | ||||
| 		languages: string[]; | ||||
| 		preferOriginal: boolean | undefined; | ||||
| 		relations: ("translations" | "videos")[]; | ||||
| 		relations: ("translations" | "studios" | "videos")[]; | ||||
| 		filters: SQL | undefined; | ||||
| 	}, | ||||
| ) { | ||||
| @ -141,18 +142,7 @@ export async function getShow( | ||||
| 		}, | ||||
| 		where: and(isUuid(id) ? eq(shows.id, id) : eq(shows.slug, id), filters), | ||||
| 		with: { | ||||
| 			selectedTranslation: { | ||||
| 				columns: { | ||||
| 					pk: false, | ||||
| 				}, | ||||
| 				where: !languages.includes("*") | ||||
| 					? eq(showTranslations.language, sql`any(${sqlarr(languages)})`) | ||||
| 					: undefined, | ||||
| 				orderBy: [ | ||||
| 					sql`array_position(${sqlarr(languages)}, ${showTranslations.language})`, | ||||
| 				], | ||||
| 				limit: 1, | ||||
| 			}, | ||||
| 			selectedTranslation: selectTranslationQuery(showTranslations, languages), | ||||
| 			originalTranslation: { | ||||
| 				columns: { | ||||
| 					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; | ||||
| @ -184,6 +191,7 @@ export async function getShow( | ||||
| 	const show = { | ||||
| 		...ret, | ||||
| 		...translation, | ||||
| 		kind: ret.kind as any, | ||||
| 		...(ot?.preferOriginal && { | ||||
| 			...(ot.poster && { poster: ot.poster }), | ||||
| 			...(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 }; | ||||
| } | ||||
|  | ||||
| @ -65,7 +65,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] }) | ||||
| 				preferOriginal: t.Optional( | ||||
| 					t.Boolean({ description: desc.preferOriginal }), | ||||
| 				), | ||||
| 				with: t.Array(t.UnionEnum(["translations", "videos"]), { | ||||
| 				with: t.Array(t.UnionEnum(["translations", "studios", "videos"]), { | ||||
| 					default: [], | ||||
| 					description: "Include related resources in the response.", | ||||
| 				}), | ||||
|  | ||||
| @ -65,7 +65,7 @@ export const series = new Elysia({ prefix: "/series", tags: ["series"] }) | ||||
| 				preferOriginal: t.Optional( | ||||
| 					t.Boolean({ description: desc.preferOriginal }), | ||||
| 				), | ||||
| 				with: t.Array(t.UnionEnum(["translations"]), { | ||||
| 				with: t.Array(t.UnionEnum(["translations", "studios"]), { | ||||
| 					default: [], | ||||
| 					description: "Include related resources in the response.", | ||||
| 				}), | ||||
|  | ||||
| @ -2,10 +2,8 @@ import { and, isNull, sql } from "drizzle-orm"; | ||||
| import { Elysia, t } from "elysia"; | ||||
| import { db } from "~/db"; | ||||
| import { shows } from "~/db/schema"; | ||||
| import { Collection } from "~/models/collections"; | ||||
| import { KError } from "~/models/error"; | ||||
| import { Movie } from "~/models/movie"; | ||||
| import { Serie } from "~/models/serie"; | ||||
| import { Show } from "~/models/show"; | ||||
| import { | ||||
| 	AcceptLanguage, | ||||
| 	Filter, | ||||
| @ -16,8 +14,6 @@ import { | ||||
| import { desc } from "~/models/utils/descriptions"; | ||||
| import { getShows, showFilters, showSort } from "./logic"; | ||||
| 
 | ||||
| const Show = t.Union([Movie, Serie, Collection]); | ||||
| 
 | ||||
| export const showsH = new Elysia({ prefix: "/shows", tags: ["shows"] }) | ||||
| 	.model({ | ||||
| 		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); | ||||
| 
 | ||||
| 			// TODO: this is a huge untested wip
 | ||||
| 			// biome-ignore lint/correctness/noUnreachable: leave me alone
 | ||||
| 			const vidsI = db.$with("vidsI").as( | ||||
| 				db.insert(videos).values(body).onConflictDoNothing().returning({ | ||||
| 					pk: videos.pk, | ||||
| 					id: videos.id, | ||||
| 					path: videos.path, | ||||
| 					guess: videos.guess, | ||||
| 				}), | ||||
| 			); | ||||
| 
 | ||||
| 			const findEntriesQ = db | ||||
| 				.select({ | ||||
| 					guess: videos.guess, | ||||
| 					entryPk: entries.pk, | ||||
| 					showSlug: shows.slug, | ||||
| 					// TODO: handle extras here
 | ||||
| 					// guessit can't know if an episode is a special or not. treat specials like a normal episode.
 | ||||
| 					kind: sql` | ||||
| 						case when ${entries.kind} = 'movie' then 'movie' else 'episode' end | ||||
| 					`.as("kind"),
 | ||||
| 					season: entries.seasonNumber, | ||||
| 					episode: entries.episodeNumber, | ||||
| 				}) | ||||
| 				.from(entries) | ||||
| 				.leftJoin(entryVideoJoin, eq(entryVideoJoin.entry, entries.pk)) | ||||
| 				.leftJoin(videos, eq(videos.pk, entryVideoJoin.video)) | ||||
| 				.leftJoin(shows, eq(shows.pk, entries.showPk)) | ||||
| 				.as("find_entries"); | ||||
| 
 | ||||
| 			const hasRenderingQ = db | ||||
| 				.select() | ||||
| 				.from(entryVideoJoin) | ||||
| 				.where(eq(entryVideoJoin.entry, findEntriesQ.entryPk)); | ||||
| 
 | ||||
| 			const ret = await db | ||||
| 				.with(vidsI) | ||||
| 				.insert(entryVideoJoin) | ||||
| 				.select( | ||||
| 					db | ||||
| 						.select({ | ||||
| 							entry: findEntriesQ.entryPk, | ||||
| 							video: vidsI.pk, | ||||
| 							slug: computeVideoSlug( | ||||
| 								findEntriesQ.showSlug, | ||||
| 								sql`exists(${hasRenderingQ})`, | ||||
| 							), | ||||
| 						}) | ||||
| 						.from(vidsI) | ||||
| 						.leftJoin( | ||||
| 							findEntriesQ, | ||||
| 							and( | ||||
| 								eq( | ||||
| 									sql`${findEntriesQ.guess}->'title'`, | ||||
| 									sql`${vidsI.guess}->'title'`, | ||||
| 								), | ||||
| 								// TODO: find if @> with a jsonb created on the fly is
 | ||||
| 								// better than multiples checks
 | ||||
| 								sql`${vidsI.guess} @> {"kind": }::jsonb`, | ||||
| 								inArray(findEntriesQ.kind, sql`${vidsI.guess}->'type'`), | ||||
| 								inArray(findEntriesQ.episode, sql`${vidsI.guess}->'episode'`), | ||||
| 								inArray(findEntriesQ.season, sql`${vidsI.guess}->'season'`), | ||||
| 							), | ||||
| 						), | ||||
| 				) | ||||
| 				.onConflictDoNothing() | ||||
| 				.returning({ | ||||
| 					slug: entryVideoJoin.slug, | ||||
| 					entryPk: entryVideoJoin.entry, | ||||
| 					id: vidsI.id, | ||||
| 					path: vidsI.path, | ||||
| 				}); | ||||
| 			return error(201, ret as any); | ||||
| 			// const vidsI = db.$with("vidsI").as(
 | ||||
| 			// 	db.insert(videos).values(body).onConflictDoNothing().returning({
 | ||||
| 			// 		pk: videos.pk,
 | ||||
| 			// 		id: videos.id,
 | ||||
| 			// 		path: videos.path,
 | ||||
| 			// 		guess: videos.guess,
 | ||||
| 			// 	}),
 | ||||
| 			// );
 | ||||
| 			//
 | ||||
| 			// const findEntriesQ = db
 | ||||
| 			// 	.select({
 | ||||
| 			// 		guess: videos.guess,
 | ||||
| 			// 		entryPk: entries.pk,
 | ||||
| 			// 		showSlug: shows.slug,
 | ||||
| 			// 		// TODO: handle extras here
 | ||||
| 			// 		// guessit can't know if an episode is a special or not. treat specials like a normal episode.
 | ||||
| 			// 		kind: sql`
 | ||||
| 			// 			case when ${entries.kind} = 'movie' then 'movie' else 'episode' end
 | ||||
| 			// 		`.as("kind"),
 | ||||
| 			// 		season: entries.seasonNumber,
 | ||||
| 			// 		episode: entries.episodeNumber,
 | ||||
| 			// 	})
 | ||||
| 			// 	.from(entries)
 | ||||
| 			// 	.leftJoin(entryVideoJoin, eq(entryVideoJoin.entry, entries.pk))
 | ||||
| 			// 	.leftJoin(videos, eq(videos.pk, entryVideoJoin.video))
 | ||||
| 			// 	.leftJoin(shows, eq(shows.pk, entries.showPk))
 | ||||
| 			// 	.as("find_entries");
 | ||||
| 			//
 | ||||
| 			// const hasRenderingQ = db
 | ||||
| 			// 	.select()
 | ||||
| 			// 	.from(entryVideoJoin)
 | ||||
| 			// 	.where(eq(entryVideoJoin.entry, findEntriesQ.entryPk));
 | ||||
| 			//
 | ||||
| 			// const ret = await db
 | ||||
| 			// 	.with(vidsI)
 | ||||
| 			// 	.insert(entryVideoJoin)
 | ||||
| 			// 	.select(
 | ||||
| 			// 		db
 | ||||
| 			// 			.select({
 | ||||
| 			// 				entry: findEntriesQ.entryPk,
 | ||||
| 			// 				video: vidsI.pk,
 | ||||
| 			// 				slug: computeVideoSlug(
 | ||||
| 			// 					findEntriesQ.showSlug,
 | ||||
| 			// 					sql`exists(${hasRenderingQ})`,
 | ||||
| 			// 				),
 | ||||
| 			// 			})
 | ||||
| 			// 			.from(vidsI)
 | ||||
| 			// 			.leftJoin(
 | ||||
| 			// 				findEntriesQ,
 | ||||
| 			// 				and(
 | ||||
| 			// 					eq(
 | ||||
| 			// 						sql`${findEntriesQ.guess}->'title'`,
 | ||||
| 			// 						sql`${vidsI.guess}->'title'`,
 | ||||
| 			// 					),
 | ||||
| 			// 					// TODO: find if @> with a jsonb created on the fly is
 | ||||
| 			// 					// better than multiples checks
 | ||||
| 			// 					sql`${vidsI.guess} @> {"kind": }::jsonb`,
 | ||||
| 			// 					inArray(findEntriesQ.kind, sql`${vidsI.guess}->'type'`),
 | ||||
| 			// 					inArray(findEntriesQ.episode, sql`${vidsI.guess}->'episode'`),
 | ||||
| 			// 					inArray(findEntriesQ.season, sql`${vidsI.guess}->'season'`),
 | ||||
| 			// 				),
 | ||||
| 			// 			),
 | ||||
| 			// 	)
 | ||||
| 			// 	.onConflictDoNothing()
 | ||||
| 			// 	.returning({
 | ||||
| 			// 		slug: entryVideoJoin.slug,
 | ||||
| 			// 		entryPk: entryVideoJoin.entry,
 | ||||
| 			// 		id: vidsI.id,
 | ||||
| 			// 		path: vidsI.path,
 | ||||
| 			// 	});
 | ||||
| 			// return error(201, ret as any);
 | ||||
| 		}, | ||||
| 		{ | ||||
| 			body: t.Array(SeedVideo), | ||||
|  | ||||
| @ -2,6 +2,7 @@ import { relations, sql } from "drizzle-orm"; | ||||
| import { | ||||
| 	check, | ||||
| 	date, | ||||
| 	index, | ||||
| 	integer, | ||||
| 	jsonb, | ||||
| 	primaryKey, | ||||
| @ -70,11 +71,17 @@ export const entries = schema.table( | ||||
| 		createdAt: timestamp({ withTimezone: true, mode: "string" }) | ||||
| 			.notNull() | ||||
| 			.defaultNow(), | ||||
| 		updatedAt: timestamp({ withTimezone: true, mode: "string" }) | ||||
| 			.notNull() | ||||
| 			.$onUpdate(() => sql`now()`), | ||||
| 		nextRefresh: timestamp({ withTimezone: true, mode: "string" }).notNull(), | ||||
| 	}, | ||||
| 	(t) => [ | ||||
| 		unique().on(t.showPk, t.seasonNumber, t.episodeNumber), | ||||
| 		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(), | ||||
| 		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 }) => ({ | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| export * from "./entries"; | ||||
| export * from "./seasons"; | ||||
| export * from "./shows"; | ||||
| export * from "./studios"; | ||||
| export * from "./videos"; | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { relations } from "drizzle-orm"; | ||||
| import { relations, sql } from "drizzle-orm"; | ||||
| import { | ||||
| 	date, | ||||
| 	index, | ||||
| @ -45,11 +45,15 @@ export const seasons = schema.table( | ||||
| 		createdAt: timestamp({ withTimezone: true, mode: "string" }) | ||||
| 			.notNull() | ||||
| 			.defaultNow(), | ||||
| 		updatedAt: timestamp({ withTimezone: true, mode: "string" }) | ||||
| 			.notNull() | ||||
| 			.$onUpdate(() => sql`now()`), | ||||
| 		nextRefresh: timestamp({ withTimezone: true, mode: "string" }).notNull(), | ||||
| 	}, | ||||
| 	(t) => [ | ||||
| 		unique().on(t.showPk, t.seasonNumber), | ||||
| 		index("show_fk").using("hash", t.showPk), | ||||
| 		index("season_nbr").on(t.seasonNumber), | ||||
| 	], | ||||
| ); | ||||
| 
 | ||||
| @ -66,7 +70,10 @@ export const seasonTranslations = schema.table( | ||||
| 		thumbnail: 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 }) => ({ | ||||
|  | ||||
| @ -5,7 +5,6 @@ import { | ||||
| 	date, | ||||
| 	index, | ||||
| 	integer, | ||||
| 	jsonb, | ||||
| 	primaryKey, | ||||
| 	smallint, | ||||
| 	text, | ||||
| @ -15,7 +14,8 @@ import { | ||||
| } from "drizzle-orm/pg-core"; | ||||
| import { entries } from "./entries"; | ||||
| 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", [ | ||||
| 	"serie", | ||||
| @ -54,20 +54,6 @@ export const genres = schema.enum("genres", [ | ||||
| 	"talk", | ||||
| ]); | ||||
| 
 | ||||
| export const externalid = () => | ||||
| 	jsonb() | ||||
| 		.$type< | ||||
| 			Record< | ||||
| 				string, | ||||
| 				{ | ||||
| 					dataId: string; | ||||
| 					link: string | null; | ||||
| 				} | ||||
| 			> | ||||
| 		>() | ||||
| 		.notNull() | ||||
| 		.default({}); | ||||
| 
 | ||||
| export const shows = schema.table( | ||||
| 	"shows", | ||||
| 	{ | ||||
| @ -92,6 +78,9 @@ export const shows = schema.table( | ||||
| 		createdAt: timestamp({ withTimezone: true, mode: "string" }) | ||||
| 			.notNull() | ||||
| 			.defaultNow(), | ||||
| 		updatedAt: timestamp({ withTimezone: true, mode: "string" }) | ||||
| 			.notNull() | ||||
| 			.$onUpdate(() => sql`now()`), | ||||
| 		nextRefresh: timestamp({ withTimezone: true, mode: "string" }).notNull(), | ||||
| 	}, | ||||
| 	(t) => [ | ||||
| @ -141,6 +130,7 @@ export const showsRelations = relations(shows, ({ many, one }) => ({ | ||||
| 	}), | ||||
| 	entries: many(entries, { relationName: "show_entries" }), | ||||
| 	seasons: many(seasons, { relationName: "show_seasons" }), | ||||
| 	studios: many(showStudioJoin, { relationName: "ssj_show" }), | ||||
| })); | ||||
| export const showsTrRelations = relations(showTranslations, ({ one }) => ({ | ||||
| 	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 = () => | ||||
| 	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" }) | ||||
| 			.notNull() | ||||
| 			.defaultNow(), | ||||
| 		updatedAt: timestamp({ withTimezone: true, mode: "string" }) | ||||
| 			.notNull() | ||||
| 			.$onUpdate(() => sql`now()`), | ||||
| 	}, | ||||
| 	(t) => [ | ||||
| 		check("part_pos", sql`${t.part} >= 0`), | ||||
| @ -36,15 +39,15 @@ export const videos = schema.table( | ||||
| export const entryVideoJoin = schema.table( | ||||
| 	"entry_video_join", | ||||
| 	{ | ||||
| 		entry: integer() | ||||
| 		entryPk: integer() | ||||
| 			.notNull() | ||||
| 			.references(() => entries.pk, { onDelete: "cascade" }), | ||||
| 		video: integer() | ||||
| 		videoPk: integer() | ||||
| 			.notNull() | ||||
| 			.references(() => videos.pk, { onDelete: "cascade" }), | ||||
| 		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 }) => ({ | ||||
| @ -56,12 +59,12 @@ export const videosRelations = relations(videos, ({ many }) => ({ | ||||
| export const evjRelations = relations(entryVideoJoin, ({ one }) => ({ | ||||
| 	video: one(videos, { | ||||
| 		relationName: "evj_video", | ||||
| 		fields: [entryVideoJoin.video], | ||||
| 		fields: [entryVideoJoin.videoPk], | ||||
| 		references: [videos.pk], | ||||
| 	}), | ||||
| 	entry: one(entries, { | ||||
| 		relationName: "evj_entry", | ||||
| 		fields: [entryVideoJoin.entry], | ||||
| 		fields: [entryVideoJoin.entryPk], | ||||
| 		references: [entries.pk], | ||||
| 	}), | ||||
| })); | ||||
|  | ||||
| @ -6,6 +6,7 @@ import { collections } from "./controllers/shows/collections"; | ||||
| import { movies } from "./controllers/shows/movies"; | ||||
| import { series } from "./controllers/shows/series"; | ||||
| import { showsH } from "./controllers/shows/shows"; | ||||
| import { studiosH } from "./controllers/studios"; | ||||
| import { videosH } from "./controllers/videos"; | ||||
| import type { KError } from "./models/error"; | ||||
| 
 | ||||
| @ -48,4 +49,5 @@ export const app = new Elysia() | ||||
| 	.use(entriesH) | ||||
| 	.use(seasonsH) | ||||
| 	.use(videosH) | ||||
| 	.use(studiosH) | ||||
| 	.use(seed); | ||||
|  | ||||
| @ -63,6 +63,7 @@ app | ||||
| 							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 { bubbleImages, duneCollection, registerExamples } from "./examples"; | ||||
| import { | ||||
| 	DbMetadata, | ||||
| 	ExternalId, | ||||
| 	Genre, | ||||
| 	Image, | ||||
| @ -33,10 +34,9 @@ const BaseCollection = t.Object({ | ||||
| 		}), | ||||
| 	), | ||||
| 
 | ||||
| 	createdAt: t.String({ format: "date-time" }), | ||||
| 	nextRefresh: t.String({ format: "date-time" }), | ||||
| 
 | ||||
| 	externalId: ExternalId, | ||||
| 	externalId: ExternalId(), | ||||
| }); | ||||
| 
 | ||||
| export const CollectionTranslation = t.Object({ | ||||
| @ -56,6 +56,7 @@ export const Collection = t.Intersect([ | ||||
| 	Resource(), | ||||
| 	CollectionTranslation, | ||||
| 	BaseCollection, | ||||
| 	DbMetadata, | ||||
| ]); | ||||
| export type Collection = Prettify<typeof Collection.static>; | ||||
| 
 | ||||
| @ -68,13 +69,7 @@ export const FullCollection = t.Intersect([ | ||||
| export type FullCollection = Prettify<typeof FullCollection.static>; | ||||
| 
 | ||||
| export const SeedCollection = t.Intersect([ | ||||
| 	t.Omit(BaseCollection, [ | ||||
| 		"kind", | ||||
| 		"startAir", | ||||
| 		"endAir", | ||||
| 		"createdAt", | ||||
| 		"nextRefresh", | ||||
| 	]), | ||||
| 	t.Omit(BaseCollection, ["kind", "startAir", "endAir", "nextRefresh"]), | ||||
| 	t.Object({ | ||||
| 		slug: t.String({ format: "slug" }), | ||||
| 		translations: TranslationRecord( | ||||
|  | ||||
| @ -12,7 +12,6 @@ export const BaseEntry = () => | ||||
| 		), | ||||
| 		thumbnail: t.Nullable(Image), | ||||
| 
 | ||||
| 		createdAt: t.String({ format: "date-time" }), | ||||
| 		nextRefresh: t.String({ format: "date-time" }), | ||||
| 	}); | ||||
| 
 | ||||
|  | ||||
| @ -1,7 +1,13 @@ | ||||
| import { t } from "elysia"; | ||||
| import type { Prettify } from "~/utils"; | ||||
| 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"; | ||||
| 
 | ||||
| export const BaseEpisode = t.Intersect([ | ||||
| @ -19,11 +25,12 @@ export const Episode = t.Intersect([ | ||||
| 	Resource(), | ||||
| 	EntryTranslation(), | ||||
| 	BaseEpisode, | ||||
| 	DbMetadata, | ||||
| ]); | ||||
| export type Episode = Prettify<typeof Episode.static>; | ||||
| 
 | ||||
| export const SeedEpisode = t.Intersect([ | ||||
| 	t.Omit(BaseEpisode, ["thumbnail", "createdAt", "nextRefresh"]), | ||||
| 	t.Omit(BaseEpisode, ["thumbnail", "nextRefresh"]), | ||||
| 	t.Object({ | ||||
| 		thumbnail: t.Nullable(SeedImage), | ||||
| 		translations: TranslationRecord(EntryTranslation()), | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| import { t } from "elysia"; | ||||
| import { type Prettify, comment } from "~/utils"; | ||||
| import { madeInAbyss, registerExamples } from "../examples"; | ||||
| import { SeedImage } from "../utils"; | ||||
| import { DbMetadata, SeedImage } from "../utils"; | ||||
| import { Resource } from "../utils/resource"; | ||||
| 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 const SeedExtra = t.Intersect([ | ||||
| 	t.Omit(BaseExtra, ["thumbnail", "createdAt"]), | ||||
| 	t.Omit(BaseExtra, ["thumbnail"]), | ||||
| 	t.Object({ | ||||
| 		slug: t.String({ format: "slug" }), | ||||
| 		thumbnail: t.Nullable(SeedImage), | ||||
|  | ||||
| @ -2,6 +2,7 @@ import { t } from "elysia"; | ||||
| import { type Prettify, comment } from "~/utils"; | ||||
| import { bubbleImages, madeInAbyss, registerExamples } from "../examples"; | ||||
| import { | ||||
| 	DbMetadata, | ||||
| 	ExternalId, | ||||
| 	Image, | ||||
| 	Resource, | ||||
| @ -18,7 +19,7 @@ export const BaseMovieEntry = t.Intersect( | ||||
| 				minimum: 1, | ||||
| 				description: "Absolute playback order. Can be mixed with episodes.", | ||||
| 			}), | ||||
| 			externalId: ExternalId, | ||||
| 			externalId: ExternalId(), | ||||
| 		}), | ||||
| 		BaseEntry(), | ||||
| 	], | ||||
| @ -42,11 +43,12 @@ export const MovieEntry = t.Intersect([ | ||||
| 	Resource(), | ||||
| 	MovieEntryTranslation, | ||||
| 	BaseMovieEntry, | ||||
| 	DbMetadata, | ||||
| ]); | ||||
| export type MovieEntry = Prettify<typeof MovieEntry.static>; | ||||
| 
 | ||||
| export const SeedMovieEntry = t.Intersect([ | ||||
| 	t.Omit(BaseMovieEntry, ["thumbnail", "createdAt", "nextRefresh"]), | ||||
| 	t.Omit(BaseMovieEntry, ["thumbnail", "nextRefresh"]), | ||||
| 	t.Object({ | ||||
| 		slug: t.Optional(t.String({ format: "slug" })), | ||||
| 		thumbnail: t.Nullable(SeedImage), | ||||
|  | ||||
| @ -1,7 +1,13 @@ | ||||
| import { t } from "elysia"; | ||||
| import { type Prettify, comment } from "~/utils"; | ||||
| 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"; | ||||
| 
 | ||||
| export const BaseSpecial = t.Intersect( | ||||
| @ -29,11 +35,12 @@ export const Special = t.Intersect([ | ||||
| 	Resource(), | ||||
| 	EntryTranslation(), | ||||
| 	BaseSpecial, | ||||
| 	DbMetadata, | ||||
| ]); | ||||
| export type Special = Prettify<typeof Special.static>; | ||||
| 
 | ||||
| export const SeedSpecial = t.Intersect([ | ||||
| 	t.Omit(BaseSpecial, ["thumbnail", "createdAt", "nextRefresh"]), | ||||
| 	t.Omit(BaseSpecial, ["thumbnail", "nextRefresh"]), | ||||
| 	t.Object({ | ||||
| 		thumbnail: t.Nullable(SeedImage), | ||||
| 		translations: TranslationRecord(EntryTranslation()), | ||||
|  | ||||
| @ -1,8 +1,7 @@ | ||||
| import { t } from "elysia"; | ||||
| import { type Prettify, comment } from "~/utils"; | ||||
| import { bubbleImages, registerExamples } from "../examples"; | ||||
| import { youtubeExample } from "../examples/others"; | ||||
| import { Resource } from "../utils/resource"; | ||||
| import { bubbleImages, registerExamples, youtubeExample } from "../examples"; | ||||
| import { DbMetadata, Resource } from "../utils"; | ||||
| import { BaseEntry, EntryTranslation } from "./base-entry"; | ||||
| 
 | ||||
| export const BaseUnknownEntry = t.Intersect( | ||||
| @ -28,6 +27,7 @@ export const UnknownEntry = t.Intersect([ | ||||
| 	Resource(), | ||||
| 	UnknownEntryTranslation, | ||||
| 	BaseUnknownEntry, | ||||
| 	DbMetadata, | ||||
| ]); | ||||
| export type UnknownEntry = Prettify<typeof UnknownEntry.static>; | ||||
| 
 | ||||
|  | ||||
| @ -9,6 +9,7 @@ export const bubbleVideo: Video = { | ||||
| 	part: null, | ||||
| 	version: 1, | ||||
| 	createdAt: "2024-11-23T15:01:24.968Z", | ||||
| 	updatedAt: "2024-11-23T15:01:24.968Z", | ||||
| }; | ||||
| 
 | ||||
| export const bubble: SeedMovie = { | ||||
| @ -60,6 +61,7 @@ export const bubble: SeedMovie = { | ||||
| 		}, | ||||
| 	}, | ||||
| 	videos: [bubbleVideo.id], | ||||
| 	studios: [], | ||||
| }; | ||||
| 
 | ||||
| export const bubbleImages = { | ||||
|  | ||||
| @ -9,6 +9,7 @@ export const dune1984Video: Video = { | ||||
| 	part: null, | ||||
| 	version: 1, | ||||
| 	createdAt: "2024-12-02T11:45:12.968Z", | ||||
| 	updatedAt: "2024-12-02T11:45:12.968Z", | ||||
| }; | ||||
| 
 | ||||
| export const dune1984: SeedMovie = { | ||||
| @ -47,6 +48,7 @@ export const dune1984: SeedMovie = { | ||||
| 		}, | ||||
| 	}, | ||||
| 	videos: [dune1984Video.id], | ||||
| 	studios: [], | ||||
| }; | ||||
| 
 | ||||
| export const dune1984Images = { | ||||
|  | ||||
| @ -9,6 +9,7 @@ export const duneVideo: Video = { | ||||
| 	part: null, | ||||
| 	version: 1, | ||||
| 	createdAt: "2024-12-02T10:10:24.968Z", | ||||
| 	updatedAt: "2024-12-02T10:10:24.968Z", | ||||
| }; | ||||
| 
 | ||||
| export const dune: SeedMovie = { | ||||
| @ -47,6 +48,7 @@ export const dune: SeedMovie = { | ||||
| 		}, | ||||
| 	}, | ||||
| 	videos: [duneVideo.id], | ||||
| 	studios: [], | ||||
| }; | ||||
| 
 | ||||
| export const duneImages = { | ||||
|  | ||||
| @ -16,6 +16,7 @@ export const madeInAbyssVideo: Video = { | ||||
| 		from: "guessit", | ||||
| 	}, | ||||
| 	createdAt: "2024-11-23T15:01:24.968Z", | ||||
| 	updatedAt: "2024-11-23T15:01:24.968Z", | ||||
| }; | ||||
| 
 | ||||
| export const madeInAbyss = { | ||||
| @ -242,4 +243,21 @@ export const madeInAbyss = { | ||||
| 			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; | ||||
|  | ||||
| @ -1,9 +1,10 @@ | ||||
| import { t } from "elysia"; | ||||
| import type { Prettify } from "~/utils"; | ||||
| import { SeedCollection } from "./collections"; | ||||
| import { bubble, registerExamples } from "./examples"; | ||||
| import { bubbleImages } from "./examples/bubble"; | ||||
| import { bubble, bubbleImages, registerExamples } from "./examples"; | ||||
| import { SeedStudio, Studio } from "./studio"; | ||||
| import { | ||||
| 	DbMetadata, | ||||
| 	ExternalId, | ||||
| 	Genre, | ||||
| 	Image, | ||||
| @ -33,10 +34,9 @@ const BaseMovie = t.Object({ | ||||
| 		}), | ||||
| 	), | ||||
| 
 | ||||
| 	createdAt: t.String({ format: "date-time" }), | ||||
| 	nextRefresh: t.String({ format: "date-time" }), | ||||
| 
 | ||||
| 	externalId: ExternalId, | ||||
| 	externalId: ExternalId(), | ||||
| }); | ||||
| 
 | ||||
| export const MovieTranslation = t.Object({ | ||||
| @ -58,6 +58,7 @@ export const Movie = t.Intersect([ | ||||
| 	Resource(), | ||||
| 	MovieTranslation, | ||||
| 	BaseMovie, | ||||
| 	DbMetadata, | ||||
| 	// t.Object({ isAvailable: t.Boolean() }),
 | ||||
| ]); | ||||
| export type Movie = Prettify<typeof Movie.static>; | ||||
| @ -67,12 +68,13 @@ export const FullMovie = t.Intersect([ | ||||
| 	t.Object({ | ||||
| 		translations: t.Optional(TranslationRecord(MovieTranslation)), | ||||
| 		videos: t.Optional(t.Array(Video)), | ||||
| 		studios: t.Optional(t.Array(Studio)), | ||||
| 	}), | ||||
| ]); | ||||
| export type FullMovie = Prettify<typeof FullMovie.static>; | ||||
| 
 | ||||
| export const SeedMovie = t.Intersect([ | ||||
| 	t.Omit(BaseMovie, ["kind", "createdAt", "nextRefresh"]), | ||||
| 	t.Omit(BaseMovie, ["kind", "nextRefresh"]), | ||||
| 	t.Object({ | ||||
| 		slug: t.String({ format: "slug", examples: ["bubble"] }), | ||||
| 		translations: TranslationRecord( | ||||
| @ -88,6 +90,7 @@ export const SeedMovie = t.Intersect([ | ||||
| 		), | ||||
| 		videos: t.Optional(t.Array(t.String({ format: "uuid" }))), | ||||
| 		collection: t.Optional(SeedCollection), | ||||
| 		studios: t.Array(SeedStudio), | ||||
| 	}), | ||||
| ]); | ||||
| export type SeedMovie = Prettify<typeof SeedMovie.static>; | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| import { t } from "elysia"; | ||||
| import type { Prettify } from "~/utils"; | ||||
| import { bubbleImages, madeInAbyss, registerExamples } from "./examples"; | ||||
| import { DbMetadata } from "./utils"; | ||||
| import { SeasonId } from "./utils/external-id"; | ||||
| import { Image, SeedImage } from "./utils/image"; | ||||
| import { TranslationRecord } from "./utils/language"; | ||||
| @ -11,7 +12,6 @@ export const BaseSeason = t.Object({ | ||||
| 	startAir: 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" }), | ||||
| 
 | ||||
| 	externalId: SeasonId, | ||||
| @ -27,11 +27,16 @@ export const SeasonTranslation = t.Object({ | ||||
| }); | ||||
| export type SeasonTranslation = typeof SeasonTranslation.static; | ||||
| 
 | ||||
| export const Season = t.Intersect([Resource(), SeasonTranslation, BaseSeason]); | ||||
| export type Season = typeof Season.static; | ||||
| export const Season = t.Intersect([ | ||||
| 	Resource(), | ||||
| 	SeasonTranslation, | ||||
| 	BaseSeason, | ||||
| 	DbMetadata, | ||||
| ]); | ||||
| export type Season = Prettify<typeof Season.static>; | ||||
| 
 | ||||
| export const SeedSeason = t.Intersect([ | ||||
| 	t.Omit(BaseSeason, ["createdAt", "nextRefresh"]), | ||||
| 	t.Omit(BaseSeason, ["nextRefresh"]), | ||||
| 	t.Object({ | ||||
| 		translations: TranslationRecord( | ||||
| 			t.Intersect([ | ||||
|  | ||||
| @ -4,11 +4,17 @@ import { SeedCollection } from "./collections"; | ||||
| import { SeedEntry, SeedExtra } from "./entry"; | ||||
| import { bubbleImages, madeInAbyss, registerExamples } from "./examples"; | ||||
| import { SeedSeason } from "./season"; | ||||
| import { ExternalId } from "./utils/external-id"; | ||||
| import { Genre } from "./utils/genres"; | ||||
| import { Image, SeedImage } from "./utils/image"; | ||||
| import { Language, TranslationRecord } from "./utils/language"; | ||||
| import { Resource } from "./utils/resource"; | ||||
| import { SeedStudio, Studio } from "./studio"; | ||||
| import { | ||||
| 	DbMetadata, | ||||
| 	ExternalId, | ||||
| 	Genre, | ||||
| 	Image, | ||||
| 	Language, | ||||
| 	Resource, | ||||
| 	SeedImage, | ||||
| 	TranslationRecord, | ||||
| } from "./utils"; | ||||
| 
 | ||||
| export const SerieStatus = t.UnionEnum([ | ||||
| 	"unknown", | ||||
| @ -18,7 +24,7 @@ export const SerieStatus = t.UnionEnum([ | ||||
| ]); | ||||
| export type SerieStatus = typeof SerieStatus.static; | ||||
| 
 | ||||
| export const BaseSerie = t.Object({ | ||||
| const BaseSerie = t.Object({ | ||||
| 	kind: t.Literal("serie"), | ||||
| 	genres: t.Array(Genre), | ||||
| 	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" }), | ||||
| 
 | ||||
| 	externalId: ExternalId, | ||||
| 	externalId: ExternalId(), | ||||
| }); | ||||
| 
 | ||||
| export const SerieTranslation = t.Object({ | ||||
| @ -59,19 +64,25 @@ export const SerieTranslation = t.Object({ | ||||
| }); | ||||
| export type SerieTranslation = typeof SerieTranslation.static; | ||||
| 
 | ||||
| export const Serie = t.Intersect([Resource(), SerieTranslation, BaseSerie]); | ||||
| export const Serie = t.Intersect([ | ||||
| 	Resource(), | ||||
| 	SerieTranslation, | ||||
| 	BaseSerie, | ||||
| 	DbMetadata, | ||||
| ]); | ||||
| export type Serie = Prettify<typeof Serie.static>; | ||||
| 
 | ||||
| export const FullSerie = t.Intersect([ | ||||
| 	Serie, | ||||
| 	t.Object({ | ||||
| 		translations: t.Optional(TranslationRecord(SerieTranslation)), | ||||
| 		studios: t.Optional(t.Array(Studio)), | ||||
| 	}), | ||||
| ]); | ||||
| export type FullMovie = Prettify<typeof FullSerie.static>; | ||||
| 
 | ||||
| export const SeedSerie = t.Intersect([ | ||||
| 	t.Omit(BaseSerie, ["kind", "createdAt", "nextRefresh"]), | ||||
| 	t.Omit(BaseSerie, ["kind", "nextRefresh"]), | ||||
| 	t.Object({ | ||||
| 		slug: t.String({ format: "slug" }), | ||||
| 		translations: TranslationRecord( | ||||
| @ -89,6 +100,7 @@ export const SeedSerie = t.Intersect([ | ||||
| 		entries: t.Array(SeedEntry), | ||||
| 		extras: t.Optional(t.Array(SeedExtra)), | ||||
| 		collection: t.Optional(SeedCollection), | ||||
| 		studios: t.Array(SeedStudio), | ||||
| 	}), | ||||
| ]); | ||||
| 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 { comment } from "../../utils"; | ||||
| 
 | ||||
| export const ExternalId = t.Record( | ||||
| export const ExternalId = () => | ||||
| 	t.Record( | ||||
| 		t.String(), | ||||
| 		t.Object({ | ||||
| 			dataId: t.String(), | ||||
| 			link: t.Nullable(t.String({ format: "uri" })), | ||||
| 		}), | ||||
| ); | ||||
| export type ExternalId = typeof ExternalId.static; | ||||
| 	); | ||||
| 
 | ||||
| export const EpisodeId = t.Record( | ||||
| 	t.String(), | ||||
|  | ||||
| @ -7,3 +7,4 @@ export * from "./filters"; | ||||
| export * from "./page"; | ||||
| export * from "./sort"; | ||||
| export * from "./keyset-paginate"; | ||||
| export * from "./db-metadata"; | ||||
|  | ||||
| @ -4,7 +4,9 @@ import { | ||||
| 	type TSchema, | ||||
| 	type TString, | ||||
| } from "@sinclair/typebox"; | ||||
| import { type Column, type Table, eq, sql } from "drizzle-orm"; | ||||
| import { t } from "elysia"; | ||||
| import { sqlarr } from "~/db/utils"; | ||||
| import { comment } from "../../utils"; | ||||
| 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 { comment } from "../utils"; | ||||
| import { t } from "elysia"; | ||||
| import { type Prettify, comment } from "~/utils"; | ||||
| import { bubbleVideo, registerExamples } from "./examples"; | ||||
| import { DbMetadata, Resource } from "./utils"; | ||||
| 
 | ||||
| export const Video = t.Object({ | ||||
| 	id: t.String({ format: "uuid" }), | ||||
| 	slug: t.String({ format: "slug" }), | ||||
| export const SeedVideo = t.Object({ | ||||
| 	path: t.String(), | ||||
| 	rendering: t.String({ | ||||
| 		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.", | ||||
| 	}), | ||||
| 
 | ||||
| 	createdAt: t.String({ format: "date-time" }), | ||||
| 
 | ||||
| 	guess: t.Optional( | ||||
| 		t.Recursive((Self) => | ||||
| 			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 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 "./series-helper"; | ||||
| export * from "./studio-helper"; | ||||
| export * from "./videos-helper"; | ||||
| 
 | ||||
| export * from "~/elysia"; | ||||
|  | ||||
| @ -4,7 +4,10 @@ import type { SeedMovie } from "~/models/movie"; | ||||
| 
 | ||||
| export const getMovie = async ( | ||||
| 	id: string, | ||||
| 	{ langs, ...query }: { langs?: string; preferOriginal?: boolean }, | ||||
| 	{ | ||||
| 		langs, | ||||
| 		...query | ||||
| 	}: { langs?: string; preferOriginal?: boolean; with?: string[] }, | ||||
| ) => { | ||||
| 	const resp = await app.handle( | ||||
| 		new Request(buildUrl(`movies/${id}`, query), { | ||||
|  | ||||
| @ -16,6 +16,27 @@ export const createSerie = async (serie: SeedSerie) => { | ||||
| 	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 ( | ||||
| 	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, | ||||
| 			originalLanguage: null, | ||||
| 			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