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]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-02 21:19:19 +08:00
1 parent 9cdf492291
commit b8876542b8
16 files changed
+770 -18

No files matched your search

@@ -26,13 +26,15 @@ import { MOVIECLAW_DEFINITION_JSON } from "./manifest.generated.ts";
const OBSERVED = "2026-09-27T00:00:00.000Z";
const PRIMARY = ["audiences", "chdbits", "hhanclub", "mteam"];
/** 手写的内置公开 BT 站(public.ts),都默认启用 */
const PUBLIC = ["anibt", "animegarden", "bangumimoe", "mikan", "nyaa"];
describe("站点定义:加载时 zod 校验", () => {
const ids = Object.keys(MOVIECLAW_DEFINITION_JSON).sort();
it("MovieClaw v0.27.0 全部 23 站转换结果均在注册表中", () => {
it("MovieClaw v0.27.0 全部 23 站转换结果 + 公开 BT 站均在注册表中", () => {
expect(ids).toHaveLength(23);
expect(listSiteDefinitions().map((d) => d.id)).toEqual(ids);
expect(listSiteDefinitions().map((d) => d.id)).toEqual([...ids, ...PUBLIC].sort());
});
it.each(ids)("%s:schema 校验通过,provenance 指向 MovieClaw v0.27.0 MIT", (id) => {
@@ -44,8 +46,8 @@ describe("站点定义:加载时 zod 校验", () => {
expect(getSiteDefinition(id)).toBe(BUILTIN_SITE_DEFINITIONS[id]);
});
it("enabledOnly 只列 4 个主力站", () => {
expect(listSiteDefinitions({ enabledOnly: true }).map((d) => d.id)).toEqual(PRIMARY);
it("enabledOnly 只列 4 个主力 PT 站与公开 BT 站", () => {
expect(listSiteDefinitions({ enabledOnly: true }).map((d) => d.id)).toEqual([...PRIMARY, ...PUBLIC].sort());
});
it("非法定义被拒绝并指出字段路径", () => {
@@ -8,6 +8,7 @@ import type { MteamDefinition } from "../mteam/definition.ts";
import type { IndexerDefinition } from "../spider/schema.ts";
import type { SpecializedSiteDefinition } from "../spiders/types.ts";
import { MOVIECLAW_DEFINITION_JSON } from "./manifest.generated.ts";
import { PUBLIC_SITE_DEFINITION_JSON } from "./public.ts";
import { parseIndexerDefinition, parseMteamDefinition, parseSpecializedSiteDefinition } from "./schema.ts";
export type SiteDefinition = IndexerDefinition | MteamDefinition | SpecializedSiteDefinition;
@@ -26,9 +27,14 @@ export function loadIndexerDefinition(json: unknown): IndexerDefinition {
return parseIndexerDefinition(json);
}
/** 随产品发布的全部定义(已校验),键为站点 id */
/** 随产品发布的全部定义(已校验),键为站点 id:MovieClaw 转换的 PT 站 + 手写的公开 BT 站 */
export const BUILTIN_SITE_DEFINITIONS: Readonly<Record<string, SiteDefinition>> = Object.freeze(
Object.fromEntries(Object.entries(MOVIECLAW_DEFINITION_JSON).map(([id, json]) => [id, loadSiteDefinition(json)])),
Object.fromEntries(
Object.entries({ ...MOVIECLAW_DEFINITION_JSON, ...PUBLIC_SITE_DEFINITION_JSON }).map(([id, json]) => [
id,
loadSiteDefinition(json),
]),
),
);
export const audiencesDefinition = BUILTIN_SITE_DEFINITIONS.audiences as IndexerDefinition;
@@ -0,0 +1,88 @@
/**
* 公开 BT 站定义(本项目手写,不来自 MovieClaw):解析器在 ../spiders/public.ts。
* 都不需要登录;能力 = 列表(最新资源,供定时抓取)+ 搜索 + 下载,没有详情页解析。
*/
import { NYAA_CATEGORY_NAMES } from "../spiders/public.ts";
const caps = { list: true, search: true, detail: false, download: true, rss: false };
const polite = { minIntervalMs: 2000, burst: 2 };
export const PUBLIC_SITE_DEFINITION_JSON: Record<string, unknown> = {
nyaa: {
id: "nyaa",
name: "Nyaa",
domain: "https://nyaa.si/",
parser: "Nyaa",
public: true,
umt: {
version: "2026-10-02",
kind: "api",
displayName: "Nyaa",
timezone: "+00:00",
rateLimit: polite,
capabilities: caps,
categoryNames: NYAA_CATEGORY_NAMES,
},
},
mikan: {
id: "mikan",
name: "蜜柑计划",
domain: "https://mikanani.me/",
parser: "Mikan",
public: true,
umt: {
version: "2026-10-02",
kind: "api",
displayName: "蜜柑计划 Mikan",
timezone: "+08:00",
rateLimit: polite,
capabilities: caps,
},
},
anibt: {
id: "anibt",
name: "AniBT",
domain: "https://anibt.net/",
parser: "AniBT",
public: true,
umt: {
version: "2026-10-02",
kind: "api",
displayName: "AniBT",
timezone: "+08:00",
rateLimit: polite,
capabilities: caps,
},
},
animegarden: {
id: "animegarden",
name: "AnimeGarden",
domain: "https://animes.garden/",
parser: "AnimeGarden",
public: true,
api_url: "https://api.animes.garden",
umt: {
version: "2026-10-02",
kind: "api",
displayName: "AnimeGarden 动画花园聚合",
timezone: "+08:00",
rateLimit: polite,
capabilities: caps,
},
},
bangumimoe: {
id: "bangumimoe",
name: "萌番组",
domain: "https://bangumi.moe/",
parser: "BangumiMoe",
public: true,
umt: {
version: "2026-10-02",
kind: "api",
displayName: "萌番组 bangumi.moe",
timezone: "+08:00",
rateLimit: polite,
capabilities: caps,
},
},
};
@@ -234,7 +234,20 @@ export const MteamDefinitionSchema = z.strictObject({
/** MP 专用 spider(`SPIDER_PARSER_CLASSES` 中除 mTorrent 外的 7 个)站点定义 */
export const SpecializedSiteDefinitionSchema = z.strictObject({
...BaseDefinitionShape,
parser: z.enum(["Haidan", "HDDolby", "RousiPro", "SunnyPT", "TNodeSpider", "TorrentLeech", "Yema"]),
parser: z.enum([
"Haidan",
"HDDolby",
"RousiPro",
"SunnyPT",
"TNodeSpider",
"TorrentLeech",
"Yema",
"Nyaa",
"Mikan",
"AniBT",
"AnimeGarden",
"BangumiMoe",
]),
public: z.boolean().optional(),
category: CategoryConfigSchema.optional(),
api_url: z.string().url().optional(),