Fix & test movie, episodes & slug linking

This commit is contained in:
Zoe Roux 2025-05-01 21:38:51 +02:00
parent 060c4d74b4
commit 6a5862ddd2
No known key found for this signature in database
9 changed files with 172 additions and 47 deletions

View File

@ -79,7 +79,6 @@ export const historyH = new Elysia({ tags: ["profiles"] })
filter: and( filter: and(
isNotNull(entryProgressQ.playedDate), isNotNull(entryProgressQ.playedDate),
ne(entries.kind, "extra"), ne(entries.kind, "extra"),
ne(entries.kind, "unknown"),
filter, filter,
), ),
languages: langs, languages: langs,
@ -125,7 +124,6 @@ export const historyH = new Elysia({ tags: ["profiles"] })
filter: and( filter: and(
isNotNull(entryProgressQ.playedDate), isNotNull(entryProgressQ.playedDate),
ne(entries.kind, "extra"), ne(entries.kind, "extra"),
ne(entries.kind, "unknown"),
filter, filter,
), ),
languages: langs, languages: langs,

View File

@ -212,10 +212,10 @@ export const insertEntries = async (
})); }));
}; };
export function computeVideoSlug(showSlug: SQL | Column, needsRendering: SQL) { export function computeVideoSlug(entrySlug: SQL | Column, needsRendering: SQL) {
return sql<string>` return sql<string>`
concat( concat(
${showSlug}, ${entrySlug},
case when ${videos.part} is not null then ('-p' || ${videos.part}) else '' end, case when ${videos.part} is not null then ('-p' || ${videos.part}) else '' end,
case when ${videos.version} <> 1 then ('-v' || ${videos.version}) else '' end, case when ${videos.version} <> 1 then ('-v' || ${videos.version}) else '' end,
case when ${needsRendering} then concat('-', ${videos.rendering}) else '' end case when ${needsRendering} then concat('-', ${videos.rendering}) else '' end

View File

@ -210,11 +210,12 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
pk: entries.pk, pk: entries.pk,
id: entries.id, id: entries.id,
slug: entries.slug, slug: entries.slug,
kind: entries.kind,
seasonNumber: entries.seasonNumber, seasonNumber: entries.seasonNumber,
episodeNumber: entries.episodeNumber, episodeNumber: entries.episodeNumber,
order: entries.order, order: entries.order,
showId: shows.id, showId: sql`${shows.id}`.as("showId"),
showSlug: shows.slug, showSlug: sql`${shows.slug}`.as("showSlug"),
}) })
.from(entries) .from(entries)
.innerJoin(shows, eq(entries.showPk, shows.pk)) .innerJoin(shows, eq(entries.showPk, shows.pk))
@ -231,10 +232,10 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
db db
.select({ .select({
entryPk: entriesQ.pk, entryPk: entriesQ.pk,
videoPk: sql`j.video`, videoPk: videos.pk,
slug: computeVideoSlug( slug: computeVideoSlug(
entriesQ.showSlug, entriesQ.slug,
sql`j.needRendering || exists(${hasRenderingQ})`, sql`j.needRendering or exists(${hasRenderingQ})`,
), ),
}) })
.from( .from(
@ -244,39 +245,51 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
entry: "jsonb", entry: "jsonb",
}).as("j"), }).as("j"),
) )
.innerJoin(videos, eq(videos.pk, sql`j.video`))
.innerJoin( .innerJoin(
entriesQ, entriesQ,
or( or(
and( and(
sql`j.entry ? 'slug'`, sql`j.entry ? 'slug'`,
eq(entriesQ.slug, sql`j.entry->'slug'`), eq(entriesQ.slug, sql`j.entry->>'slug'`),
), ),
and( and(
sql`j.entry ? 'movie'`, sql`j.entry ? 'movie'`,
or( or(
eq(entriesQ.showId, sql`j.entry #> '{movie, id}'`), eq(entriesQ.showId, sql`(j.entry #>> '{movie, id}')::uuid`),
eq(entriesQ.showSlug, sql`j.entry #> '{movie, slug}'`), eq(entriesQ.showSlug, sql`j.entry #>> '{movie, slug}'`),
), ),
eq(entriesQ.kind, "movie"),
), ),
and( and(
sql`j.entry ? 'serie'`, sql`j.entry ? 'serie'`,
or( or(
eq(entriesQ.showId, sql`j.entry #> '{serie, id}'`), eq(entriesQ.showId, sql`(j.entry #>> '{serie, id}')::uuid`),
eq(entriesQ.showSlug, sql`j.entry #> '{serie, slug}'`), eq(entriesQ.showSlug, sql`j.entry #>> '{serie, slug}'`),
), ),
or( or(
and( and(
sql`j.entry ?& array['season', 'episode']`, sql`j.entry ?& array['season', 'episode']`,
eq(entriesQ.seasonNumber, sql`j.entry->'season'`), eq(
eq(entriesQ.episodeNumber, sql`j.entry->'episode'`), entriesQ.seasonNumber,
sql`(j.entry->>'season')::integer`,
),
eq(
entriesQ.episodeNumber,
sql`(j.entry->>'episode')::integer`,
),
), ),
and( and(
sql`j.entry ? 'order'`, sql`j.entry ? 'order'`,
eq(entriesQ.order, sql`j.entry->'order'`), eq(entriesQ.order, sql`(j.entry->>'order')::float`),
), ),
and( and(
sql`j.entry ? 'special'`, sql`j.entry ? 'special'`,
eq(entriesQ.episodeNumber, sql`j.entry->'special'`), eq(
entriesQ.episodeNumber,
sql`(j.entry->>'special')::integer`,
),
eq(entriesQ.kind, "special"),
), ),
), ),
), ),
@ -299,7 +312,11 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
); );
return error( return error(
201, 201,
vids.map((x) => ({ id: x.id, path: x.path, entries: entr[x.pk] })), vids.map((x) => ({
id: x.id,
path: x.path,
entries: entr[x.pk] ?? [],
})),
); );
}, },
{ {
@ -313,7 +330,7 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
`, `,
}, },
body: t.Array(SeedVideo), body: t.Array(SeedVideo),
// response: { 201: t.Array(CreatedVideo) }, response: { 201: t.Array(CreatedVideo) },
}, },
) )
.delete( .delete(

View File

@ -3,7 +3,6 @@ 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-s1e13",
path: "/video/Made in abyss S01E13.mkv", path: "/video/Made in abyss S01E13.mkv",
rendering: "459429fa062adeebedcc2bb04b9965de0262bfa453369783132d261be79021bd", rendering: "459429fa062adeebedcc2bb04b9965de0262bfa453369783132d261be79021bd",
part: null, part: null,

View File

@ -78,7 +78,7 @@ export const SeedVideo = t.Object({
}), }),
}), }),
t.Object({ t.Object({
externalId: t.Optional(t.Union([EpisodeId, ExternalId()])), externalId: t.Union([EpisodeId, ExternalId()]),
}), }),
t.Object({ t.Object({
movie: t.Union([ movie: t.Union([

View File

@ -1,29 +1,34 @@
import { db, migrate } from "~/db"; import { db, migrate } from "~/db";
import { profiles, shows } from "~/db/schema"; import { profiles, shows } from "~/db/schema";
import { madeInAbyss } from "~/models/examples"; import { madeInAbyss } from "~/models/examples";
import { createSerie, getSerie, setSerieStatus } from "./helpers"; import { createSerie, createVideo } from "./helpers";
import { getJwtHeaders } from "./helpers/jwt";
// test file used to run manually using `bun tests/manual.ts` // test file used to run manually using `bun tests/manual.ts`
// run those before running this script
// export JWT_SECRET="this is a secret";
// export JWT_ISSUER="https://kyoo.zoriya.dev";
await migrate(); await migrate();
await db.delete(shows); await db.delete(shows);
await db.delete(profiles); await db.delete(profiles);
console.log(await getJwtHeaders()); const [__, ser] = await createSerie(madeInAbyss);
const [_, ser] = await createSerie(madeInAbyss);
console.log(ser); console.log(ser);
const [__, ret] = await setSerieStatus(madeInAbyss.slug, { const [_, body] = await createVideo({
status: "watching", guess: { title: "mia", season: [1], episode: [13], from: "test" },
startedAt: "2024-12-21", part: null,
completedAt: "2024-12-21", path: "/video/mia s1e13.mkv",
seenCount: 2, rendering: "renderingsha",
score: 85, version: 1,
for: [
{
serie: madeInAbyss.slug,
season: madeInAbyss.entries[0].seasonNumber!,
episode: madeInAbyss.entries[0].episodeNumber!,
},
],
}); });
console.log(ret); console.log(body);
const [___, got] = await getSerie(madeInAbyss.slug, {});
console.log(JSON.stringify(got, undefined, 4));
process.exit(0); process.exit(0);

View File

@ -48,7 +48,7 @@ describe("Get entries", () => {
expect(body.items[0].videos).toBeArrayOfSize(1); expect(body.items[0].videos).toBeArrayOfSize(1);
expect(body.items[0].videos[0]).toMatchObject({ expect(body.items[0].videos[0]).toMatchObject({
path: madeInAbyssVideo.path, path: madeInAbyssVideo.path,
slug: madeInAbyssVideo.slug, slug: `${madeInAbyss.slug}-s1e13`,
version: madeInAbyssVideo.version, version: madeInAbyssVideo.version,
rendering: madeInAbyssVideo.rendering, rendering: madeInAbyssVideo.rendering,
part: madeInAbyssVideo.part, part: madeInAbyssVideo.part,
@ -63,7 +63,7 @@ describe("Get entries", () => {
expect(body.items[0].videos).toBeArrayOfSize(1); expect(body.items[0].videos).toBeArrayOfSize(1);
expect(body.items[0].videos[0]).toMatchObject({ expect(body.items[0].videos[0]).toMatchObject({
path: madeInAbyssVideo.path, path: madeInAbyssVideo.path,
slug: madeInAbyssVideo.slug, slug: `${madeInAbyss.slug}-s1e13`,
version: madeInAbyssVideo.version, version: madeInAbyssVideo.version,
rendering: madeInAbyssVideo.rendering, rendering: madeInAbyssVideo.rendering,
part: madeInAbyssVideo.part, part: madeInAbyssVideo.part,

View File

@ -57,7 +57,7 @@ describe("Serie seeding", () => {
], ],
evj: [ evj: [
expect.objectContaining({ expect.objectContaining({
slug: madeInAbyssVideo.slug, slug: `${madeInAbyss.slug}-s1e13`,
video: expect.objectContaining({ path: madeInAbyssVideo.path }), video: expect.objectContaining({ path: madeInAbyssVideo.path }),
}), }),
], ],

View File

@ -77,18 +77,50 @@ describe("Video seeding", () => {
expect(vid!.evj[0].entry.slug).toBe(`${madeInAbyss.slug}-s1e13`); expect(vid!.evj[0].entry.slug).toBe(`${madeInAbyss.slug}-s1e13`);
}); });
it("With movie", async () => {
const [resp, body] = await createVideo({
guess: { title: "bubble", from: "test" },
part: null,
path: "/video/bubble.mkv",
rendering: "sha",
version: 1,
for: [{ movie: bubble.slug }],
});
expectStatus(resp, body).toBe(201);
expect(body).toBeArrayOfSize(1);
expect(body[0].id).toBeString();
const vid = await db.query.videos.findFirst({
where: eq(videos.id, body[0].id),
with: {
evj: { with: { entry: true } },
},
});
expect(vid).not.toBeNil();
expect(vid!.path).toBe("/video/bubble.mkv");
expect(vid!.guess).toMatchObject({ title: "bubble", from: "test" });
expect(body[0].entries).toBeArrayOfSize(1);
expect(vid!.evj).toBeArrayOfSize(1);
expect(vid!.evj[0].slug).toBe(bubble.slug);
expect(vid!.evj[0].entry.slug).toBe(bubble.slug);
});
it("With season/episode", async () => { it("With season/episode", async () => {
const [resp, body] = await createVideo({ const [resp, body] = await createVideo({
guess: { title: "mia", season: [1], episode: [13], from: "test" }, guess: { title: "mia", season: [2], episode: [1], from: "test" },
part: null, part: null,
path: "/video/mia s1e13.mkv", path: "/video/mia s2e1.mkv",
rendering: "renderingsha", rendering: "renderingsha",
version: 1, version: 1,
for: [ for: [
{ {
serie: madeInAbyss.slug, serie: madeInAbyss.slug,
season: madeInAbyss.entries[0].seasonNumber!, season: madeInAbyss.entries[3].seasonNumber!,
episode: madeInAbyss.entries[0].episodeNumber!, episode: madeInAbyss.entries[3].episodeNumber!,
}, },
], ],
}); });
@ -105,13 +137,87 @@ describe("Video seeding", () => {
}); });
expect(vid).not.toBeNil(); expect(vid).not.toBeNil();
expect(vid!.path).toBe("/video/mia s1e13.mkv"); expect(vid!.path).toBe("/video/mia s2e1.mkv");
expect(vid!.guess).toMatchObject({ title: "mia", from: "test" }); expect(vid!.guess).toMatchObject({ title: "mia", from: "test" });
expect(body[0].entries).toBeArrayOfSize(1); expect(body[0].entries).toBeArrayOfSize(1);
expect(vid!.evj).toBeArrayOfSize(1); expect(vid!.evj).toBeArrayOfSize(1);
expect(vid!.evj[0].slug).toBe(`${madeInAbyss.slug}-s1e13-renderingsha`); expect(vid!.evj[0].slug).toBe(`${madeInAbyss.slug}-s2e1`);
expect(vid!.evj[0].entry.slug).toBe(`${madeInAbyss.slug}-s1e13`); expect(vid!.evj[0].entry.slug).toBe(`${madeInAbyss.slug}-s2e1`);
});
it("With special", async () => {
const [resp, body] = await createVideo({
guess: { title: "mia", season: [0], episode: [3], from: "test" },
part: null,
path: "/video/mia sp3.mkv",
rendering: "notehu",
version: 1,
for: [
{
serie: madeInAbyss.slug,
special: madeInAbyss.entries[1].number!,
},
],
});
expectStatus(resp, body).toBe(201);
expect(body).toBeArrayOfSize(1);
expect(body[0].id).toBeString();
const vid = await db.query.videos.findFirst({
where: eq(videos.id, body[0].id),
with: {
evj: { with: { entry: true } },
},
});
expect(vid).not.toBeNil();
expect(vid!.path).toBe("/video/mia sp3.mkv");
expect(vid!.guess).toMatchObject({ title: "mia", from: "test" });
expect(body[0].entries).toBeArrayOfSize(1);
expect(vid!.evj).toBeArrayOfSize(1);
expect(vid!.evj[0].slug).toBe(`${madeInAbyss.slug}-sp3`);
expect(vid!.evj[0].entry.slug).toBe(`${madeInAbyss.slug}-sp3`);
});
it("With order", async () => {
const [resp, body] = await createVideo({
guess: { title: "mia", season: [0], episode: [3], from: "test" },
part: null,
path: "/video/mia 13.5.mkv",
rendering: "notehu",
version: 1,
for: [
{
serie: madeInAbyss.slug,
order: 13.5,
},
],
});
expectStatus(resp, body).toBe(201);
expect(body).toBeArrayOfSize(1);
expect(body[0].id).toBeString();
const vid = await db.query.videos.findFirst({
where: eq(videos.id, body[0].id),
with: {
evj: { with: { entry: true } },
},
});
expect(vid).not.toBeNil();
expect(vid!.path).toBe("/video/mia 13.5.mkv");
expect(vid!.guess).toMatchObject({ title: "mia", from: "test" });
expect(body[0].entries).toBeArrayOfSize(1);
expect(vid!.evj).toBeArrayOfSize(1);
expect(vid!.evj[0].slug).toBe("made-in-abyss-dawn-of-the-deep-soul");
expect(vid!.evj[0].entry.slug).toBe("made-in-abyss-dawn-of-the-deep-soul");
}); });
}); });