mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-31 14:33:50 -04:00
Add /videos/:id?with=next,previous route (#1019)
This commit is contained in:
commit
d5a747f40d
@ -141,6 +141,17 @@ export const entryVideosQ = db
|
|||||||
.leftJoin(videos, eq(videos.pk, entryVideoJoin.videoPk))
|
.leftJoin(videos, eq(videos.pk, entryVideoJoin.videoPk))
|
||||||
.as("videos");
|
.as("videos");
|
||||||
|
|
||||||
|
export const getEntryTransQ = (languages: string[]) => {
|
||||||
|
return db
|
||||||
|
.selectDistinctOn([entryTranslations.pk])
|
||||||
|
.from(entryTranslations)
|
||||||
|
.orderBy(
|
||||||
|
entryTranslations.pk,
|
||||||
|
sql`array_position(${sqlarr(languages)}, ${entryTranslations.language})`,
|
||||||
|
)
|
||||||
|
.as("entry_t");
|
||||||
|
};
|
||||||
|
|
||||||
export const mapProgress = ({ aliased }: { aliased: boolean }) => {
|
export const mapProgress = ({ aliased }: { aliased: boolean }) => {
|
||||||
const { time, percent, playedDate, videoId } = getColumns(entryProgressQ);
|
const { time, percent, playedDate, videoId } = getColumns(entryProgressQ);
|
||||||
const ret = {
|
const ret = {
|
||||||
@ -174,15 +185,7 @@ export async function getEntries({
|
|||||||
userId: string;
|
userId: string;
|
||||||
progressQ?: typeof entryProgressQ;
|
progressQ?: typeof entryProgressQ;
|
||||||
}): Promise<(Entry | Extra)[]> {
|
}): Promise<(Entry | Extra)[]> {
|
||||||
const transQ = db
|
const transQ = getEntryTransQ(languages);
|
||||||
.selectDistinctOn([entryTranslations.pk])
|
|
||||||
.from(entryTranslations)
|
|
||||||
.orderBy(
|
|
||||||
entryTranslations.pk,
|
|
||||||
sql`array_position(${sqlarr(languages)}, ${entryTranslations.language})`,
|
|
||||||
)
|
|
||||||
.as("t");
|
|
||||||
const { pk, name, ...transCol } = getColumns(transQ);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
kind,
|
kind,
|
||||||
@ -196,7 +199,7 @@ export async function getEntries({
|
|||||||
return await db
|
return await db
|
||||||
.select({
|
.select({
|
||||||
...entryCol,
|
...entryCol,
|
||||||
...transCol,
|
...getColumns(transQ),
|
||||||
videos: entryVideosQ.videos,
|
videos: entryVideosQ.videos,
|
||||||
progress: mapProgress({ aliased: true }),
|
progress: mapProgress({ aliased: true }),
|
||||||
// specials don't have an `episodeNumber` but a `number` field.
|
// specials don't have an `episodeNumber` but a `number` field.
|
||||||
@ -212,7 +215,7 @@ export async function getEntries({
|
|||||||
order: sql<number>`${order}`,
|
order: sql<number>`${order}`,
|
||||||
seasonNumber: sql<number>`${seasonNumber}`,
|
seasonNumber: sql<number>`${seasonNumber}`,
|
||||||
episodeNumber: sql<number>`${episodeNumber}`,
|
episodeNumber: sql<number>`${episodeNumber}`,
|
||||||
name: sql<string>`${name}`,
|
name: sql<string>`${transQ.name}`,
|
||||||
})
|
})
|
||||||
.from(entries)
|
.from(entries)
|
||||||
.innerJoin(transQ, eq(entries.pk, transQ.pk))
|
.innerJoin(transQ, eq(entries.pk, transQ.pk))
|
||||||
|
@ -166,7 +166,7 @@ export const imagesH = new Elysia({ tags: ["images"] })
|
|||||||
response: {
|
response: {
|
||||||
302: t.Void({
|
302: t.Void({
|
||||||
description:
|
description:
|
||||||
"Redirected to the [/images/{id}](#tag/images/GET/images/{id}) route.",
|
"Redirected to the [/images/{id}](#tag/images/get/api/images/{id}) route.",
|
||||||
}),
|
}),
|
||||||
404: {
|
404: {
|
||||||
...KError,
|
...KError,
|
||||||
|
@ -2,9 +2,9 @@ import { and, eq, sql } from "drizzle-orm";
|
|||||||
import Elysia, { t } from "elysia";
|
import Elysia, { t } from "elysia";
|
||||||
import { auth } from "~/auth";
|
import { auth } from "~/auth";
|
||||||
import { db } from "~/db";
|
import { db } from "~/db";
|
||||||
import { entries, entryTranslations } from "~/db/schema";
|
import { entries } from "~/db/schema";
|
||||||
import { watchlist } from "~/db/schema/watchlist";
|
import { watchlist } from "~/db/schema/watchlist";
|
||||||
import { getColumns, sqlarr } from "~/db/utils";
|
import { getColumns } from "~/db/utils";
|
||||||
import { Entry } from "~/models/entry";
|
import { Entry } from "~/models/entry";
|
||||||
import {
|
import {
|
||||||
AcceptLanguage,
|
AcceptLanguage,
|
||||||
@ -22,6 +22,7 @@ import {
|
|||||||
entryFilters,
|
entryFilters,
|
||||||
entryProgressQ,
|
entryProgressQ,
|
||||||
entryVideosQ,
|
entryVideosQ,
|
||||||
|
getEntryTransQ,
|
||||||
mapProgress,
|
mapProgress,
|
||||||
} from "../entries";
|
} from "../entries";
|
||||||
|
|
||||||
@ -73,16 +74,7 @@ export const nextup = new Elysia({ tags: ["profiles"] })
|
|||||||
jwt: { sub },
|
jwt: { sub },
|
||||||
}) => {
|
}) => {
|
||||||
const langs = processLanguages(languages);
|
const langs = processLanguages(languages);
|
||||||
|
const transQ = getEntryTransQ(langs);
|
||||||
const transQ = db
|
|
||||||
.selectDistinctOn([entryTranslations.pk])
|
|
||||||
.from(entryTranslations)
|
|
||||||
.orderBy(
|
|
||||||
entryTranslations.pk,
|
|
||||||
sql`array_position(${sqlarr(langs)}, ${entryTranslations.language})`,
|
|
||||||
)
|
|
||||||
.as("t");
|
|
||||||
const { pk, name, ...transCol } = getColumns(transQ);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
externalId,
|
externalId,
|
||||||
@ -97,7 +89,7 @@ export const nextup = new Elysia({ tags: ["profiles"] })
|
|||||||
const items = await db
|
const items = await db
|
||||||
.select({
|
.select({
|
||||||
...entryCol,
|
...entryCol,
|
||||||
...transCol,
|
...getColumns(transQ),
|
||||||
videos: entryVideosQ.videos,
|
videos: entryVideosQ.videos,
|
||||||
progress: mapProgress({ aliased: true }),
|
progress: mapProgress({ aliased: true }),
|
||||||
// specials don't have an `episodeNumber` but a `number` field.
|
// specials don't have an `episodeNumber` but a `number` field.
|
||||||
@ -109,7 +101,7 @@ export const nextup = new Elysia({ tags: ["profiles"] })
|
|||||||
order: sql<number>`${order}`,
|
order: sql<number>`${order}`,
|
||||||
seasonNumber: sql<number>`${seasonNumber}`,
|
seasonNumber: sql<number>`${seasonNumber}`,
|
||||||
episodeNumber: sql<number>`${episodeNumber}`,
|
episodeNumber: sql<number>`${episodeNumber}`,
|
||||||
name: sql<string>`${name}`,
|
name: sql<string>`${transQ.name}`,
|
||||||
})
|
})
|
||||||
.from(entries)
|
.from(entries)
|
||||||
.innerJoin(watchlist, eq(watchlist.nextEntry, entries.pk))
|
.innerJoin(watchlist, eq(watchlist.nextEntry, entries.pk))
|
||||||
|
@ -130,7 +130,7 @@ export const collections = new Elysia({
|
|||||||
response: {
|
response: {
|
||||||
302: t.Void({
|
302: t.Void({
|
||||||
description:
|
description:
|
||||||
"Redirected to the [/collections/{id}](#tag/collections/GET/collections/{id}) route.",
|
"Redirected to the [/collections/{id}](#tag/collections/get/api/collections/{id}) route.",
|
||||||
}),
|
}),
|
||||||
404: {
|
404: {
|
||||||
...KError,
|
...KError,
|
||||||
|
@ -2,7 +2,6 @@ import { and, eq, exists, ne, type SQL, sql } from "drizzle-orm";
|
|||||||
import { db } from "~/db";
|
import { db } from "~/db";
|
||||||
import {
|
import {
|
||||||
entries,
|
entries,
|
||||||
entryTranslations,
|
|
||||||
entryVideoJoin,
|
entryVideoJoin,
|
||||||
profiles,
|
profiles,
|
||||||
showStudioJoin,
|
showStudioJoin,
|
||||||
@ -36,7 +35,12 @@ import {
|
|||||||
} from "~/models/utils";
|
} from "~/models/utils";
|
||||||
import type { EmbeddedVideo } from "~/models/video";
|
import type { EmbeddedVideo } from "~/models/video";
|
||||||
import { WatchlistStatus } from "~/models/watchlist";
|
import { WatchlistStatus } from "~/models/watchlist";
|
||||||
import { entryProgressQ, entryVideosQ, mapProgress } from "../entries";
|
import {
|
||||||
|
entryProgressQ,
|
||||||
|
entryVideosQ,
|
||||||
|
getEntryTransQ,
|
||||||
|
mapProgress,
|
||||||
|
} from "../entries";
|
||||||
|
|
||||||
export const watchStatusQ = db
|
export const watchStatusQ = db
|
||||||
.select({
|
.select({
|
||||||
@ -147,7 +151,7 @@ const showRelations = {
|
|||||||
).as("json"),
|
).as("json"),
|
||||||
})
|
})
|
||||||
.from(studios)
|
.from(studios)
|
||||||
.leftJoin(studioTransQ, eq(studios.pk, studioTransQ.pk))
|
.innerJoin(studioTransQ, eq(studios.pk, studioTransQ.pk))
|
||||||
.where(
|
.where(
|
||||||
exists(
|
exists(
|
||||||
db
|
db
|
||||||
@ -185,21 +189,13 @@ const showRelations = {
|
|||||||
.as("videos");
|
.as("videos");
|
||||||
},
|
},
|
||||||
firstEntry: ({ languages }: { languages: string[] }) => {
|
firstEntry: ({ languages }: { languages: string[] }) => {
|
||||||
const transQ = db
|
const transQ = getEntryTransQ(languages);
|
||||||
.selectDistinctOn([entryTranslations.pk])
|
|
||||||
.from(entryTranslations)
|
|
||||||
.orderBy(
|
|
||||||
entryTranslations.pk,
|
|
||||||
sql`array_position(${sqlarr(languages)}, ${entryTranslations.language})`,
|
|
||||||
)
|
|
||||||
.as("t");
|
|
||||||
const { pk, ...transCol } = getColumns(transQ);
|
|
||||||
|
|
||||||
return db
|
return db
|
||||||
.select({
|
.select({
|
||||||
firstEntry: jsonbBuildObject<Entry>({
|
firstEntry: jsonbBuildObject<Entry>({
|
||||||
...getColumns(entries),
|
...getColumns(entries),
|
||||||
...transCol,
|
...getColumns(transQ),
|
||||||
number: entries.episodeNumber,
|
number: entries.episodeNumber,
|
||||||
videos: entryVideosQ.videos,
|
videos: entryVideosQ.videos,
|
||||||
progress: mapProgress({ aliased: false }),
|
progress: mapProgress({ aliased: false }),
|
||||||
@ -217,21 +213,13 @@ const showRelations = {
|
|||||||
.as("firstEntry");
|
.as("firstEntry");
|
||||||
},
|
},
|
||||||
nextEntry: ({ languages }: { languages: string[] }) => {
|
nextEntry: ({ languages }: { languages: string[] }) => {
|
||||||
const transQ = db
|
const transQ = getEntryTransQ(languages);
|
||||||
.selectDistinctOn([entryTranslations.pk])
|
|
||||||
.from(entryTranslations)
|
|
||||||
.orderBy(
|
|
||||||
entryTranslations.pk,
|
|
||||||
sql`array_position(${sqlarr(languages)}, ${entryTranslations.language})`,
|
|
||||||
)
|
|
||||||
.as("t");
|
|
||||||
const { pk, ...transCol } = getColumns(transQ);
|
|
||||||
|
|
||||||
return db
|
return db
|
||||||
.select({
|
.select({
|
||||||
nextEntry: jsonbBuildObject<Entry>({
|
nextEntry: jsonbBuildObject<Entry>({
|
||||||
...getColumns(entries),
|
...getColumns(entries),
|
||||||
...transCol,
|
...getColumns(transQ),
|
||||||
number: entries.episodeNumber,
|
number: entries.episodeNumber,
|
||||||
videos: entryVideosQ.videos,
|
videos: entryVideosQ.videos,
|
||||||
progress: mapProgress({ aliased: false }),
|
progress: mapProgress({ aliased: false }),
|
||||||
|
@ -120,7 +120,7 @@ export const movies = new Elysia({ prefix: "/movies", tags: ["movies"] })
|
|||||||
response: {
|
response: {
|
||||||
302: t.Void({
|
302: t.Void({
|
||||||
description:
|
description:
|
||||||
"Redirected to the [/movies/{id}](#tag/movies/GET/movies/{id}) route.",
|
"Redirected to the [/movies/{id}](#tag/movies/get/api/movies/{id}) route.",
|
||||||
}),
|
}),
|
||||||
404: {
|
404: {
|
||||||
...KError,
|
...KError,
|
||||||
|
@ -123,7 +123,7 @@ export const series = new Elysia({ prefix: "/series", tags: ["series"] })
|
|||||||
response: {
|
response: {
|
||||||
302: t.Void({
|
302: t.Void({
|
||||||
description:
|
description:
|
||||||
"Redirected to the [/series/{id}](#tag/series/GET/series/{id}) route.",
|
"Redirected to the [/series/{id}](#tag/series/get/api/series/{id}) route.",
|
||||||
}),
|
}),
|
||||||
404: {
|
404: {
|
||||||
...KError,
|
...KError,
|
||||||
|
@ -175,7 +175,7 @@ export const staffH = new Elysia({ tags: ["staff"] })
|
|||||||
response: {
|
response: {
|
||||||
302: t.Void({
|
302: t.Void({
|
||||||
description:
|
description:
|
||||||
"Redirected to the [/staff/{id}](#tag/staff/GET/staff/{id}) route.",
|
"Redirected to the [/staff/{id}](#tag/staff/get/api/staff/{id}) route.",
|
||||||
}),
|
}),
|
||||||
404: {
|
404: {
|
||||||
...KError,
|
...KError,
|
||||||
|
@ -215,7 +215,7 @@ export const studiosH = new Elysia({ prefix: "/studios", tags: ["studios"] })
|
|||||||
response: {
|
response: {
|
||||||
302: t.Void({
|
302: t.Void({
|
||||||
description:
|
description:
|
||||||
"Redirected to the [/studios/{id}](#tag/studios/GET/studios/{id}) route.",
|
"Redirected to the [/studios/{id}](#tag/studios/get/api/studios/{id}) route.",
|
||||||
}),
|
}),
|
||||||
404: {
|
404: {
|
||||||
...KError,
|
...KError,
|
||||||
|
@ -1,22 +1,43 @@
|
|||||||
import { and, eq, notExists, or, sql } from "drizzle-orm";
|
import {
|
||||||
|
and,
|
||||||
|
desc,
|
||||||
|
eq,
|
||||||
|
gt,
|
||||||
|
isNotNull,
|
||||||
|
lt,
|
||||||
|
max,
|
||||||
|
min,
|
||||||
|
notExists,
|
||||||
|
or,
|
||||||
|
sql,
|
||||||
|
} from "drizzle-orm";
|
||||||
|
import { alias } from "drizzle-orm/pg-core";
|
||||||
import { Elysia, t } from "elysia";
|
import { Elysia, t } from "elysia";
|
||||||
|
import { auth } from "~/auth";
|
||||||
import { db, type Transaction } from "~/db";
|
import { db, type Transaction } from "~/db";
|
||||||
import { entries, entryVideoJoin, shows, videos } from "~/db/schema";
|
import { entries, entryVideoJoin, shows, videos } from "~/db/schema";
|
||||||
import {
|
import {
|
||||||
|
coalesce,
|
||||||
conflictUpdateAllExcept,
|
conflictUpdateAllExcept,
|
||||||
|
getColumns,
|
||||||
isUniqueConstraint,
|
isUniqueConstraint,
|
||||||
|
jsonbAgg,
|
||||||
jsonbBuildObject,
|
jsonbBuildObject,
|
||||||
jsonbObjectAgg,
|
jsonbObjectAgg,
|
||||||
sqlarr,
|
sqlarr,
|
||||||
values,
|
values,
|
||||||
} from "~/db/utils";
|
} from "~/db/utils";
|
||||||
|
import { Entry } from "~/models/entry";
|
||||||
import { KError } from "~/models/error";
|
import { KError } from "~/models/error";
|
||||||
import { bubbleVideo } from "~/models/examples";
|
import { bubbleVideo } from "~/models/examples";
|
||||||
import {
|
import {
|
||||||
|
AcceptLanguage,
|
||||||
|
buildRelations,
|
||||||
createPage,
|
createPage,
|
||||||
isUuid,
|
isUuid,
|
||||||
keysetPaginate,
|
keysetPaginate,
|
||||||
Page,
|
Page,
|
||||||
|
processLanguages,
|
||||||
type Resource,
|
type Resource,
|
||||||
Sort,
|
Sort,
|
||||||
sortToSql,
|
sortToSql,
|
||||||
@ -24,6 +45,12 @@ import {
|
|||||||
import { desc as description } from "~/models/utils/descriptions";
|
import { desc as description } from "~/models/utils/descriptions";
|
||||||
import { Guess, Guesses, SeedVideo, Video } from "~/models/video";
|
import { Guess, Guesses, SeedVideo, Video } from "~/models/video";
|
||||||
import { comment } from "~/utils";
|
import { comment } from "~/utils";
|
||||||
|
import {
|
||||||
|
entryProgressQ,
|
||||||
|
entryVideosQ,
|
||||||
|
getEntryTransQ,
|
||||||
|
mapProgress,
|
||||||
|
} from "./entries";
|
||||||
import { computeVideoSlug } from "./seed/insert/entries";
|
import { computeVideoSlug } from "./seed/insert/entries";
|
||||||
import {
|
import {
|
||||||
updateAvailableCount,
|
updateAvailableCount,
|
||||||
@ -175,12 +202,237 @@ const CreatedVideo = t.Object({
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const videoRelations = {
|
||||||
|
slugs: () => {
|
||||||
|
return db
|
||||||
|
.select({
|
||||||
|
slugs: coalesce(jsonbAgg(entryVideoJoin.slug), sql`'[]'::jsonb`).as(
|
||||||
|
"slugs",
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.from(entryVideoJoin)
|
||||||
|
.where(eq(entryVideoJoin.videoPk, videos.pk))
|
||||||
|
.as("slugs");
|
||||||
|
},
|
||||||
|
entries: ({ languages }: { languages: string[] }) => {
|
||||||
|
const transQ = getEntryTransQ(languages);
|
||||||
|
|
||||||
|
return db
|
||||||
|
.select({
|
||||||
|
json: coalesce(
|
||||||
|
jsonbAgg(
|
||||||
|
jsonbBuildObject<Entry>({
|
||||||
|
...getColumns(entries),
|
||||||
|
...getColumns(transQ),
|
||||||
|
number: entries.episodeNumber,
|
||||||
|
videos: entryVideosQ.videos,
|
||||||
|
progress: mapProgress({ aliased: false }),
|
||||||
|
createdAt: sql`to_char(${entries.createdAt}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
|
||||||
|
updatedAt: sql`to_char(${entries.updatedAt}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
sql`'[]'::jsonb`,
|
||||||
|
).as("json"),
|
||||||
|
})
|
||||||
|
.from(entries)
|
||||||
|
.innerJoin(transQ, eq(entries.pk, transQ.pk))
|
||||||
|
.leftJoin(entryProgressQ, eq(entries.pk, entryProgressQ.entryPk))
|
||||||
|
.crossJoinLateral(entryVideosQ)
|
||||||
|
.innerJoin(entryVideoJoin, eq(entryVideoJoin.entryPk, entries.pk))
|
||||||
|
.where(eq(entryVideoJoin.videoPk, videos.pk))
|
||||||
|
.as("entries");
|
||||||
|
},
|
||||||
|
previous: ({ languages }: { languages: string[] }) => {
|
||||||
|
return getNextVideoEntry({ languages, prev: true });
|
||||||
|
},
|
||||||
|
next: getNextVideoEntry,
|
||||||
|
};
|
||||||
|
|
||||||
|
function getNextVideoEntry({
|
||||||
|
languages,
|
||||||
|
prev = false,
|
||||||
|
}: {
|
||||||
|
languages: string[];
|
||||||
|
prev?: boolean;
|
||||||
|
}) {
|
||||||
|
const transQ = getEntryTransQ(languages);
|
||||||
|
|
||||||
|
// tables we use two times in the query bellow
|
||||||
|
const vids = alias(videos, `vid_${prev ? "prev" : "next"}`);
|
||||||
|
const entr = alias(entries, `entr_${prev ? "prev" : "next"}`);
|
||||||
|
const evj = alias(entryVideoJoin, `evj_${prev ? "prev" : "next"}`);
|
||||||
|
return db
|
||||||
|
.select({
|
||||||
|
json: jsonbBuildObject<Entry>({
|
||||||
|
video: entryVideoJoin.slug,
|
||||||
|
entry: {
|
||||||
|
...getColumns(entries),
|
||||||
|
...getColumns(transQ),
|
||||||
|
number: entries.episodeNumber,
|
||||||
|
videos: entryVideosQ.videos,
|
||||||
|
progress: mapProgress({ aliased: false }),
|
||||||
|
createdAt: sql`to_char(${entries.createdAt}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
|
||||||
|
updatedAt: sql`to_char(${entries.updatedAt}, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.from(entries)
|
||||||
|
.innerJoin(transQ, eq(entries.pk, transQ.pk))
|
||||||
|
.leftJoin(entryProgressQ, eq(entries.pk, entryProgressQ.entryPk))
|
||||||
|
.crossJoinLateral(entryVideosQ)
|
||||||
|
.leftJoin(entryVideoJoin, eq(entries.pk, entryVideoJoin.entryPk))
|
||||||
|
.innerJoin(vids, eq(vids.pk, entryVideoJoin.videoPk))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
// either way it needs to be of the same show
|
||||||
|
eq(
|
||||||
|
entries.showPk,
|
||||||
|
db
|
||||||
|
.select({ showPk: entr.showPk })
|
||||||
|
.from(entr)
|
||||||
|
.innerJoin(evj, eq(evj.entryPk, entr.pk))
|
||||||
|
.where(eq(evj.videoPk, videos.pk))
|
||||||
|
.limit(1),
|
||||||
|
),
|
||||||
|
or(
|
||||||
|
// either the next entry
|
||||||
|
(prev ? lt : gt)(
|
||||||
|
entries.order,
|
||||||
|
db
|
||||||
|
.select({ order: (prev ? min : max)(entr.order) })
|
||||||
|
.from(entr)
|
||||||
|
.innerJoin(evj, eq(evj.entryPk, entr.pk))
|
||||||
|
.where(eq(evj.videoPk, videos.pk)),
|
||||||
|
),
|
||||||
|
// or the second part of the current entry
|
||||||
|
and(
|
||||||
|
isNotNull(videos.part),
|
||||||
|
eq(vids.rendering, videos.rendering),
|
||||||
|
eq(vids.part, sql`${videos.part} ${sql.raw(prev ? "-" : "+")} 1`),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.orderBy(
|
||||||
|
prev ? desc(entries.order) : entries.order,
|
||||||
|
// prefer next part of the current entry over next entry
|
||||||
|
eq(vids.rendering, videos.rendering),
|
||||||
|
// take the first part available
|
||||||
|
vids.part,
|
||||||
|
// always prefer latest version of video
|
||||||
|
desc(vids.version),
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
.as("next");
|
||||||
|
}
|
||||||
|
|
||||||
export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||||
.model({
|
.model({
|
||||||
video: Video,
|
video: Video,
|
||||||
"created-videos": t.Array(CreatedVideo),
|
"created-videos": t.Array(CreatedVideo),
|
||||||
error: t.Object({}),
|
error: t.Object({}),
|
||||||
})
|
})
|
||||||
|
.use(auth)
|
||||||
|
.get(
|
||||||
|
":id",
|
||||||
|
async ({
|
||||||
|
params: { id },
|
||||||
|
query: { with: relations },
|
||||||
|
headers: { "accept-language": langs },
|
||||||
|
jwt: { sub },
|
||||||
|
status,
|
||||||
|
}) => {
|
||||||
|
const languages = processLanguages(langs);
|
||||||
|
|
||||||
|
// make an alias so entry video join is not usable on subqueries
|
||||||
|
const evj = alias(entryVideoJoin, "evj");
|
||||||
|
|
||||||
|
const [video] = await db
|
||||||
|
.select({
|
||||||
|
...getColumns(videos),
|
||||||
|
...buildRelations(
|
||||||
|
["slugs", "entries", ...relations],
|
||||||
|
videoRelations,
|
||||||
|
{
|
||||||
|
languages,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.from(videos)
|
||||||
|
.leftJoin(evj, eq(videos.pk, evj.videoPk))
|
||||||
|
.where(isUuid(id) ? eq(videos.id, id) : eq(evj.slug, id))
|
||||||
|
.limit(1)
|
||||||
|
.execute({ userId: sub });
|
||||||
|
if (!video) {
|
||||||
|
return status(404, {
|
||||||
|
status: 404,
|
||||||
|
message: `No video found with id or slug '${id}'`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return video;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
detail: {
|
||||||
|
description: "Get a video & it's related entries",
|
||||||
|
},
|
||||||
|
params: t.Object({
|
||||||
|
id: t.String({
|
||||||
|
description: "The id or slug of the video to retrieve.",
|
||||||
|
example: "made-in-abyss-s1e13",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
query: t.Object({
|
||||||
|
with: t.Array(t.UnionEnum(["previous", "next"]), {
|
||||||
|
default: [],
|
||||||
|
description: "Include related entries in the response.",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
headers: t.Object(
|
||||||
|
{
|
||||||
|
"accept-language": AcceptLanguage(),
|
||||||
|
},
|
||||||
|
{ additionalProperties: true },
|
||||||
|
),
|
||||||
|
response: {
|
||||||
|
200: t.Composite([
|
||||||
|
Video,
|
||||||
|
t.Object({
|
||||||
|
slugs: t.Array(
|
||||||
|
t.String({ format: "slug", examples: ["made-in-abyss-s1e13"] }),
|
||||||
|
),
|
||||||
|
entries: t.Array(Entry),
|
||||||
|
previous: t.Optional(
|
||||||
|
t.Nullable(
|
||||||
|
t.Object({
|
||||||
|
video: t.String({
|
||||||
|
format: "slug",
|
||||||
|
examples: ["made-in-abyss-s1e12"],
|
||||||
|
}),
|
||||||
|
entry: Entry,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
next: t.Optional(
|
||||||
|
t.Nullable(
|
||||||
|
t.Object({
|
||||||
|
video: t.String({
|
||||||
|
format: "slug",
|
||||||
|
examples: ["made-in-abyss-dawn-of-the-deep-soul"],
|
||||||
|
}),
|
||||||
|
entry: Entry,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
404: {
|
||||||
|
...KError,
|
||||||
|
description: "No video found with the given id or slug.",
|
||||||
|
},
|
||||||
|
422: KError,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
.get(
|
.get(
|
||||||
"",
|
"",
|
||||||
async () => {
|
async () => {
|
||||||
|
@ -124,7 +124,7 @@ export const jsonbObjectAgg = <T>(
|
|||||||
>`jsonb_object_agg(${sql.join([key, value], sql.raw(","))})`;
|
>`jsonb_object_agg(${sql.join([key, value], sql.raw(","))})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const jsonbAgg = <T>(val: SQL<T>) => {
|
export const jsonbAgg = <T>(val: SQL<T> | SQLWrapper) => {
|
||||||
return sql<T[]>`jsonb_agg(${val})`;
|
return sql<T[]>`jsonb_agg(${val})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -266,6 +266,81 @@ export const madeInAbyss = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
kind: "episode",
|
||||||
|
order: 15,
|
||||||
|
seasonNumber: 2,
|
||||||
|
episodeNumber: 2,
|
||||||
|
translations: {
|
||||||
|
en: {
|
||||||
|
name: " Resurrection Festival ",
|
||||||
|
description:
|
||||||
|
"Riko and Reg find out more about their past but the question still remains, who or what exactly is Reg?",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
runtime: 23,
|
||||||
|
airDate: "2022-07-06",
|
||||||
|
thumbnail:
|
||||||
|
"https://artworks.thetvdb.com/banners/episodes/326109/6174129.jpg",
|
||||||
|
externalId: {
|
||||||
|
themoviedatabase: {
|
||||||
|
serieId: "72636",
|
||||||
|
season: 2,
|
||||||
|
episode: 2,
|
||||||
|
link: "https://www.themoviedb.org/tv/72636/season/2/episode/2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "episode",
|
||||||
|
order: 16,
|
||||||
|
seasonNumber: 2,
|
||||||
|
episodeNumber: 3,
|
||||||
|
translations: {
|
||||||
|
en: {
|
||||||
|
name: "Departure",
|
||||||
|
description:
|
||||||
|
"Reg goes on his first cave raid! Meanwhile, Riko makes preparations to go into the abyss to find her mother.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
runtime: 23,
|
||||||
|
airDate: "2022-07-06",
|
||||||
|
thumbnail:
|
||||||
|
"https://artworks.thetvdb.com/banners/episodes/326109/6180539.jpg",
|
||||||
|
externalId: {
|
||||||
|
themoviedatabase: {
|
||||||
|
serieId: "72636",
|
||||||
|
season: 2,
|
||||||
|
episode: 3,
|
||||||
|
link: "https://www.themoviedb.org/tv/72636/season/2/episode/4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "episode",
|
||||||
|
order: 17,
|
||||||
|
seasonNumber: 2,
|
||||||
|
episodeNumber: 4,
|
||||||
|
translations: {
|
||||||
|
en: {
|
||||||
|
name: "The Edge of the Abyss",
|
||||||
|
description:
|
||||||
|
"Riko and Reg start their adventure into the Abyss, while there they run into an unexpected familiar face.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
runtime: 23,
|
||||||
|
airDate: "2022-07-06",
|
||||||
|
thumbnail:
|
||||||
|
"https://artworks.thetvdb.com/banners/episodes/326109/6180540.jpg",
|
||||||
|
externalId: {
|
||||||
|
themoviedatabase: {
|
||||||
|
serieId: "72636",
|
||||||
|
season: 2,
|
||||||
|
episode: 4,
|
||||||
|
link: "https://www.themoviedb.org/tv/72636/season/2/episode/4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
extras: [
|
extras: [
|
||||||
{
|
{
|
||||||
|
@ -81,7 +81,7 @@ export const FullSerie = t.Intersect([
|
|||||||
t.Object({
|
t.Object({
|
||||||
translations: t.Optional(TranslationRecord(SerieTranslation)),
|
translations: t.Optional(TranslationRecord(SerieTranslation)),
|
||||||
studios: t.Optional(t.Array(Studio)),
|
studios: t.Optional(t.Array(Studio)),
|
||||||
firstEntry: t.Optional(Entry),
|
firstEntry: t.Optional(t.Nullable(Entry)),
|
||||||
nextEntry: t.Optional(t.Nullable(Entry)),
|
nextEntry: t.Optional(t.Nullable(Entry)),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
@ -15,7 +15,7 @@ export const buildRelations = <
|
|||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
enabled.map((x) => [x, sql`${relations[x](params!)}`]),
|
enabled.map((x) => [x, sql`${relations[x](params!)}`]),
|
||||||
) as {
|
) as {
|
||||||
[P in R]?: SQL<
|
[P in R]: SQL<
|
||||||
ReturnType<Rel[P]>["_"]["selectedFields"] extends {
|
ReturnType<Rel[P]>["_"]["selectedFields"] extends {
|
||||||
[key: string]: infer TValue;
|
[key: string]: infer TValue;
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,25 @@ export const getVideos = async () => {
|
|||||||
return [resp, body] as const;
|
return [resp, body] as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getVideo = async (
|
||||||
|
id: string,
|
||||||
|
{ langs, ...query }: { langs?: string; with?: string[] },
|
||||||
|
) => {
|
||||||
|
const resp = await handlers.handle(
|
||||||
|
new Request(buildUrl(`videos/${id}`, query), {
|
||||||
|
method: "GET",
|
||||||
|
headers: langs
|
||||||
|
? {
|
||||||
|
"Accept-Language": langs,
|
||||||
|
...(await getJwtHeaders()),
|
||||||
|
}
|
||||||
|
: await getJwtHeaders(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const body = await resp.json();
|
||||||
|
return [resp, body] as const;
|
||||||
|
};
|
||||||
|
|
||||||
export const deleteVideo = async (paths: string[]) => {
|
export const deleteVideo = async (paths: string[]) => {
|
||||||
const resp = await handlers.handle(
|
const resp = await handlers.handle(
|
||||||
new Request(buildUrl("videos"), {
|
new Request(buildUrl("videos"), {
|
||||||
|
397
api/tests/videos/get-id.test.ts
Normal file
397
api/tests/videos/get-id.test.ts
Normal file
@ -0,0 +1,397 @@
|
|||||||
|
import { beforeAll, describe, expect, it } from "bun:test";
|
||||||
|
import { createSerie, createVideo, getVideo } from "tests/helpers";
|
||||||
|
import { expectStatus } from "tests/utils";
|
||||||
|
import { db } from "~/db";
|
||||||
|
import { shows, videos } from "~/db/schema";
|
||||||
|
import { madeInAbyss } from "~/models/examples";
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await db.delete(shows);
|
||||||
|
let [ret, _] = await createSerie(madeInAbyss);
|
||||||
|
expect(ret.status).toBe(201);
|
||||||
|
await db.delete(videos);
|
||||||
|
|
||||||
|
[ret, _] = await createVideo([
|
||||||
|
{
|
||||||
|
path: "/video/Made in abyss S01E13.mkv",
|
||||||
|
rendering: "mia13",
|
||||||
|
part: null,
|
||||||
|
version: 1,
|
||||||
|
guess: {
|
||||||
|
title: "Made in abyss",
|
||||||
|
episodes: [{ season: 1, episode: 13 }],
|
||||||
|
kind: "episode",
|
||||||
|
from: "guessit",
|
||||||
|
history: [],
|
||||||
|
},
|
||||||
|
for: [{ serie: madeInAbyss.slug, season: 1, episode: 13 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/video/Made in abyss movie.mkv",
|
||||||
|
rendering: "mia-movie",
|
||||||
|
part: null,
|
||||||
|
version: 1,
|
||||||
|
guess: {
|
||||||
|
title: "Made in abyss",
|
||||||
|
kind: "movie",
|
||||||
|
from: "guessit",
|
||||||
|
history: [],
|
||||||
|
},
|
||||||
|
// TODO: i feel like there's a better way than that. we need to make this api better
|
||||||
|
for: [{ serie: madeInAbyss.slug, order: 13.5 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/video/Made in abyss s2e1 p1.mkv",
|
||||||
|
rendering: "mia-s2e1",
|
||||||
|
part: 1,
|
||||||
|
version: 1,
|
||||||
|
guess: {
|
||||||
|
title: "Made in abyss",
|
||||||
|
kind: "episode",
|
||||||
|
episodes: [{ season: 2, episode: 1 }],
|
||||||
|
from: "guessit",
|
||||||
|
history: [],
|
||||||
|
},
|
||||||
|
for: [{ serie: madeInAbyss.slug, season: 2, episode: 1 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/video/Made in abyss s2e1 p2.mkv",
|
||||||
|
rendering: "mia-s2e1",
|
||||||
|
part: 2,
|
||||||
|
version: 1,
|
||||||
|
guess: {
|
||||||
|
title: "Made in abyss",
|
||||||
|
kind: "episode",
|
||||||
|
episodes: [{ season: 2, episode: 1 }],
|
||||||
|
from: "guessit",
|
||||||
|
history: [],
|
||||||
|
},
|
||||||
|
for: [{ serie: madeInAbyss.slug, season: 2, episode: 1 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/video/Made in abyss s2e1 p2 v2.mkv",
|
||||||
|
rendering: "mia-s2e1",
|
||||||
|
part: 2,
|
||||||
|
version: 2,
|
||||||
|
guess: {
|
||||||
|
title: "Made in abyss",
|
||||||
|
kind: "episode",
|
||||||
|
episodes: [{ season: 2, episode: 1 }],
|
||||||
|
from: "guessit",
|
||||||
|
history: [],
|
||||||
|
},
|
||||||
|
for: [{ serie: madeInAbyss.slug, season: 2, episode: 1 }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/video/Made in abyss s2e2&3.mkv",
|
||||||
|
rendering: "mia-s2e2",
|
||||||
|
part: null,
|
||||||
|
version: 1,
|
||||||
|
guess: {
|
||||||
|
title: "Made in abyss",
|
||||||
|
kind: "episode",
|
||||||
|
episodes: [
|
||||||
|
{ season: 2, episode: 2 },
|
||||||
|
{ season: 2, episode: 3 },
|
||||||
|
],
|
||||||
|
from: "guessit",
|
||||||
|
history: [],
|
||||||
|
},
|
||||||
|
for: [
|
||||||
|
{ serie: madeInAbyss.slug, season: 2, episode: 2 },
|
||||||
|
{ serie: madeInAbyss.slug, season: 2, episode: 3 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/video/Made in abyss s2e4.mkv",
|
||||||
|
rendering: "mia-s2e4",
|
||||||
|
part: null,
|
||||||
|
version: 1,
|
||||||
|
guess: {
|
||||||
|
title: "Made in abyss",
|
||||||
|
kind: "episode",
|
||||||
|
episodes: [{ season: 2, episode: 4 }],
|
||||||
|
from: "guessit",
|
||||||
|
history: [],
|
||||||
|
},
|
||||||
|
for: [{ serie: madeInAbyss.slug, season: 2, episode: 4 }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
expect(ret.status).toBe(201);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Get videos", () => {
|
||||||
|
it("Invalid slug", async () => {
|
||||||
|
const [resp, body] = await getVideo("sotneuhn", { langs: "en" });
|
||||||
|
expectStatus(resp, body).toBe(404);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
status: 404,
|
||||||
|
message: expect.any(String),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Get video", async () => {
|
||||||
|
const [resp, body] = await getVideo("made-in-abyss-s1e13", { langs: "en" });
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
id: expect.any(String),
|
||||||
|
path: "/video/Made in abyss S01E13.mkv",
|
||||||
|
rendering: "mia13",
|
||||||
|
part: null,
|
||||||
|
version: 1,
|
||||||
|
guess: {
|
||||||
|
title: "Made in abyss",
|
||||||
|
episodes: [{ season: 1, episode: 13 }],
|
||||||
|
kind: "episode",
|
||||||
|
from: "guessit",
|
||||||
|
history: [],
|
||||||
|
},
|
||||||
|
slugs: ["made-in-abyss-s1e13"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Get video with null previous", async () => {
|
||||||
|
const [resp, body] = await getVideo("made-in-abyss-s1e13", {
|
||||||
|
langs: "en",
|
||||||
|
with: ["previous"],
|
||||||
|
});
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
id: expect.any(String),
|
||||||
|
path: "/video/Made in abyss S01E13.mkv",
|
||||||
|
rendering: "mia13",
|
||||||
|
part: null,
|
||||||
|
version: 1,
|
||||||
|
guess: {
|
||||||
|
title: "Made in abyss",
|
||||||
|
episodes: [{ season: 1, episode: 13 }],
|
||||||
|
kind: "episode",
|
||||||
|
from: "guessit",
|
||||||
|
history: [],
|
||||||
|
},
|
||||||
|
slugs: ["made-in-abyss-s1e13"],
|
||||||
|
previous: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Get video with movie next", async () => {
|
||||||
|
const [resp, body] = await getVideo("made-in-abyss-s1e13", {
|
||||||
|
langs: "en",
|
||||||
|
with: ["previous", "next"],
|
||||||
|
});
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
id: expect.any(String),
|
||||||
|
path: "/video/Made in abyss S01E13.mkv",
|
||||||
|
rendering: "mia13",
|
||||||
|
part: null,
|
||||||
|
version: 1,
|
||||||
|
guess: {
|
||||||
|
title: "Made in abyss",
|
||||||
|
episodes: [{ season: 1, episode: 13 }],
|
||||||
|
kind: "episode",
|
||||||
|
from: "guessit",
|
||||||
|
history: [],
|
||||||
|
},
|
||||||
|
slugs: ["made-in-abyss-s1e13"],
|
||||||
|
previous: null,
|
||||||
|
next: {
|
||||||
|
video: "made-in-abyss-dawn-of-the-deep-soul",
|
||||||
|
entry: expect.objectContaining({
|
||||||
|
slug: "made-in-abyss-dawn-of-the-deep-soul",
|
||||||
|
name: "Made in Abyss: Dawn of the Deep Soul",
|
||||||
|
order: 13.5,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Get video with multi-part next", async () => {
|
||||||
|
const [resp, body] = await getVideo("made-in-abyss-dawn-of-the-deep-soul", {
|
||||||
|
langs: "en",
|
||||||
|
with: ["previous", "next"],
|
||||||
|
});
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
path: "/video/Made in abyss movie.mkv",
|
||||||
|
slugs: ["made-in-abyss-dawn-of-the-deep-soul"],
|
||||||
|
previous: {
|
||||||
|
video: "made-in-abyss-s1e13",
|
||||||
|
entry: expect.objectContaining({
|
||||||
|
slug: "made-in-abyss-s1e13",
|
||||||
|
order: 13,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
next: {
|
||||||
|
video: "made-in-abyss-s2e1-p1",
|
||||||
|
entry: expect.objectContaining({
|
||||||
|
slug: "made-in-abyss-s2e1",
|
||||||
|
seasonNumber: 2,
|
||||||
|
episodeNumber: 1,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Get first part", async () => {
|
||||||
|
const [resp, body] = await getVideo("made-in-abyss-s2e1-p1", {
|
||||||
|
langs: "en",
|
||||||
|
with: ["previous", "next"],
|
||||||
|
});
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
path: "/video/Made in abyss s2e1 p1.mkv",
|
||||||
|
slugs: ["made-in-abyss-s2e1-p1"],
|
||||||
|
previous: {
|
||||||
|
video: "made-in-abyss-dawn-of-the-deep-soul",
|
||||||
|
entry: expect.objectContaining({
|
||||||
|
slug: "made-in-abyss-dawn-of-the-deep-soul",
|
||||||
|
order: 13.5,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
next: {
|
||||||
|
video: "made-in-abyss-s2e1-p2-v2",
|
||||||
|
entry: expect.objectContaining({
|
||||||
|
slug: "made-in-abyss-s2e1",
|
||||||
|
seasonNumber: 2,
|
||||||
|
episodeNumber: 1,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Get second part", async () => {
|
||||||
|
const [resp, body] = await getVideo("made-in-abyss-s2e1-p2-v2", {
|
||||||
|
langs: "en",
|
||||||
|
with: ["previous", "next"],
|
||||||
|
});
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
path: "/video/Made in abyss s2e1 p2 v2.mkv",
|
||||||
|
slugs: ["made-in-abyss-s2e1-p2-v2"],
|
||||||
|
previous: {
|
||||||
|
video: "made-in-abyss-s2e1-p1",
|
||||||
|
entry: expect.objectContaining({
|
||||||
|
slug: "made-in-abyss-s2e1",
|
||||||
|
seasonNumber: 2,
|
||||||
|
episodeNumber: 1,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
next: {
|
||||||
|
video: "made-in-abyss-s2e2",
|
||||||
|
entry: expect.objectContaining({
|
||||||
|
slug: "made-in-abyss-s2e2",
|
||||||
|
seasonNumber: 2,
|
||||||
|
episodeNumber: 2,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Get v1", async () => {
|
||||||
|
const [resp, body] = await getVideo("made-in-abyss-s2e1-p2", {
|
||||||
|
langs: "en",
|
||||||
|
with: ["previous", "next"],
|
||||||
|
});
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
path: "/video/Made in abyss s2e1 p2.mkv",
|
||||||
|
slugs: ["made-in-abyss-s2e1-p2"],
|
||||||
|
previous: {
|
||||||
|
video: "made-in-abyss-s2e1-p1",
|
||||||
|
entry: expect.objectContaining({
|
||||||
|
slug: "made-in-abyss-s2e1",
|
||||||
|
seasonNumber: 2,
|
||||||
|
episodeNumber: 1,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
next: {
|
||||||
|
video: "made-in-abyss-s2e2",
|
||||||
|
entry: expect.objectContaining({
|
||||||
|
slug: "made-in-abyss-s2e2",
|
||||||
|
seasonNumber: 2,
|
||||||
|
episodeNumber: 2,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Get multi entry video", async () => {
|
||||||
|
const [resp, body] = await getVideo("made-in-abyss-s2e2", {
|
||||||
|
langs: "en",
|
||||||
|
with: ["previous", "next"],
|
||||||
|
});
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
path: "/video/Made in abyss s2e2&3.mkv",
|
||||||
|
slugs: ["made-in-abyss-s2e2", "made-in-abyss-s2e3"],
|
||||||
|
previous: {
|
||||||
|
// when going to the prev episode, go to the first part of it
|
||||||
|
video: "made-in-abyss-s2e1-p1",
|
||||||
|
entry: expect.objectContaining({
|
||||||
|
slug: "made-in-abyss-s2e1",
|
||||||
|
seasonNumber: 2,
|
||||||
|
episodeNumber: 1,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
next: {
|
||||||
|
video: "made-in-abyss-s2e4",
|
||||||
|
entry: expect.objectContaining({
|
||||||
|
slug: "made-in-abyss-s2e4",
|
||||||
|
seasonNumber: 2,
|
||||||
|
episodeNumber: 4,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Get multi entry video (ep 2)", async () => {
|
||||||
|
const [resp, body] = await getVideo("made-in-abyss-s2e3", {
|
||||||
|
langs: "en",
|
||||||
|
with: ["previous", "next"],
|
||||||
|
});
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
path: "/video/Made in abyss s2e2&3.mkv",
|
||||||
|
slugs: ["made-in-abyss-s2e2", "made-in-abyss-s2e3"],
|
||||||
|
previous: {
|
||||||
|
// when going to the prev episode, go to the first part of it
|
||||||
|
video: "made-in-abyss-s2e1-p1",
|
||||||
|
entry: expect.objectContaining({
|
||||||
|
slug: "made-in-abyss-s2e1",
|
||||||
|
seasonNumber: 2,
|
||||||
|
episodeNumber: 1,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
next: {
|
||||||
|
video: "made-in-abyss-s2e4",
|
||||||
|
entry: expect.objectContaining({
|
||||||
|
slug: "made-in-abyss-s2e4",
|
||||||
|
seasonNumber: 2,
|
||||||
|
episodeNumber: 4,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Get last ep with next=null", async () => {
|
||||||
|
const [resp, body] = await getVideo("made-in-abyss-s2e4", {
|
||||||
|
langs: "en",
|
||||||
|
with: ["previous", "next"],
|
||||||
|
});
|
||||||
|
expectStatus(resp, body).toBe(200);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
path: "/video/Made in abyss s2e4.mkv",
|
||||||
|
slugs: ["made-in-abyss-s2e4"],
|
||||||
|
previous: {
|
||||||
|
video: "made-in-abyss-s2e3",
|
||||||
|
entry: expect.objectContaining({
|
||||||
|
slug: "made-in-abyss-s2e3",
|
||||||
|
seasonNumber: 2,
|
||||||
|
episodeNumber: 3,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
next: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user