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: '下载', }, }); 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); }); });