mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Add available videos in /entries response (#830)
This commit is contained in:
commit
d9e3b3a94b
@ -2,6 +2,7 @@ FROM oven/bun AS builder
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json bun.lock .
|
COPY package.json bun.lock .
|
||||||
|
COPY patches patches
|
||||||
RUN bun install --production
|
RUN bun install --production
|
||||||
|
|
||||||
COPY src src
|
COPY src src
|
||||||
|
@ -17,6 +17,9 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"patchedDependencies": {
|
||||||
|
"drizzle-orm@0.39.0": "patches/drizzle-orm@0.39.0.patch",
|
||||||
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
||||||
|
|
||||||
|
@ -21,5 +21,8 @@
|
|||||||
"@types/pg": "^8.11.11",
|
"@types/pg": "^8.11.11",
|
||||||
"bun-types": "^1.2.4"
|
"bun-types": "^1.2.4"
|
||||||
},
|
},
|
||||||
"module": "src/index.js"
|
"module": "src/index.js",
|
||||||
|
"patchedDependencies": {
|
||||||
|
"drizzle-orm@0.39.0": "patches/drizzle-orm@0.39.0.patch"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
111
api/patches/drizzle-orm@0.39.0.patch
Normal file
111
api/patches/drizzle-orm@0.39.0.patch
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
diff --git a/node_modules/drizzle-orm/.bun-tag-9fae835e61d5cc75 b/.bun-tag-9fae835e61d5cc75
|
||||||
|
new file mode 100644
|
||||||
|
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
|
||||||
|
diff --git a/pg-core/query-builders/select.d.cts b/pg-core/query-builders/select.d.cts
|
||||||
|
index b968ebb3f563f37c8c36221dd17cc6f3603270ec..3fda6d0a97997f6bd07ec6a0c83397c0fdd2e97e 100644
|
||||||
|
--- a/pg-core/query-builders/select.d.cts
|
||||||
|
+++ b/pg-core/query-builders/select.d.cts
|
||||||
|
@@ -98,7 +98,16 @@ export declare abstract class PgSelectQueryBuilderBase<THKT extends PgSelectHKTB
|
||||||
|
* .leftJoin(pets, eq(users.id, pets.ownerId))
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
- leftJoin: PgSelectJoinFn<this, TDynamic, "left">;
|
||||||
|
+ leftJoin: PgSelectJoinFn<this, TDynamic, "left", false>;
|
||||||
|
+ /**
|
||||||
|
+ * For each row of the table, include
|
||||||
|
+ * values from a matching row of the joined
|
||||||
|
+ * subquery, if there is a matching row. If not,
|
||||||
|
+ * all of the columns of the joined subquery
|
||||||
|
+ * will be set to null. The lateral keyword allows
|
||||||
|
+ * access to columns after the FROM statement.
|
||||||
|
+ */
|
||||||
|
+ leftJoinLateral: PgSelectJoinFn<this, TDynamic, "left", true>;
|
||||||
|
/**
|
||||||
|
* Executes a `right join` operation by adding another table to the current query.
|
||||||
|
*
|
||||||
|
@@ -126,7 +135,7 @@ export declare abstract class PgSelectQueryBuilderBase<THKT extends PgSelectHKTB
|
||||||
|
* .rightJoin(pets, eq(users.id, pets.ownerId))
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
- rightJoin: PgSelectJoinFn<this, TDynamic, "right">;
|
||||||
|
+ rightJoin: PgSelectJoinFn<this, TDynamic, "right", false>;
|
||||||
|
/**
|
||||||
|
* Executes an `inner join` operation, creating a new table by combining rows from two tables that have matching values.
|
||||||
|
*
|
||||||
|
@@ -154,7 +163,14 @@ export declare abstract class PgSelectQueryBuilderBase<THKT extends PgSelectHKTB
|
||||||
|
* .innerJoin(pets, eq(users.id, pets.ownerId))
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
- innerJoin: PgSelectJoinFn<this, TDynamic, "inner">;
|
||||||
|
+ innerJoin: PgSelectJoinFn<this, TDynamic, "inner", false>;
|
||||||
|
+ /**
|
||||||
|
+ * For each row of the table, the joined subquery
|
||||||
|
+ * needs to have a matching row, or it will
|
||||||
|
+ * be excluded from results. The lateral keyword allows
|
||||||
|
+ * access to columns after the FROM statement.
|
||||||
|
+ */
|
||||||
|
+ innerJoinLateral: PgSelectJoinFn<this, TDynamic, "inner", true>;
|
||||||
|
/**
|
||||||
|
* Executes a `full join` operation by combining rows from two tables into a new table.
|
||||||
|
*
|
||||||
|
@@ -182,7 +198,7 @@ export declare abstract class PgSelectQueryBuilderBase<THKT extends PgSelectHKTB
|
||||||
|
* .fullJoin(pets, eq(users.id, pets.ownerId))
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
- fullJoin: PgSelectJoinFn<this, TDynamic, "full">;
|
||||||
|
+ fullJoin: PgSelectJoinFn<this, TDynamic, "full", false>;
|
||||||
|
private createSetOperator;
|
||||||
|
/**
|
||||||
|
* Adds `union` set operator to the query.
|
||||||
|
diff --git a/pg-core/query-builders/select.js b/pg-core/query-builders/select.js
|
||||||
|
index e54406fcaf68ccfdaf32c8945d4d432212c4cf3f..0441be1e483a7ec02430978b5fac5bf6d863ffc7 100644
|
||||||
|
--- a/pg-core/query-builders/select.js
|
||||||
|
+++ b/pg-core/query-builders/select.js
|
||||||
|
@@ -98,7 +98,7 @@ class PgSelectQueryBuilderBase extends TypedQueryBuilder {
|
||||||
|
this.tableName = getTableLikeName(table);
|
||||||
|
this.joinsNotNullableMap = typeof this.tableName === "string" ? { [this.tableName]: true } : {};
|
||||||
|
}
|
||||||
|
- createJoin(joinType) {
|
||||||
|
+ createJoin(joinType, lateral = false) {
|
||||||
|
return (table, on) => {
|
||||||
|
const baseTableName = this.tableName;
|
||||||
|
const tableName = getTableLikeName(table);
|
||||||
|
@@ -127,7 +127,7 @@ class PgSelectQueryBuilderBase extends TypedQueryBuilder {
|
||||||
|
if (!this.config.joins) {
|
||||||
|
this.config.joins = [];
|
||||||
|
}
|
||||||
|
- this.config.joins.push({ on, table, joinType, alias: tableName });
|
||||||
|
+ this.config.joins.push({ on, table, joinType, alias: tableName, lateral });
|
||||||
|
if (typeof tableName === "string") {
|
||||||
|
switch (joinType) {
|
||||||
|
case "left": {
|
||||||
|
@@ -185,6 +185,15 @@ class PgSelectQueryBuilderBase extends TypedQueryBuilder {
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
leftJoin = this.createJoin("left");
|
||||||
|
+ /**
|
||||||
|
+ * For each row of the table, include
|
||||||
|
+ * values from a matching row of the joined
|
||||||
|
+ * subquery, if there is a matching row. If not,
|
||||||
|
+ * all of the columns of the joined subquery
|
||||||
|
+ * will be set to null. The lateral keyword allows
|
||||||
|
+ * access to columns after the FROM statement.
|
||||||
|
+ */
|
||||||
|
+ leftJoinLateral = this.createJoin("left", true);
|
||||||
|
/**
|
||||||
|
* Executes a `right join` operation by adding another table to the current query.
|
||||||
|
*
|
||||||
|
@@ -241,6 +250,13 @@ class PgSelectQueryBuilderBase extends TypedQueryBuilder {
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
innerJoin = this.createJoin("inner");
|
||||||
|
+ /**
|
||||||
|
+ * For each row of the table, the joined subquery
|
||||||
|
+ * needs to have a matching row, or it will
|
||||||
|
+ * be excluded from results. The lateral keyword allows
|
||||||
|
+ * access to columns after the FROM statement.
|
||||||
|
+ */
|
||||||
|
+ innerJoinLateral = this.createJoin("inner", true);
|
||||||
|
/**
|
||||||
|
* Executes a `full join` operation by combining rows from two tables into a new table.
|
||||||
|
*
|
@ -2,7 +2,13 @@ import type { StaticDecode } from "@sinclair/typebox";
|
|||||||
import { type SQL, and, eq, ne, sql } from "drizzle-orm";
|
import { type SQL, and, eq, ne, sql } from "drizzle-orm";
|
||||||
import { Elysia, t } from "elysia";
|
import { Elysia, t } from "elysia";
|
||||||
import { db } from "~/db";
|
import { db } from "~/db";
|
||||||
import { entries, entryTranslations, shows } from "~/db/schema";
|
import {
|
||||||
|
entries,
|
||||||
|
entryTranslations,
|
||||||
|
entryVideoJoin,
|
||||||
|
shows,
|
||||||
|
videos,
|
||||||
|
} from "~/db/schema";
|
||||||
import { getColumns, sqlarr } from "~/db/utils";
|
import { getColumns, sqlarr } from "~/db/utils";
|
||||||
import {
|
import {
|
||||||
Entry,
|
Entry,
|
||||||
@ -99,6 +105,20 @@ async function getEntries({
|
|||||||
.as("t");
|
.as("t");
|
||||||
const { pk, name, ...transCol } = getColumns(transQ);
|
const { pk, name, ...transCol } = getColumns(transQ);
|
||||||
|
|
||||||
|
const { guess, createdAt, updatedAt, ...videosCol } = getColumns(videos);
|
||||||
|
const videosQ = db
|
||||||
|
.select({ slug: entryVideoJoin.slug, ...videosCol })
|
||||||
|
.from(entryVideoJoin)
|
||||||
|
.where(eq(entryVideoJoin.entryPk, entries.pk))
|
||||||
|
.leftJoin(videos, eq(videos.pk, entryVideoJoin.videoPk))
|
||||||
|
.as("videos");
|
||||||
|
const videosJ = db
|
||||||
|
.select({
|
||||||
|
videos: sql`coalesce(json_agg("videos"), '[]'::json)`.as("videos"),
|
||||||
|
})
|
||||||
|
.from(videosQ)
|
||||||
|
.as("videos_json");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
kind,
|
kind,
|
||||||
externalId,
|
externalId,
|
||||||
@ -112,24 +132,25 @@ async function getEntries({
|
|||||||
.select({
|
.select({
|
||||||
...entryCol,
|
...entryCol,
|
||||||
...transCol,
|
...transCol,
|
||||||
|
videos: videosJ.videos,
|
||||||
// specials don't have an `episodeNumber` but a `number` field.
|
// specials don't have an `episodeNumber` but a `number` field.
|
||||||
number: sql<number>`${episodeNumber}`.as("order"),
|
number: episodeNumber,
|
||||||
|
|
||||||
// merge `extraKind` into `kind`
|
// merge `extraKind` into `kind`
|
||||||
kind: sql<EntryKind>`case when ${kind} = 'extra' then ${extraKind} else ${kind}::text end`.as(
|
kind: sql<EntryKind>`case when ${kind} = 'extra' then ${extraKind} else ${kind}::text end`.as(
|
||||||
"kind",
|
"kind",
|
||||||
),
|
),
|
||||||
isExtra: sql<boolean>`${kind} = 'extra'`.as("isExtra"),
|
|
||||||
|
|
||||||
// assign more restrained types to make typescript happy.
|
// assign more restrained types to make typescript happy.
|
||||||
externalId: sql<any>`${externalId}`.as("externalId"),
|
externalId: sql<any>`${externalId}`,
|
||||||
order: sql<number>`${order}`.as("order"),
|
order: sql<number>`${order}`,
|
||||||
seasonNumber: sql<number>`${seasonNumber}`.as("order"),
|
seasonNumber: sql<number>`${seasonNumber}`,
|
||||||
episodeNumber: sql<number>`${episodeNumber}`.as("order"),
|
episodeNumber: sql<number>`${episodeNumber}`,
|
||||||
name: sql<string>`${name}`.as("name"),
|
name: sql<string>`${name}`,
|
||||||
})
|
})
|
||||||
.from(entries)
|
.from(entries)
|
||||||
.innerJoin(transQ, eq(entries.pk, transQ.pk))
|
.innerJoin(transQ, eq(entries.pk, transQ.pk))
|
||||||
|
.leftJoinLateral(videosJ, sql`true`)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
filter,
|
filter,
|
||||||
|
@ -121,6 +121,7 @@ export const insertEntries = async (
|
|||||||
return {
|
return {
|
||||||
videoId: seed.video,
|
videoId: seed.video,
|
||||||
entryPk: retEntries[i].pk,
|
entryPk: retEntries[i].pk,
|
||||||
|
entrySlug: retEntries[i].slug,
|
||||||
needRendering: false,
|
needRendering: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -128,8 +129,9 @@ export const insertEntries = async (
|
|||||||
return seed.videos.map((x, j) => ({
|
return seed.videos.map((x, j) => ({
|
||||||
videoId: x,
|
videoId: x,
|
||||||
entryPk: retEntries[i].pk,
|
entryPk: retEntries[i].pk,
|
||||||
|
entrySlug: retEntries[i].slug,
|
||||||
// The first video should not have a rendering.
|
// The first video should not have a rendering.
|
||||||
needRendering: j && seed.videos!.length > 1,
|
needRendering: j !== 0 && seed.videos!.length > 1,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -142,9 +144,9 @@ export const insertEntries = async (
|
|||||||
db
|
db
|
||||||
.select({
|
.select({
|
||||||
entryPk: sql<number>`vids.entryPk::integer`.as("entry"),
|
entryPk: sql<number>`vids.entryPk::integer`.as("entry"),
|
||||||
videoPk: sql`${videos.pk}`.as("video"),
|
videoPk: videos.pk,
|
||||||
slug: computeVideoSlug(
|
slug: computeVideoSlug(
|
||||||
sql`${show.slug}::text`,
|
sql`vids.entrySlug::text`,
|
||||||
sql`vids.needRendering::boolean`,
|
sql`vids.needRendering::boolean`,
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
@ -7,8 +7,11 @@ import { processOptImage } from "../images";
|
|||||||
type StudioI = typeof studios.$inferInsert;
|
type StudioI = typeof studios.$inferInsert;
|
||||||
type StudioTransI = typeof studioTranslations.$inferInsert;
|
type StudioTransI = typeof studioTranslations.$inferInsert;
|
||||||
|
|
||||||
export const insertStudios = async (seed: SeedStudio[], showPk: number) => {
|
export const insertStudios = async (
|
||||||
if (!seed.length) return [];
|
seed: SeedStudio[] | undefined,
|
||||||
|
showPk: number,
|
||||||
|
) => {
|
||||||
|
if (!seed?.length) return [];
|
||||||
|
|
||||||
return await db.transaction(async (tx) => {
|
return await db.transaction(async (tx) => {
|
||||||
const vals: StudioI[] = seed.map((x) => {
|
const vals: StudioI[] = seed.map((x) => {
|
||||||
|
@ -39,7 +39,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
|
|||||||
if (!ret) {
|
if (!ret) {
|
||||||
return error(404, {
|
return error(404, {
|
||||||
status: 404,
|
status: 404,
|
||||||
message: "Movie not found",
|
message: `No movie found with id or slug: '${id}'.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!ret.language) {
|
if (!ret.language) {
|
||||||
|
@ -18,6 +18,10 @@ export const base = new Elysia({ name: "base" })
|
|||||||
const { code, ...ret } = details;
|
const { code, ...ret } = details;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
details.errors = details.errors.map((x: any) => {
|
||||||
|
const { schema, ...err } = x;
|
||||||
|
return err;
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
status: error.status,
|
status: error.status,
|
||||||
message: `Validation error on ${details.on}.`,
|
message: `Validation error on ${details.on}.`,
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
SeedImage,
|
SeedImage,
|
||||||
TranslationRecord,
|
TranslationRecord,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
import { EmbeddedVideo } from "../video";
|
||||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||||
|
|
||||||
export const BaseEpisode = t.Intersect([
|
export const BaseEpisode = t.Intersect([
|
||||||
@ -25,6 +26,9 @@ export const Episode = t.Intersect([
|
|||||||
Resource(),
|
Resource(),
|
||||||
EntryTranslation(),
|
EntryTranslation(),
|
||||||
BaseEpisode,
|
BaseEpisode,
|
||||||
|
t.Object({
|
||||||
|
videos: t.Optional(t.Array(EmbeddedVideo)),
|
||||||
|
}),
|
||||||
DbMetadata,
|
DbMetadata,
|
||||||
]);
|
]);
|
||||||
export type Episode = Prettify<typeof Episode.static>;
|
export type Episode = Prettify<typeof Episode.static>;
|
||||||
@ -34,7 +38,7 @@ export const SeedEpisode = t.Intersect([
|
|||||||
t.Object({
|
t.Object({
|
||||||
thumbnail: t.Nullable(SeedImage),
|
thumbnail: t.Nullable(SeedImage),
|
||||||
translations: TranslationRecord(EntryTranslation()),
|
translations: TranslationRecord(EntryTranslation()),
|
||||||
videos: t.Optional(t.Array(t.String({ format: "uuid" }))),
|
videos: t.Optional(t.Array(t.String({ format: "uuid" }), { default: [] })),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
export type SeedEpisode = Prettify<typeof SeedEpisode.static>;
|
export type SeedEpisode = Prettify<typeof SeedEpisode.static>;
|
||||||
@ -45,4 +49,5 @@ registerExamples(Episode, {
|
|||||||
...ep.translations.en,
|
...ep.translations.en,
|
||||||
...bubbleImages,
|
...bubbleImages,
|
||||||
slug: `${madeInAbyss.slug}-s${ep.seasonNumber}-e${ep.episodeNumber}`,
|
slug: `${madeInAbyss.slug}-s${ep.seasonNumber}-e${ep.episodeNumber}`,
|
||||||
|
videos: [],
|
||||||
});
|
});
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
SeedImage,
|
SeedImage,
|
||||||
TranslationRecord,
|
TranslationRecord,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
import { EmbeddedVideo } from "../video";
|
||||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||||
|
|
||||||
export const BaseMovieEntry = t.Intersect(
|
export const BaseMovieEntry = t.Intersect(
|
||||||
@ -43,6 +44,9 @@ export const MovieEntry = t.Intersect([
|
|||||||
Resource(),
|
Resource(),
|
||||||
MovieEntryTranslation,
|
MovieEntryTranslation,
|
||||||
BaseMovieEntry,
|
BaseMovieEntry,
|
||||||
|
t.Object({
|
||||||
|
videos: t.Optional(t.Array(EmbeddedVideo)),
|
||||||
|
}),
|
||||||
DbMetadata,
|
DbMetadata,
|
||||||
]);
|
]);
|
||||||
export type MovieEntry = Prettify<typeof MovieEntry.static>;
|
export type MovieEntry = Prettify<typeof MovieEntry.static>;
|
||||||
@ -58,7 +62,7 @@ export const SeedMovieEntry = t.Intersect([
|
|||||||
t.Object({ poster: t.Nullable(SeedImage) }),
|
t.Object({ poster: t.Nullable(SeedImage) }),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
videos: t.Optional(t.Array(t.String({ format: "uuid" }))),
|
videos: t.Optional(t.Array(t.String({ format: "uuid" }), { default: [] })),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
export type SeedMovieEntry = Prettify<typeof SeedMovieEntry.static>;
|
export type SeedMovieEntry = Prettify<typeof SeedMovieEntry.static>;
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
SeedImage,
|
SeedImage,
|
||||||
TranslationRecord,
|
TranslationRecord,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
import { EmbeddedVideo } from "../video";
|
||||||
import { BaseEntry, EntryTranslation } from "./base-entry";
|
import { BaseEntry, EntryTranslation } from "./base-entry";
|
||||||
|
|
||||||
export const BaseSpecial = t.Intersect(
|
export const BaseSpecial = t.Intersect(
|
||||||
@ -35,6 +36,9 @@ export const Special = t.Intersect([
|
|||||||
Resource(),
|
Resource(),
|
||||||
EntryTranslation(),
|
EntryTranslation(),
|
||||||
BaseSpecial,
|
BaseSpecial,
|
||||||
|
t.Object({
|
||||||
|
videos: t.Optional(t.Array(EmbeddedVideo)),
|
||||||
|
}),
|
||||||
DbMetadata,
|
DbMetadata,
|
||||||
]);
|
]);
|
||||||
export type Special = Prettify<typeof Special.static>;
|
export type Special = Prettify<typeof Special.static>;
|
||||||
@ -44,7 +48,7 @@ export const SeedSpecial = t.Intersect([
|
|||||||
t.Object({
|
t.Object({
|
||||||
thumbnail: t.Nullable(SeedImage),
|
thumbnail: t.Nullable(SeedImage),
|
||||||
translations: TranslationRecord(EntryTranslation()),
|
translations: TranslationRecord(EntryTranslation()),
|
||||||
videos: t.Optional(t.Array(t.String({ format: "uuid" }))),
|
videos: t.Optional(t.Array(t.String({ format: "uuid" }), { default: [] })),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
export type SeedSpecial = Prettify<typeof SeedSpecial.static>;
|
export type SeedSpecial = Prettify<typeof SeedSpecial.static>;
|
||||||
|
@ -3,15 +3,15 @@ import type { Video } from "~/models/video";
|
|||||||
|
|
||||||
export const madeInAbyssVideo: Video = {
|
export const madeInAbyssVideo: Video = {
|
||||||
id: "3cd436ee-01ff-4f45-ba98-654282531234",
|
id: "3cd436ee-01ff-4f45-ba98-654282531234",
|
||||||
slug: "made-in-abyss-s1e1",
|
slug: "made-in-abyss-s1e13",
|
||||||
path: "/video/Made in abyss S01E01.mkv",
|
path: "/video/Made in abyss S01E13.mkv",
|
||||||
rendering: "459429fa062adeebedcc2bb04b9965de0262bfa453369783132d261be79021bd",
|
rendering: "459429fa062adeebedcc2bb04b9965de0262bfa453369783132d261be79021bd",
|
||||||
part: null,
|
part: null,
|
||||||
version: 1,
|
version: 1,
|
||||||
guess: {
|
guess: {
|
||||||
title: "Made in abyss",
|
title: "Made in abyss",
|
||||||
season: [1],
|
season: [1],
|
||||||
episode: [1],
|
episode: [13],
|
||||||
type: "episode",
|
type: "episode",
|
||||||
from: "guessit",
|
from: "guessit",
|
||||||
},
|
},
|
||||||
@ -156,6 +156,7 @@ export const madeInAbyss = {
|
|||||||
link: "https://www.themoviedb.org/tv/72636/season/1/episode/13",
|
link: "https://www.themoviedb.org/tv/72636/season/1/episode/13",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
videos: [madeInAbyssVideo.id],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
kind: "special",
|
kind: "special",
|
||||||
@ -240,7 +241,7 @@ export const madeInAbyss = {
|
|||||||
name: "The Making of MADE IN ABYSS 01",
|
name: "The Making of MADE IN ABYSS 01",
|
||||||
runtime: 17,
|
runtime: 17,
|
||||||
thumbnail: null,
|
thumbnail: null,
|
||||||
video: "3cd436ee-01ff-4f45-ba98-654282531234",
|
video: madeInAbyssVideo.id,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
studios: [
|
studios: [
|
||||||
|
@ -88,9 +88,9 @@ export const SeedMovie = t.Intersect([
|
|||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
videos: t.Optional(t.Array(t.String({ format: "uuid" }))),
|
videos: t.Optional(t.Array(t.String({ format: "uuid" }), { default: [] })),
|
||||||
collection: t.Optional(SeedCollection),
|
collection: t.Optional(SeedCollection),
|
||||||
studios: t.Array(SeedStudio),
|
studios: t.Optional(t.Array(SeedStudio, { default: [] })),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
export type SeedMovie = Prettify<typeof SeedMovie.static>;
|
export type SeedMovie = Prettify<typeof SeedMovie.static>;
|
||||||
|
@ -98,9 +98,9 @@ export const SeedSerie = t.Intersect([
|
|||||||
),
|
),
|
||||||
seasons: t.Array(SeedSeason),
|
seasons: t.Array(SeedSeason),
|
||||||
entries: t.Array(SeedEntry),
|
entries: t.Array(SeedEntry),
|
||||||
extras: t.Optional(t.Array(SeedExtra)),
|
extras: t.Optional(t.Array(SeedExtra, { default: [] })),
|
||||||
collection: t.Optional(SeedCollection),
|
collection: t.Optional(SeedCollection),
|
||||||
studios: t.Array(SeedStudio),
|
studios: t.Optional(t.Array(SeedStudio, { default: [] })),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
export type SeedSerie = typeof SeedSerie.static;
|
export type SeedSerie = typeof SeedSerie.static;
|
||||||
|
@ -38,7 +38,7 @@ export const SeedVideo = t.Object({
|
|||||||
season: t.Optional(t.Array(t.Integer(), { default: [] })),
|
season: t.Optional(t.Array(t.Integer(), { default: [] })),
|
||||||
episode: t.Optional(t.Array(t.Integer(), { default: [] })),
|
episode: t.Optional(t.Array(t.Integer(), { default: [] })),
|
||||||
// TODO: maybe replace "extra" with the `extraKind` value (aka behind-the-scene, trailer, etc)
|
// TODO: maybe replace "extra" with the `extraKind` value (aka behind-the-scene, trailer, etc)
|
||||||
type: t.Optional(t.UnionEnum(["episode", "movie", "extra"])),
|
kind: t.Optional(t.UnionEnum(["episode", "movie", "extra"])),
|
||||||
|
|
||||||
from: t.String({
|
from: t.String({
|
||||||
description: "Name of the tool that made the guess",
|
description: "Name of the tool that made the guess",
|
||||||
@ -71,4 +71,8 @@ export type SeedVideo = typeof SeedVideo.static;
|
|||||||
export const Video = t.Intersect([Resource(), SeedVideo, DbMetadata]);
|
export const Video = t.Intersect([Resource(), SeedVideo, DbMetadata]);
|
||||||
export type Video = Prettify<typeof Video.static>;
|
export type Video = Prettify<typeof Video.static>;
|
||||||
|
|
||||||
|
// type used in entry responses
|
||||||
|
export const EmbeddedVideo = t.Omit(Video, ["createdAt", "updatedAt"]);
|
||||||
|
export type EmbeddedVideo = Prettify<typeof EmbeddedVideo.static>;
|
||||||
|
|
||||||
registerExamples(Video, bubbleVideo);
|
registerExamples(Video, bubbleVideo);
|
||||||
|
15
api/tests/manual.ts
Normal file
15
api/tests/manual.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { db, migrate } from "~/db";
|
||||||
|
import { shows, videos } from "~/db/schema";
|
||||||
|
import { madeInAbyss, madeInAbyssVideo } from "~/models/examples";
|
||||||
|
import { createSerie, createVideo } from "./helpers";
|
||||||
|
|
||||||
|
// test file used to run manually using `bun tests/manual.ts`
|
||||||
|
|
||||||
|
await migrate();
|
||||||
|
await db.delete(shows);
|
||||||
|
await db.delete(videos);
|
||||||
|
|
||||||
|
const [_, vid] = await createVideo(madeInAbyssVideo);
|
||||||
|
console.log(vid);
|
||||||
|
const [__, ser] = await createSerie(madeInAbyss);
|
||||||
|
console.log(ser);
|
@ -21,7 +21,7 @@ describe("Get movie", () => {
|
|||||||
expectStatus(resp, body).toBe(404);
|
expectStatus(resp, body).toBe(404);
|
||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
status: 404,
|
status: 404,
|
||||||
message: "Movie not found",
|
message: expect.any(String),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("Retrive by slug", async () => {
|
it("Retrive by slug", async () => {
|
||||||
|
@ -1,14 +1,22 @@
|
|||||||
import { beforeAll, describe, expect, it } from "bun:test";
|
import { beforeAll, describe, expect, it } from "bun:test";
|
||||||
import { getEntries, getExtras, getUnknowns } from "tests/helpers";
|
import { createSerie, createVideo, getEntries, getExtras } from "tests/helpers";
|
||||||
import { expectStatus } from "tests/utils";
|
import { expectStatus } from "tests/utils";
|
||||||
import { seedSerie } from "~/controllers/seed/series";
|
import { db } from "~/db";
|
||||||
import { madeInAbyss } from "~/models/examples";
|
import { shows, videos } from "~/db/schema";
|
||||||
|
import { madeInAbyss as base, madeInAbyssVideo } from "~/models/examples";
|
||||||
|
|
||||||
let miaId = "";
|
// make a copy so we can mutate it.
|
||||||
|
const madeInAbyss = JSON.parse(JSON.stringify(base)) as typeof base;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const ret = await seedSerie(madeInAbyss);
|
await db.delete(shows);
|
||||||
if (!("status" in ret)) miaId = ret.id;
|
await db.delete(videos);
|
||||||
|
const [_, vid] = await createVideo(madeInAbyssVideo);
|
||||||
|
for (const entry of madeInAbyss.entries.filter((x) => x.videos?.length))
|
||||||
|
entry.videos = [vid[0].id];
|
||||||
|
for (const entry of madeInAbyss.extras.filter((x) => x.video))
|
||||||
|
entry.video = vid[0].id;
|
||||||
|
await createSerie(madeInAbyss);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Get entries", () => {
|
describe("Get entries", () => {
|
||||||
@ -27,6 +35,19 @@ describe("Get entries", () => {
|
|||||||
expectStatus(resp, body).toBe(200);
|
expectStatus(resp, body).toBe(200);
|
||||||
expect(body.items).toBeArrayOfSize(madeInAbyss.entries.length);
|
expect(body.items).toBeArrayOfSize(madeInAbyss.entries.length);
|
||||||
});
|
});
|
||||||
|
it("With videos", async () => {
|
||||||
|
const [resp, body] = await getEntries(madeInAbyss.slug, { langs: "en" });
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body.items[0].videos).toBeArrayOfSize(1);
|
||||||
|
expect(body.items[0].videos[0]).toMatchObject({
|
||||||
|
path: madeInAbyssVideo.path,
|
||||||
|
slug: madeInAbyssVideo.slug,
|
||||||
|
version: madeInAbyssVideo.version,
|
||||||
|
rendering: madeInAbyssVideo.rendering,
|
||||||
|
part: madeInAbyssVideo.part,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Get extra", () => {
|
describe("Get extra", () => {
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
import { beforeAll, describe, expect, it } from "bun:test";
|
import { beforeAll, describe, expect, it } from "bun:test";
|
||||||
import { getSeasons } from "tests/helpers";
|
import { createSerie, getSeasons } from "tests/helpers";
|
||||||
import { expectStatus } from "tests/utils";
|
import { expectStatus } from "tests/utils";
|
||||||
import { seedSerie } from "~/controllers/seed/series";
|
|
||||||
import { madeInAbyss } from "~/models/examples";
|
import { madeInAbyss } from "~/models/examples";
|
||||||
|
|
||||||
let miaId = "";
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const ret = await seedSerie(madeInAbyss);
|
await createSerie(madeInAbyss);
|
||||||
if (!("status" in ret)) miaId = ret.id;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Get seasons", () => {
|
describe("Get seasons", () => {
|
||||||
|
@ -25,7 +25,12 @@ describe("Serie seeding", () => {
|
|||||||
where: eq(shows.id, body.id),
|
where: eq(shows.id, body.id),
|
||||||
with: {
|
with: {
|
||||||
seasons: { orderBy: seasons.seasonNumber },
|
seasons: { orderBy: seasons.seasonNumber },
|
||||||
entries: { with: { translations: true } },
|
entries: {
|
||||||
|
with: {
|
||||||
|
translations: true,
|
||||||
|
evj: { with: { video: true } },
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -37,7 +42,9 @@ describe("Serie seeding", () => {
|
|||||||
madeInAbyss.entries.length + madeInAbyss.extras.length,
|
madeInAbyss.entries.length + madeInAbyss.extras.length,
|
||||||
);
|
);
|
||||||
|
|
||||||
const ep13 = madeInAbyss.entries.find((x) => x.order === 13)!;
|
const { videos: _, ...ep13 } = madeInAbyss.entries.find(
|
||||||
|
(x) => x.order === 13,
|
||||||
|
)!;
|
||||||
expect(ret!.entries.find((x) => x.order === 13)).toMatchObject({
|
expect(ret!.entries.find((x) => x.order === 13)).toMatchObject({
|
||||||
...ep13,
|
...ep13,
|
||||||
slug: "made-in-abyss-s1e13",
|
slug: "made-in-abyss-s1e13",
|
||||||
@ -48,6 +55,12 @@ describe("Serie seeding", () => {
|
|||||||
...ep13.translations.en,
|
...ep13.translations.en,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
evj: [
|
||||||
|
expect.objectContaining({
|
||||||
|
slug: madeInAbyssVideo.slug,
|
||||||
|
video: expect.objectContaining({ path: madeInAbyssVideo.path }),
|
||||||
|
}),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { number, ...special } = madeInAbyss.entries.find(
|
const { number, ...special } = madeInAbyss.entries.find(
|
||||||
|
@ -1,11 +1,15 @@
|
|||||||
import { beforeAll, describe, expect, it } from "bun:test";
|
import { beforeAll, describe, expect, it } from "bun:test";
|
||||||
import { getSerie, getShowsByStudio, getStudio } from "tests/helpers";
|
import {
|
||||||
|
createSerie,
|
||||||
|
getSerie,
|
||||||
|
getShowsByStudio,
|
||||||
|
getStudio,
|
||||||
|
} from "tests/helpers";
|
||||||
import { expectStatus } from "tests/utils";
|
import { expectStatus } from "tests/utils";
|
||||||
import { seedSerie } from "~/controllers/seed/series";
|
|
||||||
import { madeInAbyss } from "~/models/examples";
|
import { madeInAbyss } from "~/models/examples";
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await seedSerie(madeInAbyss);
|
await createSerie(madeInAbyss);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Get by studio", () => {
|
describe("Get by studio", () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user