mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-23 15:30:34 -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