Add tests for video linking

This commit is contained in:
Zoe Roux 2025-06-06 13:16:11 +02:00
parent 6e2743a4be
commit 489336c77a
No known key found for this signature in database
8 changed files with 164 additions and 38 deletions

View File

@ -515,10 +515,13 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
201: t.Array(
t.Object({
id: t.String({ format: "uuid" }),
path: t.String(),
path: t.String({ examples: ["/video/made in abyss s1e13.mkv"] }),
entries: t.Array(
t.Object({
slug: t.String({ format: "slug" }),
slug: t.String({
format: "slug",
examples: ["made-in-abyss-s1e13"],
}),
}),
),
}),

View File

@ -58,7 +58,7 @@ export const toDrizzle = (expr: Expression, config: FilterDef): SQL => {
{ 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) {
throw new KErrorT(

View File

@ -43,3 +43,20 @@ export const deleteVideo = async (paths: string[]) => {
const body = await resp.json();
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;
};

View File

@ -9,6 +9,7 @@ let bubbleId = "";
beforeAll(async () => {
await db.delete(shows);
await db.delete(videos);
await db.insert(videos).values(bubbleVideo);
const [ret, body] = await createMovie(bubble);
expect(ret.status).toBe(201);
@ -66,21 +67,29 @@ describe("Get movie", () => {
const [resp, body] = await getMovie(bubble.slug, { langs: "fr,pr,*" });
expectStatus(resp, body).toBe(200);
expect(body).toMatchObject({
slug: bubble.slug,
name: bubble.translations.en.name,
});
expect(resp.headers.get("Content-Language")).toBe("en");
expect(body.slug).toBe(bubble.slug);
const lang = resp.headers.get("Content-Language");
if (lang === "en") {
expect(body.name).toBe(bubble.translations.en.name);
} else if (lang === "ja") {
expect(body.name).toBe(bubble.translations.ja.name);
} else {
expect(lang).toBe("en");
}
});
it("Works without accept-language header", async () => {
const [resp, body] = await getMovie(bubble.slug, { langs: undefined });
expectStatus(resp, body).toBe(200);
expect(body).toMatchObject({
slug: bubble.slug,
name: bubble.translations.en.name,
});
expect(resp.headers.get("Content-Language")).toBe("en");
expect(body.slug).toBe(bubble.slug);
const lang = resp.headers.get("Content-Language");
if (lang === "en") {
expect(body.name).toBe(bubble.translations.en.name);
} 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 () => {
const [resp, body] = await getMovie(bubble.slug, { langs: "en-au" });

View File

@ -6,10 +6,11 @@ import {
createVideo,
deleteVideo,
getVideos,
linkVideos,
} from "tests/helpers";
import { expectStatus } from "tests/utils";
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";
beforeAll(async () => {
@ -57,12 +58,35 @@ beforeAll(async () => {
version: 1,
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);
expect(body).toBeArrayOfSize(3);
expect(body).toBeArrayOfSize(5);
expect(body[0].entries).toBeArrayOfSize(1);
expect(body[1].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();
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[0]).toBe("/video/mia s1e13 unknown test.mkv");
expect(body.unmatched).toBeArrayOfSize(3);
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 () => {
@ -249,3 +275,69 @@ describe("Video get/deletion", () => {
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);
});
});

View File

@ -7,9 +7,9 @@ from aiohttp import ClientSession
from pydantic import TypeAdapter
from .models.movie import Movie
from .models.request import Request
from .models.serie import Serie
from .models.videos import For, Resource, Video, VideoCreated, VideoInfo, VideoLink
from .requests import Request
from .utils import Singleton
logger = getLogger(__name__)
@ -96,7 +96,7 @@ class KyooClient(metaclass=Singleton):
)
async with self._client.post(
"videos",
"videos/link",
data=TypeAdapter(list[VideoLink]).dump_json(
[map_request(x) for x in videos],
by_alias=True,

View 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]

View File

@ -1,33 +1,18 @@
from __future__ import annotations
from asyncio import CancelledError, Event, TaskGroup
from logging import getLogger
from typing import Literal, cast
from typing import cast
from asyncpg import Connection, Pool
from pydantic import Field, TypeAdapter
from pydantic import TypeAdapter
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 .utils import Model
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:
def __init__(self, database: Connection):
self._database = database