feat(subtitles): 字幕站(SubHD、字幕库)在线搜索与下载;rar / 7z 解压;验证码与页面解析在沙箱里
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
92d456d123
commit
59b3680f35
32 files changed
+1704
-166
No files matched your search
@@ -0,0 +1,70 @@
|
||||
// 压缩包解压(沙箱内):zip 用 fflate,rar 用 node-unrar-js(WASM),7z 用 7z-wasm。
|
||||
// WASM 库要读自己包里的 .wasm 文件:prepareArchives() 在撤销权限前预热(载入内存),之后只在内存里解压。
|
||||
import SevenZip from "7z-wasm";
|
||||
import { unzipSync } from "fflate";
|
||||
import { createExtractorFromData } from "node-unrar-js";
|
||||
|
||||
type Seven = Awaited<ReturnType<typeof SevenZip>>;
|
||||
let seven: Seven | null = null;
|
||||
let warmed = false;
|
||||
|
||||
export async function prepareArchives(): Promise<void> {
|
||||
if (warmed) return;
|
||||
warmed = true;
|
||||
try {
|
||||
await createExtractorFromData({ data: new Uint8Array([0x52, 0x61, 0x72, 0x21]).buffer });
|
||||
} catch {
|
||||
// 只为载入 WASM,内容无效报错是预期的
|
||||
}
|
||||
seven = await SevenZip({ print: () => {}, printErr: () => {} });
|
||||
}
|
||||
|
||||
export type AcceptEntry = (name: string, size: number) => boolean;
|
||||
|
||||
export function archiveKindOf(name: string): "zip" | "rar" | "7z" | null {
|
||||
const m = /\.(zip|rar|7z)$/i.exec(name);
|
||||
return m ? (m[1]!.toLowerCase() as "zip" | "rar" | "7z") : null;
|
||||
}
|
||||
|
||||
/** 解出通过 accept 的条目;压缩包损坏 / 加密抛错 */
|
||||
export async function extractArchive(
|
||||
kind: "zip" | "rar" | "7z",
|
||||
bytes: Uint8Array,
|
||||
accept: AcceptEntry,
|
||||
): Promise<Record<string, Uint8Array>> {
|
||||
if (kind === "zip")
|
||||
return unzipSync(bytes, { filter: (e) => !e.name.endsWith("/") && accept(e.name, e.originalSize) });
|
||||
if (kind === "rar") {
|
||||
const ex = await createExtractorFromData({ data: bytes.slice().buffer });
|
||||
const out: Record<string, Uint8Array> = {};
|
||||
for (const f of ex.extract({ files: (h) => !h.flags.directory && accept(h.name, h.unpSize) }).files)
|
||||
if (f.extraction) out[f.fileHeader.name] = f.extraction;
|
||||
return out;
|
||||
}
|
||||
await prepareArchives();
|
||||
const z = seven!;
|
||||
const dir = `/x${crypto.randomUUID().slice(0, 8)}`;
|
||||
z.FS.writeFile(`${dir}.7z`, bytes);
|
||||
// callMain 出错时抛 ExitStatus 或静默不写文件:两种都当成损坏处理
|
||||
z.callMain(["x", `${dir}.7z`, `-o${dir}`, "-y", "-p-"]);
|
||||
try {
|
||||
z.FS.stat(dir);
|
||||
} catch {
|
||||
throw new Error("7z 解压失败");
|
||||
}
|
||||
const out: Record<string, Uint8Array> = {};
|
||||
const walk = (d: string, rel: string) => {
|
||||
for (const n of z.FS.readdir(d) as string[]) {
|
||||
if (n === "." || n === "..") continue;
|
||||
const p = `${d}/${n}`;
|
||||
const r = rel ? `${rel}/${n}` : n;
|
||||
if (z.FS.isDir(z.FS.stat(p).mode)) walk(p, r);
|
||||
else {
|
||||
const data = z.FS.readFile(p) as Uint8Array;
|
||||
if (accept(r, data.length)) out[r] = data;
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(dir, "");
|
||||
return out;
|
||||
}
|
||||
Reference in new issue
Block a user