123 lines
5.7 KiB
TypeScript
123 lines
5.7 KiB
TypeScript
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<string, string> = {};
|
|
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("<html></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(/已停用/);
|
|
});
|
|
});
|