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

@@ -56,6 +56,11 @@ const T_SITES = ["sites"];
const T_FEEDS = ["feed-sources"];
const T_RELEASES = ["source-releases"];
/** 定义对应的站点类型:手写的公开 BT 站定义带 public */
export function definitionKind(def: SiteDefinition): "pt" | "public_bt" {
return "public" in def && def.public === true ? "public_bt" : "pt";
}
export function adapterOf(def: SiteDefinition): SiteAdapter {
return def.umt.kind === "mteam" ? "mteam_api" : def.umt.kind === "api" ? "api" : "nexusphp";
}
@@ -90,7 +95,7 @@ export function sourcesRoutes(app: ModuleApp, rt: () => SourcesRuntime) {
items: listSiteDefinitions().map((d) => ({
key: d.id,
name: d.umt.kind === "nexusphp" ? (d.umt.displayName ?? d.name) : d.umt.displayName,
kind: "pt" as const,
kind: definitionKind(d),
adapter: adapterOf(d),
baseUrl: d.domain,
shippedEnabled: d.enabled !== false,
@@ -140,8 +145,10 @@ export function sourcesRoutes(app: ModuleApp, rt: () => SourcesRuntime) {
route(app, D.sites.get, async ({ params }) => siteToApi(await getSite(params.id)));
route(app, D.sites.create, async ({ body, actor }) => {
const def = body.key ? listSiteDefinitions().find((d) => d.id === body.key) : undefined;
// 类型缺省:注册表里有定义 = PT 站;否则按旧口径(generic_rss = 自定义 RSS 站点)
const kind = body.kind ?? (def ? "pt" : "rss_feed");
// 类型缺省:注册表里有定义 = 定义的类型(PT / 公开 BT);否则按旧口径(generic_rss = 自定义 RSS 站点)
const kind = body.kind ?? (def ? definitionKind(def) : "rss_feed");
// 内置公开 BT 站(Nyaa、蜜柑等):用定义的专用适配器,默认定时抓最新资源,不默认进资源搜索
const publicDef = kind === "public_bt" && def && definitionKind(def) === "public_bt" ? def : undefined;
if (!SITE_KINDS[kind].implemented) throw new ApiException("bad_request", `还不支持添加${SITE_KINDS[kind].label}`);
if (kind === "pt") {
if (!body.key) throw new ApiException("bad_request", "PT 站要从站点定义里选(缺 key)");
@@ -151,6 +158,10 @@ export function sourcesRoutes(app: ModuleApp, rt: () => SourcesRuntime) {
if (!def && body.adapter !== "generic_rss") {
throw new ApiException("bad_request", `注册表中没有站点 ${body.key};未适配站点只能以 generic_rss 登记`);
}
if (def && definitionKind(def) !== "pt") throw new ApiException("bad_request", `${body.key} 不是 PT 站`);
} else if (publicDef) {
if (body.adapter !== adapterOf(publicDef))
throw new ApiException("bad_request", `站点 ${body.key} 的适配器类型应为 ${adapterOf(publicDef)}`);
} else if (body.adapter !== "generic_rss") {
throw new ApiException("bad_request", `${SITE_KINDS[kind].label}只能以 generic_rss 登记`);
}
@@ -170,11 +181,13 @@ export function sourcesRoutes(app: ModuleApp, rt: () => SourcesRuntime) {
enabled: body.enabled ?? true,
rate_limit: jsonb(body.rateLimit ?? {}),
config: jsonb(body.config ?? {}),
snapshot_enabled: kind === "pt" ? (body.snapshotEnabled ?? false) : false,
snapshot_enabled:
kind === "pt" ? (body.snapshotEnabled ?? false) : publicDef ? (body.snapshotEnabled ?? true) : false,
snapshot_interval_sec: body.snapshotIntervalSec ?? null,
proxy: body.proxy ?? false,
// 不能搜索的站点不进默认搜索集合
search_default: kind === "pt" ? (body.searchDefault ?? true) : false,
search_default:
kind === "pt" ? (body.searchDefault ?? true) : publicDef ? (body.searchDefault ?? false) : false,
})
.returningAll()
.executeTakeFirstOrThrow();