mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
Put POST /videos
in a transaction, handle dups
This commit is contained in:
parent
379765b28f
commit
466b67afe5
@ -160,166 +160,174 @@ export const videosH = new Elysia({ prefix: "/videos", tags: ["videos"] })
|
||||
.post(
|
||||
"",
|
||||
async ({ body, error }) => {
|
||||
const vids = await db
|
||||
.insert(videos)
|
||||
.values(body)
|
||||
.onConflictDoUpdate({
|
||||
target: [videos.path],
|
||||
set: conflictUpdateAllExcept(videos, ["pk", "id", "createdAt"]),
|
||||
})
|
||||
.returning({
|
||||
pk: videos.pk,
|
||||
id: videos.id,
|
||||
path: videos.path,
|
||||
return await db.transaction(async (tx) => {
|
||||
const vids = await tx
|
||||
.insert(videos)
|
||||
.values(body)
|
||||
.onConflictDoUpdate({
|
||||
target: [videos.path],
|
||||
set: conflictUpdateAllExcept(videos, ["pk", "id", "createdAt"]),
|
||||
})
|
||||
.returning({
|
||||
pk: videos.pk,
|
||||
id: videos.id,
|
||||
path: videos.path,
|
||||
});
|
||||
|
||||
const vidEntries = body.flatMap((x) => {
|
||||
if (!x.for) return [];
|
||||
return x.for.map((e) => ({
|
||||
video: vids.find((v) => v.path === x.path)!.pk,
|
||||
path: x.path,
|
||||
entry: {
|
||||
...e,
|
||||
movie:
|
||||
"movie" in e
|
||||
? isUuid(e.movie)
|
||||
? { id: e.movie }
|
||||
: { slug: e.movie }
|
||||
: undefined,
|
||||
serie:
|
||||
"serie" in e
|
||||
? isUuid(e.serie)
|
||||
? { id: e.serie }
|
||||
: { slug: e.serie }
|
||||
: undefined,
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
const vidEntries = body.flatMap((x) => {
|
||||
if (!x.for) return [];
|
||||
return x.for.map((e) => ({
|
||||
video: vids.find((v) => v.path === x.path)!.pk,
|
||||
path: x.path,
|
||||
entry: {
|
||||
...e,
|
||||
movie:
|
||||
"movie" in e
|
||||
? isUuid(e.movie)
|
||||
? { id: e.movie }
|
||||
: { slug: e.movie }
|
||||
: undefined,
|
||||
serie:
|
||||
"serie" in e
|
||||
? isUuid(e.serie)
|
||||
? { id: e.serie }
|
||||
: { slug: e.serie }
|
||||
: undefined,
|
||||
},
|
||||
}));
|
||||
});
|
||||
if (!vidEntries.length) {
|
||||
return error(
|
||||
201,
|
||||
vids.map((x) => ({ id: x.id, path: x.path, entries: [] })),
|
||||
);
|
||||
}
|
||||
|
||||
if (!vidEntries.length) {
|
||||
const entriesQ = tx
|
||||
.select({
|
||||
pk: entries.pk,
|
||||
id: entries.id,
|
||||
slug: entries.slug,
|
||||
kind: entries.kind,
|
||||
seasonNumber: entries.seasonNumber,
|
||||
episodeNumber: entries.episodeNumber,
|
||||
order: entries.order,
|
||||
showId: sql`${shows.id}`.as("showId"),
|
||||
showSlug: sql`${shows.slug}`.as("showSlug"),
|
||||
externalId: entries.externalId,
|
||||
})
|
||||
.from(entries)
|
||||
.innerJoin(shows, eq(entries.showPk, shows.pk))
|
||||
.as("entriesQ");
|
||||
|
||||
const hasRenderingQ = tx
|
||||
.select()
|
||||
.from(entryVideoJoin)
|
||||
.where(eq(entryVideoJoin.entryPk, entriesQ.pk));
|
||||
|
||||
const ret = await tx
|
||||
.insert(entryVideoJoin)
|
||||
.select(
|
||||
tx
|
||||
.selectDistinctOn([entriesQ.pk, videos.pk], {
|
||||
entryPk: entriesQ.pk,
|
||||
videoPk: videos.pk,
|
||||
slug: computeVideoSlug(
|
||||
entriesQ.slug,
|
||||
sql`exists(${hasRenderingQ})`,
|
||||
),
|
||||
})
|
||||
.from(
|
||||
values(vidEntries, {
|
||||
video: "integer",
|
||||
entry: "jsonb",
|
||||
}).as("j"),
|
||||
)
|
||||
.innerJoin(videos, eq(videos.pk, sql`j.video`))
|
||||
.innerJoin(
|
||||
entriesQ,
|
||||
or(
|
||||
and(
|
||||
sql`j.entry ? 'slug'`,
|
||||
eq(entriesQ.slug, sql`j.entry->>'slug'`),
|
||||
),
|
||||
and(
|
||||
sql`j.entry ? 'movie'`,
|
||||
or(
|
||||
eq(
|
||||
entriesQ.showId,
|
||||
sql`(j.entry #>> '{movie, id}')::uuid`,
|
||||
),
|
||||
eq(entriesQ.showSlug, sql`j.entry #>> '{movie, slug}'`),
|
||||
),
|
||||
eq(entriesQ.kind, "movie"),
|
||||
),
|
||||
and(
|
||||
sql`j.entry ? 'serie'`,
|
||||
or(
|
||||
eq(
|
||||
entriesQ.showId,
|
||||
sql`(j.entry #>> '{serie, id}')::uuid`,
|
||||
),
|
||||
eq(entriesQ.showSlug, sql`j.entry #>> '{serie, slug}'`),
|
||||
),
|
||||
or(
|
||||
and(
|
||||
sql`j.entry ?& array['season', 'episode']`,
|
||||
eq(
|
||||
entriesQ.seasonNumber,
|
||||
sql`(j.entry->>'season')::integer`,
|
||||
),
|
||||
eq(
|
||||
entriesQ.episodeNumber,
|
||||
sql`(j.entry->>'episode')::integer`,
|
||||
),
|
||||
),
|
||||
and(
|
||||
sql`j.entry ? 'order'`,
|
||||
eq(entriesQ.order, sql`(j.entry->>'order')::float`),
|
||||
),
|
||||
and(
|
||||
sql`j.entry ? 'special'`,
|
||||
eq(
|
||||
entriesQ.episodeNumber,
|
||||
sql`(j.entry->>'special')::integer`,
|
||||
),
|
||||
eq(entriesQ.kind, "special"),
|
||||
),
|
||||
),
|
||||
),
|
||||
and(
|
||||
sql`j.entry ? 'externalId'`,
|
||||
sql`j.entry->'externalId' <@ ${entriesQ.externalId}`,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.onConflictDoNothing()
|
||||
.returning({
|
||||
slug: entryVideoJoin.slug,
|
||||
entryPk: entryVideoJoin.entryPk,
|
||||
videoPk: entryVideoJoin.videoPk,
|
||||
});
|
||||
const entr = ret.reduce(
|
||||
(acc, x) => {
|
||||
acc[x.videoPk] ??= [];
|
||||
acc[x.videoPk].push({ slug: x.slug });
|
||||
return acc;
|
||||
},
|
||||
{} as Record<number, { slug: string }[]>,
|
||||
);
|
||||
return error(
|
||||
201,
|
||||
vids.map((x) => ({ id: x.id, path: x.path, entries: [] })),
|
||||
vids.map((x) => ({
|
||||
id: x.id,
|
||||
path: x.path,
|
||||
entries: entr[x.pk] ?? [],
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
const entriesQ = db
|
||||
.select({
|
||||
pk: entries.pk,
|
||||
id: entries.id,
|
||||
slug: entries.slug,
|
||||
kind: entries.kind,
|
||||
seasonNumber: entries.seasonNumber,
|
||||
episodeNumber: entries.episodeNumber,
|
||||
order: entries.order,
|
||||
showId: sql`${shows.id}`.as("showId"),
|
||||
showSlug: sql`${shows.slug}`.as("showSlug"),
|
||||
externalId: entries.externalId,
|
||||
})
|
||||
.from(entries)
|
||||
.innerJoin(shows, eq(entries.showPk, shows.pk))
|
||||
.as("entriesQ");
|
||||
|
||||
const hasRenderingQ = db
|
||||
.select()
|
||||
.from(entryVideoJoin)
|
||||
.where(eq(entryVideoJoin.entryPk, entriesQ.pk));
|
||||
|
||||
const ret = await db
|
||||
.insert(entryVideoJoin)
|
||||
.select(
|
||||
db
|
||||
.select({
|
||||
entryPk: entriesQ.pk,
|
||||
videoPk: videos.pk,
|
||||
slug: computeVideoSlug(
|
||||
entriesQ.slug,
|
||||
sql`exists(${hasRenderingQ})`,
|
||||
),
|
||||
})
|
||||
.from(
|
||||
values(vidEntries, {
|
||||
video: "integer",
|
||||
entry: "jsonb",
|
||||
}).as("j"),
|
||||
)
|
||||
.innerJoin(videos, eq(videos.pk, sql`j.video`))
|
||||
.innerJoin(
|
||||
entriesQ,
|
||||
or(
|
||||
and(
|
||||
sql`j.entry ? 'slug'`,
|
||||
eq(entriesQ.slug, sql`j.entry->>'slug'`),
|
||||
),
|
||||
and(
|
||||
sql`j.entry ? 'movie'`,
|
||||
or(
|
||||
eq(entriesQ.showId, sql`(j.entry #>> '{movie, id}')::uuid`),
|
||||
eq(entriesQ.showSlug, sql`j.entry #>> '{movie, slug}'`),
|
||||
),
|
||||
eq(entriesQ.kind, "movie"),
|
||||
),
|
||||
and(
|
||||
sql`j.entry ? 'serie'`,
|
||||
or(
|
||||
eq(entriesQ.showId, sql`(j.entry #>> '{serie, id}')::uuid`),
|
||||
eq(entriesQ.showSlug, sql`j.entry #>> '{serie, slug}'`),
|
||||
),
|
||||
or(
|
||||
and(
|
||||
sql`j.entry ?& array['season', 'episode']`,
|
||||
eq(
|
||||
entriesQ.seasonNumber,
|
||||
sql`(j.entry->>'season')::integer`,
|
||||
),
|
||||
eq(
|
||||
entriesQ.episodeNumber,
|
||||
sql`(j.entry->>'episode')::integer`,
|
||||
),
|
||||
),
|
||||
and(
|
||||
sql`j.entry ? 'order'`,
|
||||
eq(entriesQ.order, sql`(j.entry->>'order')::float`),
|
||||
),
|
||||
and(
|
||||
sql`j.entry ? 'special'`,
|
||||
eq(
|
||||
entriesQ.episodeNumber,
|
||||
sql`(j.entry->>'special')::integer`,
|
||||
),
|
||||
eq(entriesQ.kind, "special"),
|
||||
),
|
||||
),
|
||||
),
|
||||
and(
|
||||
sql`j.entry ? 'externalId'`,
|
||||
sql`j.entry->'externalId' <@ ${entriesQ.externalId}`,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.onConflictDoNothing()
|
||||
.returning({
|
||||
slug: entryVideoJoin.slug,
|
||||
entryPk: entryVideoJoin.entryPk,
|
||||
videoPk: entryVideoJoin.videoPk,
|
||||
});
|
||||
const entr = ret.reduce(
|
||||
(acc, x) => {
|
||||
acc[x.videoPk] ??= [];
|
||||
acc[x.videoPk].push({ slug: x.slug });
|
||||
return acc;
|
||||
},
|
||||
{} as Record<number, { slug: string }[]>,
|
||||
);
|
||||
return error(
|
||||
201,
|
||||
vids.map((x) => ({
|
||||
id: x.id,
|
||||
path: x.path,
|
||||
entries: entr[x.pk] ?? [],
|
||||
})),
|
||||
);
|
||||
});
|
||||
},
|
||||
{
|
||||
detail: {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PatternString } from "@sinclair/typebox";
|
||||
import { PatternStringExact } from "@sinclair/typebox";
|
||||
import { t } from "elysia";
|
||||
import { type Prettify, comment } from "~/utils";
|
||||
import { ExtraType } from "./entry/extra";
|
||||
@ -9,12 +9,13 @@ const ExternalIds = t.Record(
|
||||
t.String(),
|
||||
t.Omit(
|
||||
t.Union([
|
||||
EpisodeId.patternProperties[PatternString],
|
||||
ExternalId().patternProperties[PatternString],
|
||||
EpisodeId.patternProperties[PatternStringExact],
|
||||
ExternalId().patternProperties[PatternStringExact],
|
||||
]),
|
||||
["link"],
|
||||
),
|
||||
);
|
||||
type ExternalIds = typeof ExternalIds.static;
|
||||
|
||||
export const Guess = t.Recursive((Self) =>
|
||||
t.Object(
|
||||
|
@ -109,6 +109,38 @@ describe("Video seeding", () => {
|
||||
expect(vid!.evj[0].entry.slug).toBe(bubble.slug);
|
||||
});
|
||||
|
||||
it("Conflicting path", 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 () => {
|
||||
const [resp, body] = await createVideo({
|
||||
guess: { title: "mia", season: [2], episode: [1], from: "test" },
|
||||
@ -229,7 +261,7 @@ describe("Video seeding", () => {
|
||||
episode: [3],
|
||||
from: "test",
|
||||
externalId: {
|
||||
themoviedb: { serieId: "72636", season: 1, episode: 13 },
|
||||
themoviedatabase: { serieId: "72636", season: 1, episode: 13 },
|
||||
},
|
||||
},
|
||||
part: null,
|
||||
@ -239,7 +271,7 @@ describe("Video seeding", () => {
|
||||
for: [
|
||||
{
|
||||
externalId: {
|
||||
themoviedb: { serieId: "72636", season: 1, episode: 13 },
|
||||
themoviedatabase: { serieId: "72636", season: 1, episode: 13 },
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -273,7 +305,7 @@ describe("Video seeding", () => {
|
||||
title: "bubble",
|
||||
from: "test",
|
||||
externalId: {
|
||||
themoviedb: { dataId: "912598", season: 1, episode: 13 },
|
||||
themoviedatabase: { dataId: "912598" },
|
||||
},
|
||||
},
|
||||
part: null,
|
||||
@ -283,7 +315,7 @@ describe("Video seeding", () => {
|
||||
for: [
|
||||
{
|
||||
externalId: {
|
||||
themoviedb: { serieId: "912598", season: 1, episode: 13 },
|
||||
themoviedatabase: { dataId: "912598" },
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -317,18 +349,18 @@ describe("Video seeding", () => {
|
||||
title: "bubble",
|
||||
from: "test",
|
||||
externalId: {
|
||||
themoviedb: { dataId: "912598", season: 1, episode: 13 },
|
||||
themoviedatabase: { dataId: "912598" },
|
||||
},
|
||||
},
|
||||
part: null,
|
||||
path: "/video/bubble [tmdb=912598].mkv",
|
||||
rendering: "cwhtn",
|
||||
path: "/video/bubble ue [tmdb=912598].mkv",
|
||||
rendering: "aoeubnht",
|
||||
version: 1,
|
||||
for: [
|
||||
{ movie: "bubble" },
|
||||
{
|
||||
externalId: {
|
||||
themoviedb: { serieId: "912598", season: 1, episode: 13 },
|
||||
themoviedatabase: { dataId: "912598" },
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -346,13 +378,13 @@ describe("Video seeding", () => {
|
||||
});
|
||||
|
||||
expect(vid).not.toBeNil();
|
||||
expect(vid!.path).toBe("/video/bubble [tmdb=912598].mkv");
|
||||
expect(vid!.path).toBe("/video/bubble ue [tmdb=912598].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-cwhtn");
|
||||
expect(vid!.evj[0].slug).toBe("bubble-aoeubnht");
|
||||
expect(vid!.evj[0].entry.slug).toBe("bubble");
|
||||
});
|
||||
|
||||
@ -363,7 +395,7 @@ describe("Video seeding", () => {
|
||||
title: "bubble",
|
||||
from: "test",
|
||||
externalId: {
|
||||
themoviedb: { dataId: "912598", season: 1, episode: 13 },
|
||||
themoviedatabase: { dataId: "912598" },
|
||||
},
|
||||
},
|
||||
part: null,
|
||||
@ -374,7 +406,7 @@ describe("Video seeding", () => {
|
||||
{ movie: "bubble" },
|
||||
{
|
||||
externalId: {
|
||||
themoviedb: { serieId: "912598", season: 1, episode: 13 },
|
||||
themoviedatabase: { dataId: "912598" },
|
||||
},
|
||||
},
|
||||
],
|
||||
@ -401,4 +433,120 @@ describe("Video seeding", () => {
|
||||
expect(vid!.evj[0].slug).toBe("bubble");
|
||||
expect(vid!.evj[0].entry.slug).toBe("bubble");
|
||||
});
|
||||
|
||||
it("Multi part", async () => {
|
||||
await db.delete(videos);
|
||||
const [resp, body] = await createVideo([
|
||||
{
|
||||
guess: {
|
||||
title: "bubble",
|
||||
from: "test",
|
||||
externalId: {
|
||||
themoviedatabase: { dataId: "912598" },
|
||||
},
|
||||
},
|
||||
part: 1,
|
||||
path: "/video/bubble p1 [tmdb=912598].mkv",
|
||||
rendering: "cwhtn",
|
||||
version: 1,
|
||||
for: [
|
||||
{ movie: "bubble" },
|
||||
{
|
||||
externalId: {
|
||||
themoviedatabase: { dataId: "912598" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
guess: {
|
||||
title: "bubble",
|
||||
from: "test",
|
||||
externalId: {
|
||||
themoviedatabase: { dataId: "912598" },
|
||||
},
|
||||
},
|
||||
part: 2,
|
||||
path: "/video/bubble p2 [tmdb=912598].mkv",
|
||||
rendering: "cwhtn",
|
||||
version: 1,
|
||||
for: [
|
||||
{ movie: "bubble" },
|
||||
{
|
||||
externalId: {
|
||||
themoviedatabase: { dataId: "912598" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
expectStatus(resp, body).toBe(201);
|
||||
expect(body).toBeArrayOfSize(2);
|
||||
expect(body[0].id).toBeString();
|
||||
expect(body[1].id).toBeString();
|
||||
expect(body[0].entries).toBeArrayOfSize(1);
|
||||
expect(body[1].entries).toBeArrayOfSize(1);
|
||||
|
||||
const entr = (await db.query.entries.findFirst({
|
||||
where: eq(entries.slug, bubble.slug),
|
||||
with: {
|
||||
evj: { with: { video: true } },
|
||||
},
|
||||
}))!;
|
||||
|
||||
expect(entr.evj).toBeArrayOfSize(2);
|
||||
expect(entr.evj[0].video.path).toBe("/video/bubble p1 [tmdb=912598].mkv");
|
||||
|
||||
expect(entr.evj[0].slug).toBe("bubble-p1");
|
||||
expect(entr.evj[1].slug).toBe("bubble-p2");
|
||||
});
|
||||
|
||||
it("Multi entry", async () => {
|
||||
await db.delete(videos);
|
||||
const [resp, body] = await createVideo({
|
||||
guess: {
|
||||
title: "mia",
|
||||
season: [1, 2],
|
||||
episode: [13, 1],
|
||||
from: "test",
|
||||
},
|
||||
part: null,
|
||||
path: "/video/mia s1e13 & s2e1 [tmdb=72636].mkv",
|
||||
rendering: "notehu",
|
||||
version: 1,
|
||||
for: [
|
||||
{ serie: madeInAbyss.slug, season: 1, episode: 13 },
|
||||
{
|
||||
externalId: {
|
||||
themoviedatabase: { serieId: "72636", season: 1, episode: 13 },
|
||||
},
|
||||
},
|
||||
{ serie: madeInAbyss.slug, season: 2, episode: 1 },
|
||||
],
|
||||
});
|
||||
|
||||
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 s1e13 & s2e1 [tmdb=72636].mkv");
|
||||
expect(vid!.guess).toMatchObject({ title: "mia", from: "test" });
|
||||
|
||||
expect(body[0].entries).toBeArrayOfSize(2);
|
||||
expect(vid!.evj).toBeArrayOfSize(2);
|
||||
|
||||
expect(vid!.evj[0].slug).toBe("made-in-abyss-s1e13");
|
||||
expect(vid!.evj[0].entry.slug).toBe("made-in-abyss-s1e13");
|
||||
expect(vid!.evj[1].slug).toBe("made-in-abyss-s2e1");
|
||||
expect(vid!.evj[1].entry.slug).toBe("made-in-abyss-s2e1");
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user