mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Add tests for video linking
This commit is contained in:
parent
6e2743a4be
commit
489336c77a
@ -515,10 +515,13 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
|||||||
201: t.Array(
|
201: t.Array(
|
||||||
t.Object({
|
t.Object({
|
||||||
id: t.String({ format: "uuid" }),
|
id: t.String({ format: "uuid" }),
|
||||||
path: t.String(),
|
path: t.String({ examples: ["/video/made in abyss s1e13.mkv"] }),
|
||||||
entries: t.Array(
|
entries: t.Array(
|
||||||
t.Object({
|
t.Object({
|
||||||
slug: t.String({ format: "slug" }),
|
slug: t.String({
|
||||||
|
format: "slug",
|
||||||
|
examples: ["made-in-abyss-s1e13"],
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
@ -58,7 +58,7 @@ export const toDrizzle = (expr: Expression, config: FilterDef): SQL => {
|
|||||||
{ in: where },
|
{ in: where },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
expr.value = { type: "bool", value: expr.value.value === "true" }
|
expr.value = { type: "bool", value: expr.value.value === "true" };
|
||||||
}
|
}
|
||||||
if (prop.type !== expr.value.type) {
|
if (prop.type !== expr.value.type) {
|
||||||
throw new KErrorT(
|
throw new KErrorT(
|
||||||
|
@ -43,3 +43,20 @@ export const deleteVideo = async (paths: string[]) => {
|
|||||||
const body = await resp.json();
|
const body = await resp.json();
|
||||||
return [resp, body] as const;
|
return [resp, body] as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const linkVideos = async (
|
||||||
|
links: { id: string; for: SeedVideo["for"] }[],
|
||||||
|
) => {
|
||||||
|
const resp = await handlers.handle(
|
||||||
|
new Request(buildUrl("videos/link"), {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(links),
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(await getJwtHeaders()),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const body = await resp.json();
|
||||||
|
return [resp, body] as const;
|
||||||
|
};
|
||||||
|
@ -9,6 +9,7 @@ let bubbleId = "";
|
|||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await db.delete(shows);
|
await db.delete(shows);
|
||||||
|
await db.delete(videos);
|
||||||
await db.insert(videos).values(bubbleVideo);
|
await db.insert(videos).values(bubbleVideo);
|
||||||
const [ret, body] = await createMovie(bubble);
|
const [ret, body] = await createMovie(bubble);
|
||||||
expect(ret.status).toBe(201);
|
expect(ret.status).toBe(201);
|
||||||
@ -66,21 +67,29 @@ describe("Get movie", () => {
|
|||||||
const [resp, body] = await getMovie(bubble.slug, { langs: "fr,pr,*" });
|
const [resp, body] = await getMovie(bubble.slug, { langs: "fr,pr,*" });
|
||||||
|
|
||||||
expectStatus(resp, body).toBe(200);
|
expectStatus(resp, body).toBe(200);
|
||||||
expect(body).toMatchObject({
|
expect(body.slug).toBe(bubble.slug);
|
||||||
slug: bubble.slug,
|
const lang = resp.headers.get("Content-Language");
|
||||||
name: bubble.translations.en.name,
|
if (lang === "en") {
|
||||||
});
|
expect(body.name).toBe(bubble.translations.en.name);
|
||||||
expect(resp.headers.get("Content-Language")).toBe("en");
|
} else if (lang === "ja") {
|
||||||
|
expect(body.name).toBe(bubble.translations.ja.name);
|
||||||
|
} else {
|
||||||
|
expect(lang).toBe("en");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
it("Works without accept-language header", async () => {
|
it("Works without accept-language header", async () => {
|
||||||
const [resp, body] = await getMovie(bubble.slug, { langs: undefined });
|
const [resp, body] = await getMovie(bubble.slug, { langs: undefined });
|
||||||
|
|
||||||
expectStatus(resp, body).toBe(200);
|
expectStatus(resp, body).toBe(200);
|
||||||
expect(body).toMatchObject({
|
expect(body.slug).toBe(bubble.slug);
|
||||||
slug: bubble.slug,
|
const lang = resp.headers.get("Content-Language");
|
||||||
name: bubble.translations.en.name,
|
if (lang === "en") {
|
||||||
});
|
expect(body.name).toBe(bubble.translations.en.name);
|
||||||
expect(resp.headers.get("Content-Language")).toBe("en");
|
} else if (lang === "ja") {
|
||||||
|
expect(body.name).toBe(bubble.translations.ja.name);
|
||||||
|
} else {
|
||||||
|
expect(lang).toBe("en");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
it("Fallback if translations does not exist", async () => {
|
it("Fallback if translations does not exist", async () => {
|
||||||
const [resp, body] = await getMovie(bubble.slug, { langs: "en-au" });
|
const [resp, body] = await getMovie(bubble.slug, { langs: "en-au" });
|
||||||
|
@ -6,10 +6,11 @@ import {
|
|||||||
createVideo,
|
createVideo,
|
||||||
deleteVideo,
|
deleteVideo,
|
||||||
getVideos,
|
getVideos,
|
||||||
|
linkVideos,
|
||||||
} from "tests/helpers";
|
} from "tests/helpers";
|
||||||
import { expectStatus } from "tests/utils";
|
import { expectStatus } from "tests/utils";
|
||||||
import { db } from "~/db";
|
import { db } from "~/db";
|
||||||
import { entries, shows, videos } from "~/db/schema";
|
import { entries, entryVideoJoin, shows, videos } from "~/db/schema";
|
||||||
import { bubble, madeInAbyss } from "~/models/examples";
|
import { bubble, madeInAbyss } from "~/models/examples";
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@ -57,12 +58,35 @@ beforeAll(async () => {
|
|||||||
version: 1,
|
version: 1,
|
||||||
for: [{ movie: bubble.slug }],
|
for: [{ movie: bubble.slug }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
guess: {
|
||||||
|
title: "mia",
|
||||||
|
episodes: [{ season: 1, episode: 1 }], // Different episode for unlinked
|
||||||
|
from: "test",
|
||||||
|
history: [],
|
||||||
|
},
|
||||||
|
part: null,
|
||||||
|
path: "/video/mia-unlinked.mkv",
|
||||||
|
rendering: "sha-unlinked-1",
|
||||||
|
version: 1,
|
||||||
|
// No 'for' initially
|
||||||
|
},
|
||||||
|
{
|
||||||
|
guess: { title: "bubble", from: "test", history: [] },
|
||||||
|
part: null,
|
||||||
|
path: "/video/bubble-unlinked.mkv",
|
||||||
|
rendering: "sha-unlinked-2",
|
||||||
|
version: 1,
|
||||||
|
// No 'for' initially
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
expectStatus(ret, body).toBe(201);
|
expectStatus(ret, body).toBe(201);
|
||||||
expect(body).toBeArrayOfSize(3);
|
expect(body).toBeArrayOfSize(5);
|
||||||
expect(body[0].entries).toBeArrayOfSize(1);
|
expect(body[0].entries).toBeArrayOfSize(1);
|
||||||
expect(body[1].entries).toBeArrayOfSize(1);
|
expect(body[1].entries).toBeArrayOfSize(1);
|
||||||
expect(body[2].entries).toBeArrayOfSize(1);
|
expect(body[2].entries).toBeArrayOfSize(1);
|
||||||
|
expect(body[3].entries).toBeArrayOfSize(0); // Unlinked
|
||||||
|
expect(body[4].entries).toBeArrayOfSize(0); // Unlinked
|
||||||
|
|
||||||
const items = await db.query.shows.findMany();
|
const items = await db.query.shows.findMany();
|
||||||
expect(items.find((x) => x.slug === "bubble")!.availableCount).toBe(1);
|
expect(items.find((x) => x.slug === "bubble")!.availableCount).toBe(1);
|
||||||
@ -141,8 +165,10 @@ describe("Video get/deletion", () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(body.unmatched).toBeArrayOfSize(1);
|
expect(body.unmatched).toBeArrayOfSize(3);
|
||||||
expect(body.unmatched[0]).toBe("/video/mia s1e13 unknown test.mkv");
|
expect(body.unmatched).toContain("/video/mia s1e13 unknown test.mkv");
|
||||||
|
expect(body.unmatched).toContain("/video/mia-unlinked.mkv");
|
||||||
|
expect(body.unmatched).toContain("/video/bubble-unlinked.mkv");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Mismatch title guess", async () => {
|
it("Mismatch title guess", async () => {
|
||||||
@ -249,3 +275,69 @@ describe("Video get/deletion", () => {
|
|||||||
expect(body[0]).toBe("/video/mia s1e13 unknown test.mkv");
|
expect(body[0]).toBe("/video/mia s1e13 unknown test.mkv");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Video linking", () => {
|
||||||
|
it("Should link videos to entries", async () => {
|
||||||
|
const allVideos = await db
|
||||||
|
.select({
|
||||||
|
id: videos.id,
|
||||||
|
path: videos.path,
|
||||||
|
rendering: videos.rendering,
|
||||||
|
})
|
||||||
|
.from(videos);
|
||||||
|
|
||||||
|
const miaUnlinkedVideo = allVideos.find(
|
||||||
|
(v) => v.rendering === "sha-unlinked-1",
|
||||||
|
);
|
||||||
|
const bubbleUnlinkedVideo = allVideos.find(
|
||||||
|
(v) => v.rendering === "sha-unlinked-2",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(miaUnlinkedVideo).toBeDefined();
|
||||||
|
expect(bubbleUnlinkedVideo).toBeDefined();
|
||||||
|
|
||||||
|
const [resp, body] = await linkVideos([
|
||||||
|
{
|
||||||
|
id: miaUnlinkedVideo!.id,
|
||||||
|
for: [{ slug: `${madeInAbyss.slug}-s1e13` }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: bubbleUnlinkedVideo!.id,
|
||||||
|
for: [{ movie: bubble.slug }],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expectStatus(resp, body).toBe(201);
|
||||||
|
expect(body).toBeArrayOfSize(2);
|
||||||
|
|
||||||
|
expect(body[0]).toMatchObject({
|
||||||
|
id: miaUnlinkedVideo!.id,
|
||||||
|
path: "/video/mia-unlinked.mkv",
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
slug: expect.stringContaining(`${madeInAbyss.slug}-s1e13`),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(body[1]).toMatchObject({
|
||||||
|
id: bubbleUnlinkedVideo!.id,
|
||||||
|
path: "/video/bubble-unlinked.mkv",
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
slug: expect.stringContaining(bubble.slug),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const miaShow = await db.query.shows.findFirst({
|
||||||
|
where: eq(shows.slug, madeInAbyss.slug),
|
||||||
|
});
|
||||||
|
expect(miaShow!.availableCount).toBe(1);
|
||||||
|
|
||||||
|
const bubbleShow = await db.query.shows.findFirst({
|
||||||
|
where: eq(shows.slug, bubble.slug),
|
||||||
|
});
|
||||||
|
expect(bubbleShow!.availableCount).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -7,9 +7,9 @@ from aiohttp import ClientSession
|
|||||||
from pydantic import TypeAdapter
|
from pydantic import TypeAdapter
|
||||||
|
|
||||||
from .models.movie import Movie
|
from .models.movie import Movie
|
||||||
|
from .models.request import Request
|
||||||
from .models.serie import Serie
|
from .models.serie import Serie
|
||||||
from .models.videos import For, Resource, Video, VideoCreated, VideoInfo, VideoLink
|
from .models.videos import For, Resource, Video, VideoCreated, VideoInfo, VideoLink
|
||||||
from .requests import Request
|
|
||||||
from .utils import Singleton
|
from .utils import Singleton
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
@ -96,7 +96,7 @@ class KyooClient(metaclass=Singleton):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async with self._client.post(
|
async with self._client.post(
|
||||||
"videos",
|
"videos/link",
|
||||||
data=TypeAdapter(list[VideoLink]).dump_json(
|
data=TypeAdapter(list[VideoLink]).dump_json(
|
||||||
[map_request(x) for x in videos],
|
[map_request(x) for x in videos],
|
||||||
by_alias=True,
|
by_alias=True,
|
||||||
|
20
scanner/scanner/models/request.py
Normal file
20
scanner/scanner/models/request.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from .videos import Guess
|
||||||
|
from ..utils import Model
|
||||||
|
|
||||||
|
|
||||||
|
class Request(Model, extra="allow"):
|
||||||
|
pk: int | None = Field(exclude=True, default=None)
|
||||||
|
kind: Literal["episode", "movie"]
|
||||||
|
title: str
|
||||||
|
year: int | None
|
||||||
|
external_id: dict[str, str]
|
||||||
|
videos: list[Request.Video]
|
||||||
|
|
||||||
|
class Video(Model):
|
||||||
|
id: str
|
||||||
|
episodes: list[Guess.Episode]
|
@ -1,33 +1,18 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from asyncio import CancelledError, Event, TaskGroup
|
from asyncio import CancelledError, Event, TaskGroup
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
from typing import Literal, cast
|
from typing import cast
|
||||||
|
|
||||||
from asyncpg import Connection, Pool
|
from asyncpg import Connection, Pool
|
||||||
from pydantic import Field, TypeAdapter
|
from pydantic import TypeAdapter
|
||||||
|
|
||||||
from .client import KyooClient
|
from .client import KyooClient
|
||||||
from .models.videos import Guess, Resource
|
from .models.request import Request
|
||||||
|
from .models.videos import Resource
|
||||||
from .providers.provider import Provider
|
from .providers.provider import Provider
|
||||||
from .utils import Model
|
|
||||||
|
|
||||||
logger = getLogger(__name__)
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Request(Model, extra="allow"):
|
|
||||||
pk: int | None = Field(exclude=True, default=None)
|
|
||||||
kind: Literal["episode", "movie"]
|
|
||||||
title: str
|
|
||||||
year: int | None
|
|
||||||
external_id: dict[str, str]
|
|
||||||
videos: list[Request.Video]
|
|
||||||
|
|
||||||
class Video(Model):
|
|
||||||
id: str
|
|
||||||
episodes: list[Guess.Episode]
|
|
||||||
|
|
||||||
|
|
||||||
class RequestCreator:
|
class RequestCreator:
|
||||||
def __init__(self, database: Connection):
|
def __init__(self, database: Connection):
|
||||||
self._database = database
|
self._database = database
|
||||||
|
Loading…
x
Reference in New Issue
Block a user