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>(); const sha = (data: Uint8Array) => createHash("sha256").update(data).digest("hex"); export function captionFontFile(id: string): Promise { 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; }