89 lines
3.2 KiB
TypeScript

import path from "node:path";
import { readFile, writeFile, mkdir, rename, rm } from "node:fs/promises";
import { createHash, randomUUID } from "node:crypto";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { captionFont } from "./fonts";
const pending = new Map<string, Promise<string>>();
const sha = (data: Uint8Array) => createHash("sha256").update(data).digest("hex");
export function captionFontFile(id: string): Promise<string> {
const font = captionFont(id);
if (!font) return Promise.reject(Error("未知字幕字体"));
if (!font.url)
return Promise.resolve(
path.join(
process.env.SMART_EDIT_FONTS_DIR || path.resolve("public/fonts/smart-edit"),
font.file,
),
);
if (pending.has(id)) return pending.get(id)!;
const work = (async () => {
const root = path.resolve(
process.env.SMART_EDIT_FONT_CACHE_DIR || ".cache/smart-edit-fonts",
);
await mkdir(root, { recursive: true });
const output = path.join(root, font.file);
try {
const data = await readFile(output);
if (font.preparedSha256 && sha(data) === font.preparedSha256) return output;
} catch {}
if (pending.size > 4) throw Error("字体正在加载,请稍后重试");
const source = new URL(font.url!);
if (
source.protocol !== "https:" ||
!["fonts.gstatic.com", "fonts.gstatic.cn", "libtv-res.liblib.art"].includes(
source.hostname,
)
)
throw Error("字体来源无效");
const response = await fetch(source, {
signal: AbortSignal.timeout(60000),
redirect: "error",
cache: "no-store",
});
if (!response.ok || Number(response.headers.get("content-length")) > 40 * 1024 * 1024)
throw Error("字体下载失败");
const reader = response.body!.getReader(),
chunks: Uint8Array[] = [];
let bytes = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
bytes += value.length;
if (bytes > 40 * 1024 * 1024) throw Error("字体文件过大");
chunks.push(value);
}
} finally {
await reader.cancel();
}
const data = Buffer.concat(chunks);
if (!font.sha256 || sha(data) !== font.sha256)
throw Error("字体资源校验失败,请更新字体目录");
const temp = path.join(root, randomUUID()),
prepared = temp + ".ttf";
try {
await writeFile(temp, data);
await promisify(execFile)(
process.env.SMART_EDIT_FONT_PYTHON || process.env.SMART_EDIT_PYTHON || "python3",
[
process.env.SMART_EDIT_FONT_SCRIPT || path.resolve("services/smart-edit/font.py"),
temp,
prepared,
font.family,
],
{ timeout: 120000, maxBuffer: 512 * 1024 },
);
if (sha(await readFile(prepared)) !== font.preparedSha256)
throw Error("字体处理版本不一致,请安装指定字体依赖");
await rename(prepared, output);
return output;
} finally {
await rm(temp, { force: true });
await rm(prepared, { force: true });
}
})().finally(() => pending.delete(id));
pending.set(id, work);
return work;
}