Files
UHD_Media_Tool/packages/adapters/src/sites/spiders/public.test.ts
T
rootandClaude Opus 5.5 b8876542b8 feat(sites): 内置公开 BT 站 Nyaa / 蜜柑 / AniBT / AnimeGarden / 萌番组
- 新专用解析器(spiders/public.ts):RSS / Torznab / JSON API,搜索 + 最新列表 + 按资源 ID 取种子或磁力
- 手写定义(definitions/public.ts,public: true),并入内置定义表;定义视图按 public 标为 public_bt
- 添加站点:公开 BT 站可从内置站一键添加(专用适配器、默认定时抓取、默认不进搜索集合),手填 RSS 保留
- 样例取自 2026-10-02 公开接口,截前两条

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-02 21:19:19 +08:00

105 lines
4.9 KiB
TypeScript

import {
ANIBT_RSS_LATEST,
ANIBT_TORZNAB_SEARCH,
ANIMEGARDEN_RESOURCES,
BANGUMIMOE_LATEST,
createMockFetch,
MIKAN_RSS_SEARCH,
NYAA_RSS_SEARCH,
} from "@umt/testkit";
import { describe, expect, it } from "vitest";
import { BUILTIN_SITE_DEFINITIONS } from "../definitions/index.ts";
import { anibtSpider, animeGardenSpider, bangumiMoeSpider, mikanSpider, nyaaSpider } from "./public.ts";
import { adapterWith, withTorrent } from "./testing.ts";
import type { SpecializedSiteDefinition } from "./types.ts";
const def = (id: string) => BUILTIN_SITE_DEFINITIONS[id] as SpecializedSiteDefinition;
const XML = { "content-type": "application/rss+xml; charset=utf-8" };
describe("公开 BT 站(本项目扩展)", () => {
it("Nyaa:RSS 搜索带 q;做种 / 下载 / info hash / 大小;下载按 ID 拼 .torrent", async () => {
const m = createMockFetch({ "GET /": { body: NYAA_RSS_SEARCH, headers: XML } });
const t = withTorrent(m.fetch, (u) => u.pathname.startsWith("/download/"));
const a = adapterWith(def("nyaa"), nyaaSpider, t.fetch);
const page = await a.search("葬送的芙莉莲");
expect(m.requests[0]!.url.searchParams.get("q")).toBe("葬送的芙莉莲");
expect(page.items[0]).toMatchObject({
siteId: "nyaa",
siteReleaseId: "2160092",
detailUrl: "https://nyaa.si/view/2160092",
seeders: 33,
leechers: 3,
snatched: 833,
infoHash: "ef3e7ad1b12bdd9fc341691d8866cd1fa8374a4b",
uploadedAt: "2026-09-12T15:45:00.000Z",
});
expect(page.items[0]!.sizeBytes).toBe(Math.round(6.6 * 1024 ** 3));
const got = await a.downloadTarget("2160092");
expect(got.kind).toBe("torrent");
expect(t.hits[0]!.url.pathname).toBe("/download/2160092.torrent");
});
it("蜜柑:搜索 RSS;资源 ID = info hash,北京时间转 UTC;下载取剧集页里的 .torrent", async () => {
const m = createMockFetch({
"GET /RSS/Search": { body: MIKAN_RSS_SEARCH, headers: XML },
"GET /Home/Episode/f8651444a2c0b3ff7e3da3cbd75da58ef5f09e19": {
body: '<a href="/Download/20260828/f8651444a2c0b3ff7e3da3cbd75da58ef5f09e19.torrent">下载</a>',
},
});
const t = withTorrent(m.fetch, (u) => u.pathname.startsWith("/Download/"));
const a = adapterWith(def("mikan"), mikanSpider, t.fetch);
const page = await a.search("葬送的芙莉莲");
expect(page.items[0]).toMatchObject({
siteReleaseId: "f8651444a2c0b3ff7e3da3cbd75da58ef5f09e19",
infoHash: "f8651444a2c0b3ff7e3da3cbd75da58ef5f09e19",
sizeBytes: 52076478464,
uploadedAt: "2026-08-27T21:32:00.000Z",
});
expect((await a.downloadTarget("f8651444a2c0b3ff7e3da3cbd75da58ef5f09e19")).kind).toBe("torrent");
expect(t.hits[0]!.url.pathname).toBe("/Download/20260828/f8651444a2c0b3ff7e3da3cbd75da58ef5f09e19.torrent");
});
it("AniBT:最新 RSS 带字幕组 / 分辨率标签与 Bangumi ID;搜索走 Torznab", async () => {
const m = createMockFetch({
"GET /rss/magnets.xml": { body: ANIBT_RSS_LATEST, headers: XML },
"GET /torznab/api": { body: ANIBT_TORZNAB_SEARCH, headers: XML },
});
const a = adapterWith(def("anibt"), anibtSpider, m.fetch);
const latest = await a.list(1);
expect(latest.items[0]).toMatchObject({
siteReleaseId: "rel_TC7FrmcvDwIw",
tags: expect.arrayContaining(["Nix-Raws", "1080p"]),
});
expect(latest.items[0]!.raw.bangumi_id).toBe("637339");
const found = await a.search("葬送");
expect(m.requests.at(-1)!.url.searchParams.get("t")).toBe("search");
expect(found.items[0]).toMatchObject({
siteReleaseId: "rel_U6lR5dHzDTZS",
sizeBytes: 52023960599,
infoHash: "f8651444a2c0b3ff7e3da3cbd75da58ef5f09e19",
});
});
it("AnimeGarden:JSON 搜索参数是数组;资源 ID = 来源-来源 ID,只给磁力", async () => {
const m = createMockFetch({ "GET https://api.animes.garden/resources": { body: ANIMEGARDEN_RESOURCES } });
const a = adapterWith(def("animegarden"), animeGardenSpider, m.fetch);
const page = await a.search("葬送");
expect(JSON.parse(m.requests[0]!.url.searchParams.get("search")!)).toEqual(["葬送"]);
expect(page.items[0]).toMatchObject({ siteReleaseId: "dmhy-725809", tags: ["dmhy", "合集"] });
const got = await a.downloadTarget("dmhy-725809");
expect(got).toMatchObject({ kind: "magnet" });
});
it("萌番组:最新 / 磁力;资源 ID 是 24 位 ObjectId", async () => {
const m = createMockFetch({ "GET /api/torrent/latest": { body: BANGUMIMOE_LATEST } });
const a = adapterWith(def("bangumimoe"), bangumiMoeSpider, m.fetch);
const page = await a.list(1);
expect(page.items[0]).toMatchObject({ siteReleaseId: "66f1c2d3e4f5a6b7c8d9e0f1", seeders: 12, snatched: 40 });
expect(page.items[0]!.sizeBytes).toBe(Math.round(345.6 * 1024 ** 2));
});
it("五个内置公开站都是 public 定义", () => {
for (const id of ["nyaa", "mikan", "anibt", "animegarden", "bangumimoe"]) expect(def(id).public).toBe(true);
});
});