mirror of
				https://github.com/zoriya/Kyoo.git
				synced 2025-11-04 03:27:14 -05:00 
			
		
		
		
	Add routes to access images (#852)
This commit is contained in:
		
						commit
						b77aa39770
					
				
							
								
								
									
										386
									
								
								api/src/controllers/images.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										386
									
								
								api/src/controllers/images.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,386 @@
 | 
				
			|||||||
 | 
					import { stat } from "node:fs/promises";
 | 
				
			||||||
 | 
					import type { BunFile } from "bun";
 | 
				
			||||||
 | 
					import { type SQL, and, eq, sql } from "drizzle-orm";
 | 
				
			||||||
 | 
					import Elysia, { type Context, t } from "elysia";
 | 
				
			||||||
 | 
					import { db } from "~/db";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						showTranslations,
 | 
				
			||||||
 | 
						shows,
 | 
				
			||||||
 | 
						staff,
 | 
				
			||||||
 | 
						studioTranslations,
 | 
				
			||||||
 | 
						studios,
 | 
				
			||||||
 | 
					} from "~/db/schema";
 | 
				
			||||||
 | 
					import { sqlarr } from "~/db/utils";
 | 
				
			||||||
 | 
					import { KError } from "~/models/error";
 | 
				
			||||||
 | 
					import { bubble } from "~/models/examples";
 | 
				
			||||||
 | 
					import { AcceptLanguage, isUuid, processLanguages } from "~/models/utils";
 | 
				
			||||||
 | 
					import { imageDir } from "./seed/images";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getRedirectToImageHandler({
 | 
				
			||||||
 | 
						filter,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
						filter?: SQL;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
						return async function Handler({
 | 
				
			||||||
 | 
							params: { id, image },
 | 
				
			||||||
 | 
							headers: { "accept-language": languages },
 | 
				
			||||||
 | 
							query: { quality },
 | 
				
			||||||
 | 
							set,
 | 
				
			||||||
 | 
							error,
 | 
				
			||||||
 | 
							redirect,
 | 
				
			||||||
 | 
						}: {
 | 
				
			||||||
 | 
							params: { id: string; image: "poster" | "thumbnail" | "banner" | "logo" };
 | 
				
			||||||
 | 
							headers: { "accept-language": string };
 | 
				
			||||||
 | 
							query: { quality: "high" | "medium" | "low" };
 | 
				
			||||||
 | 
							set: Context["set"];
 | 
				
			||||||
 | 
							error: Context["error"];
 | 
				
			||||||
 | 
							redirect: Context["redirect"];
 | 
				
			||||||
 | 
						}) {
 | 
				
			||||||
 | 
							id ??= "random";
 | 
				
			||||||
 | 
							const lang = processLanguages(languages);
 | 
				
			||||||
 | 
							const item = db.$with("item").as(
 | 
				
			||||||
 | 
								db
 | 
				
			||||||
 | 
									.select({ pk: shows.pk })
 | 
				
			||||||
 | 
									.from(shows)
 | 
				
			||||||
 | 
									.where(
 | 
				
			||||||
 | 
										and(
 | 
				
			||||||
 | 
											filter,
 | 
				
			||||||
 | 
											id !== "random"
 | 
				
			||||||
 | 
												? isUuid(id)
 | 
				
			||||||
 | 
													? eq(shows.id, id)
 | 
				
			||||||
 | 
													: eq(shows.slug, id)
 | 
				
			||||||
 | 
												: undefined,
 | 
				
			||||||
 | 
										),
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
 | 
									.orderBy(sql`random()`)
 | 
				
			||||||
 | 
									.limit(1),
 | 
				
			||||||
 | 
							);
 | 
				
			||||||
 | 
							const [ret] = await db
 | 
				
			||||||
 | 
								.with(item)
 | 
				
			||||||
 | 
								.select({
 | 
				
			||||||
 | 
									image: showTranslations[image],
 | 
				
			||||||
 | 
									language: showTranslations.language,
 | 
				
			||||||
 | 
								})
 | 
				
			||||||
 | 
								.from(item)
 | 
				
			||||||
 | 
								.leftJoin(showTranslations, eq(item.pk, showTranslations.pk))
 | 
				
			||||||
 | 
								.where(
 | 
				
			||||||
 | 
									!lang.includes("*")
 | 
				
			||||||
 | 
										? eq(showTranslations.language, sql`any(${sqlarr(lang)})`)
 | 
				
			||||||
 | 
										: undefined,
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
								.orderBy(
 | 
				
			||||||
 | 
									sql`array_position(${sqlarr(lang)}, ${showTranslations.language})`,
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
								.limit(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (!ret) {
 | 
				
			||||||
 | 
								return error(404, {
 | 
				
			||||||
 | 
									status: 404,
 | 
				
			||||||
 | 
									message: `No item found with id or slug: '${id}'.`,
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (!ret.language) {
 | 
				
			||||||
 | 
								return error(422, {
 | 
				
			||||||
 | 
									status: 422,
 | 
				
			||||||
 | 
									message: "Accept-Language header could not be satisfied.",
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							set.headers["content-language"] = ret.language;
 | 
				
			||||||
 | 
							return quality
 | 
				
			||||||
 | 
								? redirect(`/images/${ret.image!.id}?quality=${quality}`)
 | 
				
			||||||
 | 
								: redirect(`/images/${ret.image!.id}`);
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const imagesH = new Elysia({ tags: ["images"] })
 | 
				
			||||||
 | 
						.get(
 | 
				
			||||||
 | 
							"/images/:id",
 | 
				
			||||||
 | 
							async ({ params: { id }, query: { quality }, headers: reqHeaders }) => {
 | 
				
			||||||
 | 
								const path = `${imageDir}/${id}.${quality}.jpg`;
 | 
				
			||||||
 | 
								const file = Bun.file(path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const etag = await generateETag(file);
 | 
				
			||||||
 | 
								if (await isCached(reqHeaders, etag, path))
 | 
				
			||||||
 | 
									return new Response(null, { status: 304 });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								const [start = 0, end = Number.POSITIVE_INFINITY] =
 | 
				
			||||||
 | 
									reqHeaders.range?.split("-").map(Number) ?? [];
 | 
				
			||||||
 | 
								return new Response(file.slice(start, end), {
 | 
				
			||||||
 | 
									headers: {
 | 
				
			||||||
 | 
										Etag: etag,
 | 
				
			||||||
 | 
										"Cache-Control": `public, max-age=${3 * 60 * 60}`,
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
								}) as any;
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								detail: { description: "Access an image by id." },
 | 
				
			||||||
 | 
								params: t.Object({
 | 
				
			||||||
 | 
									id: t.String({
 | 
				
			||||||
 | 
										desription: "Id of the image to retrive.",
 | 
				
			||||||
 | 
										format: "regex",
 | 
				
			||||||
 | 
										pattern: "^[0-9a-fA-F]*$",
 | 
				
			||||||
 | 
									}),
 | 
				
			||||||
 | 
								}),
 | 
				
			||||||
 | 
								query: t.Object({
 | 
				
			||||||
 | 
									quality: t.Optional(
 | 
				
			||||||
 | 
										t.UnionEnum(["high", "medium", "low"], {
 | 
				
			||||||
 | 
											default: "high",
 | 
				
			||||||
 | 
											description: "The quality you want your image to be in.",
 | 
				
			||||||
 | 
										}),
 | 
				
			||||||
 | 
									),
 | 
				
			||||||
 | 
								}),
 | 
				
			||||||
 | 
								response: {
 | 
				
			||||||
 | 
									200: t.File({ description: "The whole image" }),
 | 
				
			||||||
 | 
									206: t.File({ description: "Only the range of the image requested" }),
 | 
				
			||||||
 | 
									304: t.Void({ description: "Cached image already up-to-date" }),
 | 
				
			||||||
 | 
									404: { ...KError, description: "No image found with this id." },
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						.guard({
 | 
				
			||||||
 | 
							query: t.Object({
 | 
				
			||||||
 | 
								quality: t.Optional(
 | 
				
			||||||
 | 
									t.UnionEnum(["high", "medium", "low"], {
 | 
				
			||||||
 | 
										default: "high",
 | 
				
			||||||
 | 
										description: "The quality you want your image to be in.",
 | 
				
			||||||
 | 
									}),
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
							response: {
 | 
				
			||||||
 | 
								302: t.Void({
 | 
				
			||||||
 | 
									description:
 | 
				
			||||||
 | 
										"Redirected to the [/images/{id}](#tag/images/GET/images/{id}) route.",
 | 
				
			||||||
 | 
								}),
 | 
				
			||||||
 | 
								404: {
 | 
				
			||||||
 | 
									...KError,
 | 
				
			||||||
 | 
									description: "No item found with the given id or slug.",
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								422: KError,
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						.get(
 | 
				
			||||||
 | 
							"/staff/:id/image",
 | 
				
			||||||
 | 
							async ({ params: { id }, query: { quality }, error, redirect }) => {
 | 
				
			||||||
 | 
								const [ret] = await db
 | 
				
			||||||
 | 
									.select({ image: staff.image })
 | 
				
			||||||
 | 
									.from(staff)
 | 
				
			||||||
 | 
									.where(
 | 
				
			||||||
 | 
										id !== "random"
 | 
				
			||||||
 | 
											? isUuid(id)
 | 
				
			||||||
 | 
												? eq(shows.id, id)
 | 
				
			||||||
 | 
												: eq(shows.slug, id)
 | 
				
			||||||
 | 
											: undefined,
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
 | 
									.orderBy(sql`random()`)
 | 
				
			||||||
 | 
									.limit(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (!ret) {
 | 
				
			||||||
 | 
									return error(404, {
 | 
				
			||||||
 | 
										status: 404,
 | 
				
			||||||
 | 
										message: `No staff member found with id or slug: '${id}'.`,
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return quality
 | 
				
			||||||
 | 
									? redirect(`/images/${ret.image!.id}?quality=${quality}`)
 | 
				
			||||||
 | 
									: redirect(`/images/${ret.image!.id}`);
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								detail: { description: "Get the image of a staff member." },
 | 
				
			||||||
 | 
								params: t.Object({
 | 
				
			||||||
 | 
									id: t.String({
 | 
				
			||||||
 | 
										description: "The id or slug of the staff member.",
 | 
				
			||||||
 | 
										example: bubble.slug,
 | 
				
			||||||
 | 
									}),
 | 
				
			||||||
 | 
								}),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						.guard({
 | 
				
			||||||
 | 
							headers: t.Object({
 | 
				
			||||||
 | 
								"accept-language": AcceptLanguage(),
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						.get(
 | 
				
			||||||
 | 
							"/studios/:id/logo",
 | 
				
			||||||
 | 
							async ({
 | 
				
			||||||
 | 
								params: { id },
 | 
				
			||||||
 | 
								headers: { "accept-language": languages },
 | 
				
			||||||
 | 
								query: { quality },
 | 
				
			||||||
 | 
								set,
 | 
				
			||||||
 | 
								error,
 | 
				
			||||||
 | 
								redirect,
 | 
				
			||||||
 | 
							}) => {
 | 
				
			||||||
 | 
								const lang = processLanguages(languages);
 | 
				
			||||||
 | 
								const item = db.$with("item").as(
 | 
				
			||||||
 | 
									db
 | 
				
			||||||
 | 
										.select({ pk: studios.pk })
 | 
				
			||||||
 | 
										.from(studios)
 | 
				
			||||||
 | 
										.where(
 | 
				
			||||||
 | 
											id !== "random"
 | 
				
			||||||
 | 
												? isUuid(id)
 | 
				
			||||||
 | 
													? eq(studios.id, id)
 | 
				
			||||||
 | 
													: eq(studios.slug, id)
 | 
				
			||||||
 | 
												: undefined,
 | 
				
			||||||
 | 
										)
 | 
				
			||||||
 | 
										.orderBy(sql`random()`)
 | 
				
			||||||
 | 
										.limit(1),
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								const [ret] = await db
 | 
				
			||||||
 | 
									.with(item)
 | 
				
			||||||
 | 
									.select({
 | 
				
			||||||
 | 
										image: studioTranslations.logo,
 | 
				
			||||||
 | 
										language: studioTranslations.language,
 | 
				
			||||||
 | 
									})
 | 
				
			||||||
 | 
									.from(item)
 | 
				
			||||||
 | 
									.leftJoin(studioTranslations, eq(item.pk, studioTranslations.pk))
 | 
				
			||||||
 | 
									.where(
 | 
				
			||||||
 | 
										!lang.includes("*")
 | 
				
			||||||
 | 
											? eq(studioTranslations.language, sql`any(${sqlarr(lang)})`)
 | 
				
			||||||
 | 
											: undefined,
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
 | 
									.orderBy(
 | 
				
			||||||
 | 
										sql`array_position(${sqlarr(lang)}, ${studioTranslations.language})`,
 | 
				
			||||||
 | 
									)
 | 
				
			||||||
 | 
									.limit(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if (!ret) {
 | 
				
			||||||
 | 
									return error(404, {
 | 
				
			||||||
 | 
										status: 404,
 | 
				
			||||||
 | 
										message: `No studio found with id or slug: '${id}'.`,
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if (!ret.language) {
 | 
				
			||||||
 | 
									return error(422, {
 | 
				
			||||||
 | 
										status: 422,
 | 
				
			||||||
 | 
										message: "Accept-Language header could not be satisfied.",
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								set.headers["content-language"] = ret.language;
 | 
				
			||||||
 | 
								return quality
 | 
				
			||||||
 | 
									? redirect(`/images/${ret.image!.id}?quality=${quality}`)
 | 
				
			||||||
 | 
									: redirect(`/images/${ret.image!.id}`);
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								detail: { description: "Get the logo of a studio." },
 | 
				
			||||||
 | 
								params: t.Object({
 | 
				
			||||||
 | 
									id: t.String({
 | 
				
			||||||
 | 
										description: "The id or slug of the studio.",
 | 
				
			||||||
 | 
										example: bubble.slug,
 | 
				
			||||||
 | 
									}),
 | 
				
			||||||
 | 
								}),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						.get("/shows/random/:image", getRedirectToImageHandler({}), {
 | 
				
			||||||
 | 
							detail: { description: "Get the specified image of a random show." },
 | 
				
			||||||
 | 
							params: t.Object({
 | 
				
			||||||
 | 
								image: t.UnionEnum(["poster", "thumbnail", "logo", "banner"], {
 | 
				
			||||||
 | 
									description: "The type of image to retrive.",
 | 
				
			||||||
 | 
								}),
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						.guard({
 | 
				
			||||||
 | 
							params: t.Object({
 | 
				
			||||||
 | 
								id: t.String({
 | 
				
			||||||
 | 
									description: "The id or slug of the item to retrieve.",
 | 
				
			||||||
 | 
									example: bubble.slug,
 | 
				
			||||||
 | 
								}),
 | 
				
			||||||
 | 
								image: t.UnionEnum(["poster", "thumbnail", "logo", "banner"], {
 | 
				
			||||||
 | 
									description: "The type of image to retrive.",
 | 
				
			||||||
 | 
								}),
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
						.get(
 | 
				
			||||||
 | 
							"/movies/:id/:image",
 | 
				
			||||||
 | 
							getRedirectToImageHandler({
 | 
				
			||||||
 | 
								filter: eq(shows.kind, "movie"),
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								detail: { description: "Get the specified image of a movie" },
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						.get(
 | 
				
			||||||
 | 
							"/series/:id/:image",
 | 
				
			||||||
 | 
							getRedirectToImageHandler({
 | 
				
			||||||
 | 
								filter: eq(shows.kind, "serie"),
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								detail: { description: "Get the specified image of a serie" },
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						.get(
 | 
				
			||||||
 | 
							"/collections/:id/:image",
 | 
				
			||||||
 | 
							getRedirectToImageHandler({
 | 
				
			||||||
 | 
								filter: eq(shows.kind, "collection"),
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								detail: { description: "Get the specified image of a collection" },
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// stolen from https://github.com/elysiajs/elysia-static/blob/main/src/cache.ts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function isCached(
 | 
				
			||||||
 | 
						headers: Record<string, string | undefined>,
 | 
				
			||||||
 | 
						etag: string,
 | 
				
			||||||
 | 
						filePath: string,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
						// Always return stale when Cache-Control: no-cache
 | 
				
			||||||
 | 
						// to support end-to-end reload requests
 | 
				
			||||||
 | 
						// https://tools.ietf.org/html/rfc2616#section-14.9.4
 | 
				
			||||||
 | 
						if (
 | 
				
			||||||
 | 
							headers["cache-control"] &&
 | 
				
			||||||
 | 
							headers["cache-control"].indexOf("no-cache") !== -1
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// if-none-match
 | 
				
			||||||
 | 
						if ("if-none-match" in headers) {
 | 
				
			||||||
 | 
							const ifNoneMatch = headers["if-none-match"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (ifNoneMatch === "*") return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (ifNoneMatch === null) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (typeof etag !== "string") return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const isMatching = ifNoneMatch === etag;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (isMatching) return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							/**
 | 
				
			||||||
 | 
							 * A recipient MUST ignore If-Modified-Since if the request contains an
 | 
				
			||||||
 | 
							 * If-None-Match header field; the condition in If-None-Match is considered
 | 
				
			||||||
 | 
							 * to be a more accurate replacement for the condition in If-Modified-Since,
 | 
				
			||||||
 | 
							 * and the two are only combined for the sake of interoperating with older
 | 
				
			||||||
 | 
							 * intermediaries that might not implement If-None-Match.
 | 
				
			||||||
 | 
							 *
 | 
				
			||||||
 | 
							 * @see RFC 9110 section 13.1.3
 | 
				
			||||||
 | 
							 */
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// if-modified-since
 | 
				
			||||||
 | 
						if (headers["if-modified-since"]) {
 | 
				
			||||||
 | 
							const ifModifiedSince = headers["if-modified-since"];
 | 
				
			||||||
 | 
							let lastModified: Date | undefined;
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								lastModified = (await stat(filePath)).mtime;
 | 
				
			||||||
 | 
							} catch {
 | 
				
			||||||
 | 
								/* empty */
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (
 | 
				
			||||||
 | 
								lastModified !== undefined &&
 | 
				
			||||||
 | 
								lastModified.getTime() <= Date.parse(ifModifiedSince)
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
								return true;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function generateETag(file: BunFile) {
 | 
				
			||||||
 | 
						const hash = new Bun.CryptoHasher("md5");
 | 
				
			||||||
 | 
						hash.update(await file.arrayBuffer());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return hash.digest("base64");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import { mkdir, writeFile } from "node:fs/promises";
 | 
					import { mkdir, writeFile } from "node:fs/promises";
 | 
				
			||||||
import path from "node:path";
 | 
					import path from "node:path";
 | 
				
			||||||
import { encode } from "blurhash";
 | 
					import { encode } from "blurhash";
 | 
				
			||||||
import { type SQL, eq, is, sql } from "drizzle-orm";
 | 
					import { type SQL, and, eq, is, lt, sql } from "drizzle-orm";
 | 
				
			||||||
import { PgColumn, type PgTable } from "drizzle-orm/pg-core";
 | 
					import { PgColumn, type PgTable } from "drizzle-orm/pg-core";
 | 
				
			||||||
import { version } from "package.json";
 | 
					import { version } from "package.json";
 | 
				
			||||||
import type { PoolClient } from "pg";
 | 
					import type { PoolClient } from "pg";
 | 
				
			||||||
@ -86,24 +86,32 @@ export const processImages = async () => {
 | 
				
			|||||||
				.select()
 | 
									.select()
 | 
				
			||||||
				.from(mqueue)
 | 
									.from(mqueue)
 | 
				
			||||||
				.for("update", { skipLocked: true })
 | 
									.for("update", { skipLocked: true })
 | 
				
			||||||
				.where(eq(mqueue.kind, "image"))
 | 
									.where(and(eq(mqueue.kind, "image"), lt(mqueue.attempt, 5)))
 | 
				
			||||||
				.orderBy(mqueue.createdAt)
 | 
									.orderBy(mqueue.attempt, mqueue.createdAt)
 | 
				
			||||||
				.limit(1);
 | 
									.limit(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (!item) return false;
 | 
								if (!item) return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const img = item.message as ImageTask;
 | 
								const img = item.message as ImageTask;
 | 
				
			||||||
			const blurhash = await downloadImage(img.id, img.url);
 | 
								try {
 | 
				
			||||||
			const ret: Image = { id: img.id, source: img.url, blurhash };
 | 
									const blurhash = await downloadImage(img.id, img.url);
 | 
				
			||||||
 | 
									const ret: Image = { id: img.id, source: img.url, blurhash };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			const table = sql.raw(img.table);
 | 
									const table = sql.raw(img.table);
 | 
				
			||||||
			const column = sql.raw(img.column);
 | 
									const column = sql.raw(img.column);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			await tx.execute(sql`
 | 
									await tx.execute(sql`
 | 
				
			||||||
				update ${table} set ${column} = ${ret} where ${column}->'id' = ${sql.raw(`'"${img.id}"'::jsonb`)}
 | 
									update ${table} set ${column} = ${ret} where ${column}->'id' = ${sql.raw(`'"${img.id}"'::jsonb`)}
 | 
				
			||||||
			`);
 | 
								`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			await tx.delete(mqueue).where(eq(mqueue.id, item.id));
 | 
									await tx.delete(mqueue).where(eq(mqueue.id, item.id));
 | 
				
			||||||
 | 
								} catch (err) {
 | 
				
			||||||
 | 
									console.error("Failed to download image", img.url, err);
 | 
				
			||||||
 | 
									await tx
 | 
				
			||||||
 | 
										.update(mqueue)
 | 
				
			||||||
 | 
										.set({ attempt: sql`${mqueue.attempt}+1` })
 | 
				
			||||||
 | 
										.where(eq(mqueue.id, item.id));
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
			return true;
 | 
								return true;
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
import { Elysia } from "elysia";
 | 
					import { Elysia } from "elysia";
 | 
				
			||||||
import { entriesH } from "./controllers/entries";
 | 
					import { entriesH } from "./controllers/entries";
 | 
				
			||||||
 | 
					import { imagesH } from "./controllers/images";
 | 
				
			||||||
import { seasonsH } from "./controllers/seasons";
 | 
					import { seasonsH } from "./controllers/seasons";
 | 
				
			||||||
import { seed } from "./controllers/seed";
 | 
					import { seed } from "./controllers/seed";
 | 
				
			||||||
import { collections } from "./controllers/shows/collections";
 | 
					import { collections } from "./controllers/shows/collections";
 | 
				
			||||||
@ -53,7 +54,8 @@ export const app = new Elysia()
 | 
				
			|||||||
	.use(collections)
 | 
						.use(collections)
 | 
				
			||||||
	.use(entriesH)
 | 
						.use(entriesH)
 | 
				
			||||||
	.use(seasonsH)
 | 
						.use(seasonsH)
 | 
				
			||||||
	.use(videosH)
 | 
					 | 
				
			||||||
	.use(studiosH)
 | 
						.use(studiosH)
 | 
				
			||||||
	.use(staffH)
 | 
						.use(staffH)
 | 
				
			||||||
 | 
						.use(videosH)
 | 
				
			||||||
 | 
						.use(imagesH)
 | 
				
			||||||
	.use(seed);
 | 
						.use(seed);
 | 
				
			||||||
 | 
				
			|||||||
@ -60,6 +60,8 @@ app
 | 
				
			|||||||
					{ name: "movies", description: "Routes about movies" },
 | 
										{ name: "movies", description: "Routes about movies" },
 | 
				
			||||||
					{ name: "series", description: "Routes about series" },
 | 
										{ name: "series", description: "Routes about series" },
 | 
				
			||||||
					{ name: "collections", description: "Routes about collections" },
 | 
										{ name: "collections", description: "Routes about collections" },
 | 
				
			||||||
 | 
										{ name: "studios", description: "Routes about studios" },
 | 
				
			||||||
 | 
										{ name: "staff", description: "Routes about staff & roles" },
 | 
				
			||||||
					{
 | 
										{
 | 
				
			||||||
						name: "videos",
 | 
											name: "videos",
 | 
				
			||||||
						description: comment`
 | 
											description: comment`
 | 
				
			||||||
@ -67,8 +69,10 @@ app
 | 
				
			|||||||
							Can be used for administration or third party apps.
 | 
												Can be used for administration or third party apps.
 | 
				
			||||||
						`,
 | 
											`,
 | 
				
			||||||
					},
 | 
										},
 | 
				
			||||||
					{ name: "studios", description: "Routes about studios" },
 | 
										{
 | 
				
			||||||
					{ name: "staff", description: "Routes about staff & roles" },
 | 
											name: "images",
 | 
				
			||||||
 | 
											description: "Routes about images: posters, thumbnails...",
 | 
				
			||||||
 | 
										},
 | 
				
			||||||
				],
 | 
									],
 | 
				
			||||||
			},
 | 
								},
 | 
				
			||||||
		}),
 | 
							}),
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user