feat(identity): 豆瓣 / bgm.tv 辅助信息源与发现页

- adapters/meta:bgm.tv 官方 API(条目 / 搜索 / 每日放送)与豆瓣移动版接口(条目 / 实时热门)
- 识别升级链:豆瓣条目缓存没有时在线查;AniBT 的 bgm ID、公开 BT 站标题搜 bgm → 名称 + 年份匹配 TMDB(季条目去季名与年份)
- 条目缓存 30 天,查不到 6 小时负缓存;测试进程缺省不出外网
- 发现页:bgm 每日放送、豆瓣热门电影 / 剧集,点条目匹配 TMDB 作品

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-02 21:30:16 +08:00
1 parent b8876542b8
commit 709414778e
23 files changed
+958 -31

No files matched your search

@@ -65,6 +65,7 @@ describe.skipIf(!pg)("ID-3 识别升级链:详情页 ID", () => {
let audiences: string;
let hhan: string;
const siteRequests: string[] = [];
const metaRequests: string[] = [];
beforeAll(async () => {
tmdb = await createMockTmdb({
@@ -78,6 +79,8 @@ describe.skipIf(!pg)("ID-3 识别升级链:详情页 ID", () => {
fx("tv", 290001, "Sample Series", "示例剧集", "2026-08-01", "tt45950001"),
// 豆瓣路径:只凭豆瓣条目(标题 + 年份)匹配
fx("tv", 990021, "Lanterns of Qing", "青灯记", "2025-04-01"),
// bgm 路径:罗马音标题搜不到,bgm 条目中文名能搜到
fx("tv", 990031, "Frieren: Beyond Journey's End", "葬送的芙莉莲", "2023-09-29"),
],
});
h = await createHarness("id3", {
@@ -94,6 +97,25 @@ describe.skipIf(!pg)("ID-3 识别升级链:详情页 ID", () => {
if (u.host === "hhanclub.net" && u.pathname === "/viewfilelist.php") return html(HHANCLUB_FILELIST_FRAGMENT);
throw new Error(`测试不允许的站点请求 ${u.href}`);
},
// bgm.tv:第二季条目(按站点给的 ID)与搜索(公开 BT 站兜底)
metaFetch: async (input, init) => {
const u = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
metaRequests.push(`${init?.method ?? "GET"} ${u.host}${u.pathname}`);
const json = (b: unknown) =>
new Response(JSON.stringify(b), { headers: { "content-type": "application/json" } });
if (u.host === "api.bgm.tv" && u.pathname === "/v0/subjects/515759")
return json({
id: 515759,
name: "葬送のフリーレン 第2期",
name_cn: "葬送的芙莉莲 第二季",
date: "2026-01-16",
});
if (u.host === "api.bgm.tv" && u.pathname === "/v0/search/subjects")
return json({
data: [{ id: 400602, name: "葬送のフリーレン", name_cn: "葬送的芙莉莲", date: "2023-09-29" }],
});
return new Response("", { status: 404 });
},
});
audiences = (await seedSite(h, { key: "audiences", baseUrl: "https://audiences.me/", cookie: "c_secure_uid=1" }))
.site.id;
@@ -304,7 +326,7 @@ describe.skipIf(!pg)("ID-3 识别升级链:详情页 ID", () => {
escalation: { via: "douban", douban: { id: "36000099", title: "青灯记", source: "web_snippet" } },
});
// 缓存里没有这个豆瓣条目:不请求豆瓣,只进池;有 ID 的资源不开「没识别出作品」
// 缓存里没有这个豆瓣条目、在线也查不到(404):只进池;有 ID 的资源不开「没识别出作品」
await sharedCache(h.ctx).delete(DOUBAN_SUBJECT_REGION, "36000098");
const r2 = await release(hhan, "91006", "Unknown Drama S01E01 2025 1080p WEB-DL", {
_detail: { doubanId: "36000098" },
@@ -316,6 +338,47 @@ describe.skipIf(!pg)("ID-3 识别升级链:详情页 ID", () => {
expect(siteRequests.some((x) => x.includes("douban"))).toBe(false);
});
it("bgm 路径:AniBT 给的 bgm ID → 第二季条目去掉季名与年份 → TMDB;公开 BT 站没 ID 时按标题搜 bgm", async () => {
const pub = await h.db
.insertInto("site")
.values({
key: "anibt",
name: "AniBT",
kind: "public_bt",
adapter: "api",
base_url: "https://anibt.net/",
} as never)
.returning("id")
.executeTakeFirstOrThrow();
const r = await release(pub.id, "rel_A1", "[Nix-Raws] Sousou no Frieren S2 - 05 [1080p]", {
_site: { bangumiId: "515759" },
});
const res = await resolveRelease(h.rt, { sourceReleaseId: r.id, trigger: "manual" });
expect(res.outcome).toBe("auto");
expect(await tmdbIdOf(res.mediaEntityId!)).toBe("990031");
expect((await row(r.id)).resolution_evidence).toMatchObject({
escalation: { via: "bangumi", bangumi: { id: "515759", search: false } },
});
// 条目进缓存:再识别同 ID 不再请求
const n = metaRequests.length;
const r2 = await release(pub.id, "rel_A2", "[Nix-Raws] Sousou no Frieren S2 - 06 [1080p]", {
_site: { bangumiId: "515759" },
});
await resolveRelease(h.rt, { sourceReleaseId: r2.id, trigger: "manual" });
expect(metaRequests.length).toBe(n);
const r3 = await release(pub.id, "rel_A3", "[SubsPlease] Sousou no Frieren - 03 (1080p)");
const res3 = await resolveRelease(h.rt, { sourceReleaseId: r3.id, trigger: "manual" });
expect(res3.outcome).toBe("auto");
expect(await tmdbIdOf(res3.mediaEntityId!)).toBe("990031");
expect(metaRequests).toContain("POST api.bgm.tv/v0/search/subjects");
// PT 站的资源不按标题搜 bgm
const m = metaRequests.length;
const r4 = await release(hhan, "91099", "Sousou no Frieren S01E03 1080p WEB-DL");
await resolveRelease(h.rt, { sourceReleaseId: r4.id, trigger: "manual" });
expect(metaRequests.length).toBe(m);
});
it("升级链没认出(豆瓣召回无匹配)时保留原有歧义:候选不丢,照常开有歧义的事项", async () => {
await saveDoubanSubject(h.ctx, {
id: "36000097",