89 lines
3.2 KiB
TypeScript
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;
|
|
}
|