import { SUBHD_DETAIL_PAGE, SUBHD_SUB_PAGE, ZIMUKU_CAPTCHA, ZIMUKU_CAPTCHA_PAGE, ZIMUKU_DETAIL_PAGE, ZIMUKU_DL_PAGE, ZIMUKU_SEARCH_PAGE, ZIMUKU_SUBS_PAGE, } from "@umt/testkit"; import { zipSync } from "fflate"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { downloadOnline, searchOnline } from "../../src/modules/subtitles/online/index.ts"; import { createHarness, type Harness, pgAvailable } from "../sources-identity/harness.ts"; // 在线字幕(docs/plan/subtitles.md 第 2 步):站点请求全部走注入的 fetch,页面是 2026-10-03 的真实截取; // 字幕库先回验证码页,沙箱识别后带 security_verify_img 提交,再开搜索页;SubHD 走 prepare-download → down → api/sub/down const pg = await pgAvailable(); const enc = (s: string) => new TextEncoder().encode(s); describe.skipIf(!pg)("在线字幕:SubHD / 字幕库(注入 fetch)", () => { let h: Harness; const requests: string[] = []; let verified = false; const siteIds: Record = {}; const html = (body: string, status = 200) => new Response(body, { status, headers: { "content-type": "text/html; charset=utf-8" } }); const zip = zipSync({ "The.Shawshank.Redemption.1994.chs&eng.ass": enc( "[Script Info]\nDialogue: 0,0:00:01.00,0:00:02.00,Default,,0,0,0,,你好 Hello", ), }); beforeAll(async () => { h = await createHarness("subonline", { fetch: async (input, init) => { const u = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url); const method = init?.method ?? "GET"; requests.push(`${method} ${u.host}${u.pathname}${u.search}`); if (u.host === "zmk.pw") { if (u.searchParams.has("security_verify_img")) { const want = [...ZIMUKU_CAPTCHA.code].map((c) => c.charCodeAt(0).toString(16)).join(""); verified = u.searchParams.get("security_verify_img") === want; return html("ok", 302); } if (!verified) return html(ZIMUKU_CAPTCHA_PAGE, 404); if (u.pathname === "/search") return html(ZIMUKU_SEARCH_PAGE); if (/^\/subs\/\d+\.html$/.test(u.pathname)) return html(ZIMUKU_SUBS_PAGE); if (u.pathname === "/detail/203404.html") return html(ZIMUKU_DETAIL_PAGE); if (u.pathname === "/dld/203404.html") return html(ZIMUKU_DL_PAGE); if (u.pathname.startsWith("/download/")) return new Response(zip, { headers: { "content-type": "application/octet-stream", "content-disposition": 'attachment; filename="sub.zip"', }, }); } if (u.host === "subhd.tv") { if (u.pathname === "/d/1292052") return html(SUBHD_DETAIL_PAGE); if (u.pathname.startsWith("/a/")) return html(SUBHD_SUB_PAGE); if (u.pathname === "/api/sub/prepare-download") return Response.json({ success: true, url: "/down/Fp90EG" }); if (u.pathname === "/down/Fp90EG") return html(""); if (u.pathname === "/api/sub/down") return Response.json({ success: true, pass: true, url: "https://dl.subhd.example/x/1.srt" }); } if (u.host === "dl.subhd.example") return new Response(enc("1\n00:00:01,000 --> 00:00:02,000\n你好\n"), { headers: { "content-disposition": 'attachment; filename="Shawshank.chs.srt"' }, }); return html("not found", 404); }, }); for (const [key, name, url] of [ ["subhd", "SubHD", "https://subhd.tv/"], ["zimuku", "字幕库", "https://zmk.pw/"], ] as const) { const r = await h.db .insertInto("site") .values({ key, name, kind: "subtitle", adapter: "subtitle", base_url: url } as never) .returning("id") .executeTakeFirstOrThrow(); siteIds[key] = r.id; } }, 120_000); afterAll(async () => { await h?.close(); }); // 字幕库样例是《葬送的芙莉莲》的搜索结果;SubHD 按豆瓣 ID 开条目页,不按标题过滤 const q = { keywords: ["葬送的芙莉莲"], doubanId: "1292052", season: null, episode: null }; it("搜索:SubHD 有豆瓣 ID 直接开条目页;字幕库先过验证码再搜;结果按语言与下载量排序", async () => { const r = await searchOnline(h.ctx, h.rt, q); expect(r.errors).toEqual([]); expect(r.noSites).toBe(false); expect(verified).toBe(true); expect(requests).toContain("GET subhd.tv/d/1292052"); const zim = r.results.filter((x) => x.siteKey === "zimuku"); const sub = r.results.filter((x) => x.siteKey === "subhd"); expect(zim.length).toBeGreaterThan(0); expect(sub.length).toBeGreaterThan(0); // 双语排在前面 expect(r.results[0]!.languages).toContain("双语"); expect(zim.find((x) => x.ref === "/detail/203404.html")).toMatchObject({ siteName: "字幕库", downloads: 17000 }); }); it("下载:字幕库走下载页取文件;SubHD 走 prepare-download → api/sub/down", async () => { const z = await downloadOnline(h.ctx, h.rt, siteIds.zimuku!, "/detail/203404.html"); expect(z).toMatchObject({ name: "sub.zip", siteKey: "zimuku", pageUrl: "https://zmk.pw/detail/203404.html" }); const s = await downloadOnline(h.ctx, h.rt, siteIds.subhd!, "/a/Fp90EG"); expect(s.name).toBe("Shawshank.chs.srt"); expect(requests).toContain("POST subhd.tv/api/sub/prepare-download"); }); it("字幕站停用:不搜;下载报错", async () => { await h.db.updateTable("site").set({ enabled: false }).where("kind", "=", "subtitle").execute(); const r = await searchOnline(h.ctx, h.rt, q); expect(r.noSites).toBe(true); await expect(downloadOnline(h.ctx, h.rt, siteIds.subhd!, "/a/x")).rejects.toThrow(/已停用/); }); });