diff --git a/fresh.gen.ts b/fresh.gen.ts index f9ed9e7..8f6e270 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -12,8 +12,10 @@ import * as $6 from "./routes/api/file/random.ts"; import * as $7 from "./routes/api/filemeta.ts"; import * as $8 from "./routes/api/filemeta/[token].ts"; import * as $9 from "./routes/api/gallery/[gid].ts"; -import * as $10 from "./routes/api/task.ts"; -import * as $11 from "./routes/index.tsx"; +import * as $10 from "./routes/api/status.ts"; +import * as $11 from "./routes/api/task.ts"; +import * as $12 from "./routes/api/thumbnail/[id].ts"; +import * as $13 from "./routes/index.tsx"; import * as $$0 from "./islands/Container.tsx"; import * as $$1 from "./islands/Settings.tsx"; import * as $$2 from "./islands/TaskManager.tsx"; @@ -30,8 +32,10 @@ const manifest = { "./routes/api/filemeta.ts": $7, "./routes/api/filemeta/[token].ts": $8, "./routes/api/gallery/[gid].ts": $9, - "./routes/api/task.ts": $10, - "./routes/index.tsx": $11, + "./routes/api/status.ts": $10, + "./routes/api/task.ts": $11, + "./routes/api/thumbnail/[id].ts": $12, + "./routes/index.tsx": $13, }, islands: { "./islands/Container.tsx": $$0, diff --git a/routes/api/status.ts b/routes/api/status.ts new file mode 100644 index 0000000..56d2815 --- /dev/null +++ b/routes/api/status.ts @@ -0,0 +1,28 @@ +import { Handlers } from "$fresh/server.ts"; +import { get_task_manager } from "../../server.ts"; +import { StatusData } from "../../server/status.ts"; +import { return_data } from "../../server/utils.ts"; +import { check_ffmpeg_binary } from "../../thumbnail/ffmpeg_binary.ts"; + +export const handler: Handlers = { + async GET(_req, _ctx) { + const m = get_task_manager(); + const ffmpeg_binary_enabled = await check_ffmpeg_binary( + m.cfg.ffmpeg_path, + ); + const meilisearch_enabled = m.meilisearch !== undefined; + const meilisearch = meilisearch_enabled && m.cfg.meili_host && + m.cfg.meili_update_api_key + ? { + host: m.cfg.meili_host, + key: m.cfg.meili_search_api_key || + m.cfg.meili_update_api_key, + } + : undefined; + return return_data({ + ffmpeg_binary_enabled, + meilisearch_enabled, + meilisearch, + }); + }, +}; diff --git a/routes/api/thumbnail/[id].ts b/routes/api/thumbnail/[id].ts new file mode 100644 index 0000000..d62dcb6 --- /dev/null +++ b/routes/api/thumbnail/[id].ts @@ -0,0 +1,80 @@ +import { Handlers } from "$fresh/server.ts"; +import { exists } from "std/fs/exists.ts"; +import { get_task_manager } from "../../../server.ts"; +import { parse_bool, parse_int } from "../../../server/parse_form.ts"; +import { generate_filename, ThumbnailConfig } from "../../../thumbnail/base.ts"; +import { sure_dir } from "../../../utils.ts"; +import { ThumbnailMethod } from "../../../config.ts"; +import { fb_generate_thumbnail } from "../../../thumbnail/ffmpeg_binary.ts"; +import { + get_file_response, + GetFileResponseOptions, +} from "../../../server/get_file_response.ts"; + +export const handler: Handlers = { + async GET(req, ctx) { + const id = parseInt(ctx.params.id); + if (isNaN(id)) { + return new Response("Bad Request", { status: 400 }); + } + const m = get_task_manager(); + const b = m.cfg.thumbnail_dir; + const method = m.cfg.thumbnail_method; + await sure_dir(b); + const f = m.db.get_file(id); + if (!f) { + return new Response("File not found.", { status: 404 }); + } + const u = new URL(req.url); + const max = await parse_int(u.searchParams.get("max"), 1200); + const width = await parse_int(u.searchParams.get("width"), null); + const height = await parse_int(u.searchParams.get("height"), null); + const quality = await parse_int(u.searchParams.get("quality"), 1); + const force = await parse_bool(u.searchParams.get("force"), false); + const cfg: ThumbnailConfig = { width: 0, height: 0, quality }; + if (width !== null && height !== null) { + cfg.width = width; + cfg.height = height; + } else if (width !== null) { + cfg.width = width; + cfg.height = Math.round(f.height / f.width * width); + } else if (height !== null) { + cfg.height = height; + cfg.width = Math.round(f.width / f.height * height); + } else { + if (f.width > f.height) { + cfg.width = max; + cfg.height = Math.round(f.height / f.width * max); + } else { + cfg.height = max; + cfg.width = Math.round(f.width / f.height * max); + } + } + if (!force) { + if (cfg.width > f.width || cfg.height > f.height) { + return Response.redirect(`${u.origin}/api/file/${f.id}`); + } + } + const output = generate_filename(b, f, cfg); + if (!(await exists(output))) { + if (method === ThumbnailMethod.FFMPEG_BINARY) { + const re = await fb_generate_thumbnail( + m.cfg.ffmpeg_path, + f.path, + output, + cfg, + ); + if (!re) { + return new Response("Failed to generate thumbnail.", { + status: 500, + }); + } + } + } + const opts: GetFileResponseOptions = {}; + opts.range = req.headers.get("range"); + opts.if_modified_since = req.headers.get("If-Modified-Since"); + opts.if_unmodified_since = req.headers.get("If-Unmodified-Since"); + return await get_file_response(output, opts); + }, +}; diff --git a/server/status.ts b/server/status.ts new file mode 100644 index 0000000..5a88fca --- /dev/null +++ b/server/status.ts @@ -0,0 +1,8 @@ +export type StatusData = { + ffmpeg_binary_enabled: boolean; + meilisearch_enabled: boolean; + meilisearch?: { + host: string; + key: string; + }; +}; diff --git a/thumbnail/base.ts b/thumbnail/base.ts new file mode 100644 index 0000000..9e1f502 --- /dev/null +++ b/thumbnail/base.ts @@ -0,0 +1,22 @@ +import { join } from "std/path/mod.ts"; +import { filterFilename } from "../utils.ts"; +import { EhFile } from "../db.ts"; + +export type ThumbnailConfig = { + width: number; + height: number; + quality: number; +}; + +export function generate_filename( + base: string, + f: EhFile, + cfg: ThumbnailConfig, +) { + return join( + base, + filterFilename( + `${f.id}-${f.token}-${cfg.width}x${cfg.height}-q${cfg.quality}.jpg`, + ), + ); +} diff --git a/thumbnail/ffmpeg_binary.ts b/thumbnail/ffmpeg_binary.ts new file mode 100644 index 0000000..b5f3d82 --- /dev/null +++ b/thumbnail/ffmpeg_binary.ts @@ -0,0 +1,43 @@ +import { ThumbnailConfig } from "./base.ts"; + +export async function check_ffmpeg_binary(p: string) { + const cmd = new Deno.Command(p, { + stdout: "null", + stderr: "null", + args: ["-h"], + }); + const c = cmd.spawn(); + const o = await c.output(); + return o.code === 0; +} + +export async function fb_generate_thumbnail( + p: string, + i: string, + o: string, + cfg: ThumbnailConfig, +) { + const args = [ + "-i", + i, + "-vf", + `scale=${cfg.width}:${cfg.height}`, + "-qmin", + `${cfg.quality}`, + "-qmax", + `${cfg.quality}`, + o, + ]; + const cmd = new Deno.Command(p, { args, stdout: "null", stderr: "piped" }); + const c = cmd.spawn(); + const s = await c.output(); + if (s.code !== 0) { + try { + const d = (new TextDecoder()).decode(s.stderr); + console.log(d); + } catch (_) { + console.log(s.stderr); + } + } + return s.code === 0; +}