mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-30 19:54:16 -04:00
wip: Add image downloading
This commit is contained in:
parent
82d8a00eb4
commit
1a11bc3492
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "api",
|
"name": "api",
|
||||||
"version": "1.0.50",
|
"version": "5.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun --watch src/index.ts",
|
"dev": "bun --watch src/index.ts",
|
||||||
"build": "bun build src/index.ts --target bun --outdir ./dist",
|
"build": "bun build src/index.ts --target bun --outdir ./dist",
|
||||||
|
@ -1,22 +1,74 @@
|
|||||||
|
import { eq, sql } from "drizzle-orm";
|
||||||
|
import { version } from "package.json";
|
||||||
|
import type { PoolClient } from "pg";
|
||||||
|
import { db } from "~/db";
|
||||||
|
import * as schema from "~/db/schema";
|
||||||
|
import { mqueue } from "~/db/schema/queue";
|
||||||
import type { Image } from "~/models/utils";
|
import type { Image } from "~/models/utils";
|
||||||
|
|
||||||
|
type ImageTask = {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
table: string;
|
||||||
|
column: string;
|
||||||
|
};
|
||||||
|
|
||||||
// this will only push a task to the image downloader service and not download it instantly.
|
// this will only push a task to the image downloader service and not download it instantly.
|
||||||
// this is both done to prevent to many requests to be sent at once and to make sure POST
|
// this is both done to prevent to many requests to be sent at once and to make sure POST
|
||||||
// requests are not blocked by image downloading or blurhash calculation
|
// requests are not blocked by image downloading or blurhash calculation
|
||||||
export const processImage = (url: string): Image => {
|
export const enqueueImage = async (
|
||||||
|
tx: typeof db,
|
||||||
|
url: string,
|
||||||
|
): Promise<Image> => {
|
||||||
const hasher = new Bun.CryptoHasher("sha256");
|
const hasher = new Bun.CryptoHasher("sha256");
|
||||||
hasher.update(url);
|
hasher.update(url);
|
||||||
|
const id = hasher.digest().toString("hex");
|
||||||
|
|
||||||
// TODO: download source, save it in multiples qualities & process blurhash
|
await tx.insert(mqueue).values({ kind: "image", message: { id, url } });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: hasher.digest().toString("hex"),
|
id,
|
||||||
source: url,
|
source: url,
|
||||||
blurhash: "",
|
blurhash: "",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const processOptImage = (url: string | null): Image | null => {
|
export const enqueueOptImage = async (
|
||||||
|
tx: typeof db,
|
||||||
|
url: string | null,
|
||||||
|
): Promise<Image | null> => {
|
||||||
if (!url) return null;
|
if (!url) return null;
|
||||||
return processImage(url);
|
return await enqueueImage(tx, url);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const processImages = async () => {
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
const [item] = await tx
|
||||||
|
.select()
|
||||||
|
.from(mqueue)
|
||||||
|
.for("update", { skipLocked: true })
|
||||||
|
.where(eq(mqueue.kind, "image"))
|
||||||
|
.orderBy(mqueue.createdAt)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
const img = item.message as ImageTask;
|
||||||
|
await fetch(img.url, { headers: { "User-Agent": `Kyoo v${version}` }, });
|
||||||
|
const blurhash = "";
|
||||||
|
|
||||||
|
const table = schema[img.table as keyof typeof schema] as any;
|
||||||
|
|
||||||
|
await tx
|
||||||
|
.update(table)
|
||||||
|
.set({
|
||||||
|
[img.column]: { id: img.id, source: img.url, blurhash } satisfies Image,
|
||||||
|
})
|
||||||
|
.where(eq(sql`${table[img.column]}->'id'`, img.id));
|
||||||
|
|
||||||
|
await tx.delete(mqueue).where(eq(mqueue.id, item.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = (await db.$client.connect()) as PoolClient;
|
||||||
|
client.on("notification", (evt) => {
|
||||||
|
if (evt.channel !== "image") return;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,7 @@ import { conflictUpdateAllExcept } from "~/db/utils";
|
|||||||
import type { SeedCollection } from "~/models/collections";
|
import type { SeedCollection } from "~/models/collections";
|
||||||
import type { SeedMovie } from "~/models/movie";
|
import type { SeedMovie } from "~/models/movie";
|
||||||
import type { SeedSerie } from "~/models/serie";
|
import type { SeedSerie } from "~/models/serie";
|
||||||
import { processOptImage } from "../images";
|
import { enqueueOptImage } from "../images";
|
||||||
|
|
||||||
type ShowTrans = typeof showTranslations.$inferInsert;
|
type ShowTrans = typeof showTranslations.$inferInsert;
|
||||||
|
|
||||||
@ -53,10 +53,10 @@ export const insertCollection = async (
|
|||||||
pk: ret.pk,
|
pk: ret.pk,
|
||||||
language: lang,
|
language: lang,
|
||||||
...tr,
|
...tr,
|
||||||
poster: processOptImage(tr.poster),
|
poster: enqueueOptImage(tr.poster),
|
||||||
thumbnail: processOptImage(tr.thumbnail),
|
thumbnail: enqueueOptImage(tr.thumbnail),
|
||||||
logo: processOptImage(tr.logo),
|
logo: enqueueOptImage(tr.logo),
|
||||||
banner: processOptImage(tr.banner),
|
banner: enqueueOptImage(tr.banner),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await tx
|
await tx
|
||||||
|
@ -8,7 +8,7 @@ import {
|
|||||||
} from "~/db/schema";
|
} from "~/db/schema";
|
||||||
import { conflictUpdateAllExcept, sqlarr, values } from "~/db/utils";
|
import { conflictUpdateAllExcept, sqlarr, values } from "~/db/utils";
|
||||||
import type { SeedEntry as SEntry, SeedExtra as SExtra } from "~/models/entry";
|
import type { SeedEntry as SEntry, SeedExtra as SExtra } from "~/models/entry";
|
||||||
import { processOptImage } from "../images";
|
import { enqueueOptImage } from "../images";
|
||||||
import { guessNextRefresh } from "../refresh";
|
import { guessNextRefresh } from "../refresh";
|
||||||
import { updateAvailableCount } from "./shows";
|
import { updateAvailableCount } from "./shows";
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ export const insertEntries = async (
|
|||||||
...entry,
|
...entry,
|
||||||
showPk: show.pk,
|
showPk: show.pk,
|
||||||
slug: generateSlug(show.slug, seed),
|
slug: generateSlug(show.slug, seed),
|
||||||
thumbnail: processOptImage(seed.thumbnail),
|
thumbnail: enqueueOptImage(seed.thumbnail),
|
||||||
nextRefresh:
|
nextRefresh:
|
||||||
entry.kind !== "extra"
|
entry.kind !== "extra"
|
||||||
? guessNextRefresh(entry.airDate ?? new Date())
|
? guessNextRefresh(entry.airDate ?? new Date())
|
||||||
@ -103,7 +103,7 @@ export const insertEntries = async (
|
|||||||
...tr,
|
...tr,
|
||||||
poster:
|
poster:
|
||||||
seed.kind === "movie"
|
seed.kind === "movie"
|
||||||
? processOptImage((tr as any).poster)
|
? enqueueOptImage((tr as any).poster)
|
||||||
: undefined,
|
: undefined,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
@ -2,7 +2,7 @@ import { db } from "~/db";
|
|||||||
import { seasonTranslations, seasons } from "~/db/schema";
|
import { seasonTranslations, seasons } from "~/db/schema";
|
||||||
import { conflictUpdateAllExcept } from "~/db/utils";
|
import { conflictUpdateAllExcept } from "~/db/utils";
|
||||||
import type { SeedSeason } from "~/models/season";
|
import type { SeedSeason } from "~/models/season";
|
||||||
import { processOptImage } from "../images";
|
import { enqueueOptImage } from "../images";
|
||||||
import { guessNextRefresh } from "../refresh";
|
import { guessNextRefresh } from "../refresh";
|
||||||
|
|
||||||
type SeasonI = typeof seasons.$inferInsert;
|
type SeasonI = typeof seasons.$inferInsert;
|
||||||
@ -43,9 +43,9 @@ export const insertSeasons = async (
|
|||||||
pk: ret[i].pk,
|
pk: ret[i].pk,
|
||||||
language: lang,
|
language: lang,
|
||||||
...tr,
|
...tr,
|
||||||
poster: processOptImage(tr.poster),
|
poster: enqueueOptImage(tr.poster),
|
||||||
thumbnail: processOptImage(tr.thumbnail),
|
thumbnail: enqueueOptImage(tr.thumbnail),
|
||||||
banner: processOptImage(tr.banner),
|
banner: enqueueOptImage(tr.banner),
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
await tx
|
await tx
|
||||||
|
@ -6,7 +6,7 @@ import type { SeedCollection } from "~/models/collections";
|
|||||||
import type { SeedMovie } from "~/models/movie";
|
import type { SeedMovie } from "~/models/movie";
|
||||||
import type { SeedSerie } from "~/models/serie";
|
import type { SeedSerie } from "~/models/serie";
|
||||||
import { getYear } from "~/utils";
|
import { getYear } from "~/utils";
|
||||||
import { processOptImage } from "../images";
|
import { enqueueOptImage } from "../images";
|
||||||
|
|
||||||
type Show = typeof shows.$inferInsert;
|
type Show = typeof shows.$inferInsert;
|
||||||
type ShowTrans = typeof showTranslations.$inferInsert;
|
type ShowTrans = typeof showTranslations.$inferInsert;
|
||||||
@ -27,10 +27,10 @@ export const insertShow = async (
|
|||||||
pk: ret.pk,
|
pk: ret.pk,
|
||||||
language: lang,
|
language: lang,
|
||||||
...tr,
|
...tr,
|
||||||
poster: processOptImage(tr.poster),
|
poster: enqueueOptImage(tr.poster),
|
||||||
thumbnail: processOptImage(tr.thumbnail),
|
thumbnail: enqueueOptImage(tr.thumbnail),
|
||||||
logo: processOptImage(tr.logo),
|
logo: enqueueOptImage(tr.logo),
|
||||||
banner: processOptImage(tr.banner),
|
banner: enqueueOptImage(tr.banner),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await tx
|
await tx
|
||||||
|
@ -3,7 +3,7 @@ import { db } from "~/db";
|
|||||||
import { roles, staff } from "~/db/schema";
|
import { roles, staff } from "~/db/schema";
|
||||||
import { conflictUpdateAllExcept } from "~/db/utils";
|
import { conflictUpdateAllExcept } from "~/db/utils";
|
||||||
import type { SeedStaff } from "~/models/staff";
|
import type { SeedStaff } from "~/models/staff";
|
||||||
import { processOptImage } from "../images";
|
import { enqueueOptImage } from "../images";
|
||||||
|
|
||||||
export const insertStaff = async (
|
export const insertStaff = async (
|
||||||
seed: SeedStaff[] | undefined,
|
seed: SeedStaff[] | undefined,
|
||||||
@ -14,7 +14,7 @@ export const insertStaff = async (
|
|||||||
return await db.transaction(async (tx) => {
|
return await db.transaction(async (tx) => {
|
||||||
const people = seed.map((x) => ({
|
const people = seed.map((x) => ({
|
||||||
...x.staff,
|
...x.staff,
|
||||||
image: processOptImage(x.staff.image),
|
image: enqueueOptImage(x.staff.image),
|
||||||
}));
|
}));
|
||||||
const ret = await tx
|
const ret = await tx
|
||||||
.insert(staff)
|
.insert(staff)
|
||||||
@ -32,7 +32,7 @@ export const insertStaff = async (
|
|||||||
order: i,
|
order: i,
|
||||||
character: {
|
character: {
|
||||||
...x.character,
|
...x.character,
|
||||||
image: processOptImage(x.character.image),
|
image: enqueueOptImage(x.character.image),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import { db } from "~/db";
|
|||||||
import { showStudioJoin, studioTranslations, studios } from "~/db/schema";
|
import { showStudioJoin, studioTranslations, studios } from "~/db/schema";
|
||||||
import { conflictUpdateAllExcept } from "~/db/utils";
|
import { conflictUpdateAllExcept } from "~/db/utils";
|
||||||
import type { SeedStudio } from "~/models/studio";
|
import type { SeedStudio } from "~/models/studio";
|
||||||
import { processOptImage } from "../images";
|
import { enqueueOptImage } from "../images";
|
||||||
|
|
||||||
type StudioI = typeof studios.$inferInsert;
|
type StudioI = typeof studios.$inferInsert;
|
||||||
type StudioTransI = typeof studioTranslations.$inferInsert;
|
type StudioTransI = typeof studioTranslations.$inferInsert;
|
||||||
@ -38,7 +38,7 @@ export const insertStudios = async (
|
|||||||
pk: ret[i].pk,
|
pk: ret[i].pk,
|
||||||
language: lang,
|
language: lang,
|
||||||
name: tr.name,
|
name: tr.name,
|
||||||
logo: processOptImage(tr.logo),
|
logo: enqueueOptImage(tr.logo),
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
await tx
|
await tx
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { t } from "elysia";
|
import { t } from "elysia";
|
||||||
import type { SeedMovie } from "~/models/movie";
|
import type { SeedMovie } from "~/models/movie";
|
||||||
import { getYear } from "~/utils";
|
import { getYear } from "~/utils";
|
||||||
import { processOptImage } from "./images";
|
import { enqueueOptImage } from "./images";
|
||||||
import { insertCollection } from "./insert/collection";
|
import { insertCollection } from "./insert/collection";
|
||||||
import { insertEntries } from "./insert/entries";
|
import { insertEntries } from "./insert/entries";
|
||||||
import { insertShow, updateAvailableCount } from "./insert/shows";
|
import { insertShow, updateAvailableCount } from "./insert/shows";
|
||||||
@ -80,10 +80,10 @@ export const seedMovie = async (
|
|||||||
language: movie.originalLanguage,
|
language: movie.originalLanguage,
|
||||||
name: original.name,
|
name: original.name,
|
||||||
latinName: original.latinName ?? null,
|
latinName: original.latinName ?? null,
|
||||||
poster: processOptImage(original.poster),
|
poster: enqueueOptImage(original.poster),
|
||||||
thumbnail: processOptImage(original.thumbnail),
|
thumbnail: enqueueOptImage(original.thumbnail),
|
||||||
logo: processOptImage(original.logo),
|
logo: enqueueOptImage(original.logo),
|
||||||
banner: processOptImage(original.banner),
|
banner: enqueueOptImage(original.banner),
|
||||||
},
|
},
|
||||||
...movie,
|
...movie,
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { t } from "elysia";
|
import { t } from "elysia";
|
||||||
import type { SeedSerie } from "~/models/serie";
|
import type { SeedSerie } from "~/models/serie";
|
||||||
import { getYear } from "~/utils";
|
import { getYear } from "~/utils";
|
||||||
import { processOptImage } from "./images";
|
import { enqueueOptImage } from "./images";
|
||||||
import { insertCollection } from "./insert/collection";
|
import { insertCollection } from "./insert/collection";
|
||||||
import { insertEntries } from "./insert/entries";
|
import { insertEntries } from "./insert/entries";
|
||||||
import { insertSeasons } from "./insert/seasons";
|
import { insertSeasons } from "./insert/seasons";
|
||||||
@ -115,10 +115,10 @@ export const seedSerie = async (
|
|||||||
language: serie.originalLanguage,
|
language: serie.originalLanguage,
|
||||||
name: original.name,
|
name: original.name,
|
||||||
latinName: original.latinName ?? null,
|
latinName: original.latinName ?? null,
|
||||||
poster: processOptImage(original.poster),
|
poster: enqueueOptImage(original.poster),
|
||||||
thumbnail: processOptImage(original.thumbnail),
|
thumbnail: enqueueOptImage(original.thumbnail),
|
||||||
logo: processOptImage(original.logo),
|
logo: enqueueOptImage(original.logo),
|
||||||
banner: processOptImage(original.banner),
|
banner: enqueueOptImage(original.banner),
|
||||||
},
|
},
|
||||||
...serie,
|
...serie,
|
||||||
},
|
},
|
||||||
|
23
api/src/db/schema/queue.ts
Normal file
23
api/src/db/schema/queue.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import {
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
jsonb,
|
||||||
|
timestamp,
|
||||||
|
uuid,
|
||||||
|
varchar,
|
||||||
|
} from "drizzle-orm/pg-core";
|
||||||
|
import { schema } from "./utils";
|
||||||
|
|
||||||
|
export const mqueue = schema.table(
|
||||||
|
"mqueue",
|
||||||
|
{
|
||||||
|
id: uuid().notNull().primaryKey().defaultRandom(),
|
||||||
|
kind: varchar({ length: 255 }).notNull(),
|
||||||
|
message: jsonb().notNull(),
|
||||||
|
attempt: integer().notNull().default(0),
|
||||||
|
createdAt: timestamp({ withTimezone: true, mode: "string" })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
},
|
||||||
|
(t) => [index("mqueue_created").on(t.createdAt)],
|
||||||
|
);
|
@ -3,15 +3,23 @@
|
|||||||
"target": "ES2021",
|
"target": "ES2021",
|
||||||
"module": "ES2022",
|
"module": "ES2022",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"types": ["bun-types"],
|
"types": [
|
||||||
|
"bun-types"
|
||||||
|
],
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"noErrorTruncation": true,
|
"noErrorTruncation": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["./src/*"]
|
"~/*": [
|
||||||
|
"./src/*"
|
||||||
|
],
|
||||||
|
"package.json": [
|
||||||
|
"package.json"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user