From 04a8a0cffaecc82e47cc197461f229b22d662ae1 Mon Sep 17 00:00:00 2001 From: huangxd- <649924757@qq.com> Date: Sun, 30 Aug 2026 11:15:42 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=BC=95=E5=85=A5=E6=BA=90?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E8=A1=A8=EF=BC=8C=E6=96=B0=E5=A2=9E=E6=BA=90?= =?UTF-8?q?=E5=8F=AA=E9=9C=80=E5=8A=A0=E6=96=87=E4=BB=B6+=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E4=B8=80=E8=A1=8C=20(#467)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 引入源注册表,新增源只需加文件+注册一行 将散落在 dandan-api.js / worker.test.js 的 import + new + if/else 分发三段重复代码集中到 sources/registry.js 统一管理。 问题:每新增一个弹幕源需改 5 处(import、new、search 分发、handleAnimes 分发、getComments 分发),维护 3 段重复的 if/else 链(各 22 个分支),违反开闭原则。 方案:新建 sources/registry.js 注册表,集中管理 23 个源的元数据与实例化: - 拓扑排序按依赖顺序实例化(douban 依赖 5 源、tmdb 依赖 douban 自动注入) - handleAdapter 适配器统一各源 handleAnimes 签名差异(vod 遍历多服务器、custom 少参) - extraSearchArgs 标记处理 vod 源 search 多参数特例 - logName 字段统一日志标签别名(360->360kan, imgo->mango) 效果:新增源只需在 registry.js 加一行配置,所有分发链路自动查表,零改动。 dandan-api.js 净减 149 行(-225/+76),23 个源的 if/else 分发链全部消除。 * 修改小版本号 --- danmu_api/apis/dandan-api.js | 245 ++++++++-------------------------- danmu_api/configs/globals.js | 2 +- danmu_api/sources/registry.js | 194 +++++++++++++++++++++++++++ danmu_api/utils/http-util.js | 4 +- danmu_api/worker.test.js | 52 +++----- 5 files changed, 271 insertions(+), 226 deletions(-) create mode 100644 danmu_api/sources/registry.js diff --git a/danmu_api/apis/dandan-api.js b/danmu_api/apis/dandan-api.js index 698aa45..5464796 100644 --- a/danmu_api/apis/dandan-api.js +++ b/danmu_api/apis/dandan-api.js @@ -1,5 +1,5 @@ import { globals } from '../configs/globals.js'; -import { getPageTitle, jsonResponse, httpGet, sourceLogContext, toLogSourceName, runWithHttpCache, httpCacheContext } from '../utils/http-util.js'; +import { getPageTitle, jsonResponse, httpGet, sourceLogContext, runWithHttpCache, httpCacheContext } from '../utils/http-util.js'; import { log } from '../utils/log-util.js' import { simplized } from '../utils/zh-util.js'; import { setRedisKey, updateRedisCaches } from "../utils/redis-util.js"; @@ -21,58 +21,41 @@ import { getTMDBChineseTitle, getTmdbSeasonBoundaries } from "../utils/tmdb-util import { applyMergeLogic, mergeDanmakuList, MERGE_DELIMITER, alignSourceTimelines, sanitizeUrl } from "../utils/merge-util.js"; import { getHanjutvSourceLabel } from "../utils/hanjutv-util.js"; import AIClient from '../utils/ai-util.js'; -import Kan360Source from "../sources/kan360.js"; -import VodSource from "../sources/vod.js"; -import TmdbSource from "../sources/tmdb.js"; -import DoubanSource from "../sources/douban.js"; -import RenrenSource from "../sources/renren.js"; -import HanjutvSource from "../sources/hanjutv.js"; -import BahamutSource from "../sources/bahamut.js"; -import DandanSource from "../sources/dandan.js"; -import CustomSource from "../sources/custom.js"; -import TencentSource from "../sources/tencent.js"; -import IqiyiSource from "../sources/iqiyi.js"; -import MangoSource from "../sources/mango.js"; -import BilibiliSource from "../sources/bilibili.js"; -import MiguSource from "../sources/migu.js"; -import YoukuSource from "../sources/youku.js"; -import SohuSource from "../sources/sohu.js"; -import LeshiSource from "../sources/leshi.js"; -import XiguaSource from "../sources/xigua.js"; -import MaiduiduiSource from "../sources/maiduidui.js"; -import AiyifanSource from "../sources/aiyifan.js"; -import HongguoSource, { isHongguoPlayerUrl } from "../sources/hongguo.js"; -import AnimekoSource from "../sources/animeko.js"; -import OtherSource from "../sources/other.js"; +import { getSourceByKey, getSourceMetaByKey, getLogNameByKey } from "../sources/registry.js"; +import { isHongguoPlayerUrl } from "../sources/hongguo.js"; +import BilibiliSource from "../sources/bilibili.js"; // resolveB23Link 为 BilibiliSource 实例方法,单测中仍需直接 new import { Anime, AnimeMatch, Episodes, Bangumi } from "../models/dandan-model.js"; // ===================== // 兼容弹弹play接口 // ===================== -const kan360Source = new Kan360Source(); -const vodSource = new VodSource(); -const renrenSource = new RenrenSource(); -const hanjutvSource = new HanjutvSource(); -const bahamutSource = new BahamutSource(); -const dandanSource = new DandanSource(); -const customSource = new CustomSource(); -const tencentSource = new TencentSource(); -const youkuSource = new YoukuSource(); -const iqiyiSource = new IqiyiSource(); -const mangoSource = new MangoSource(); -const bilibiliSource = new BilibiliSource(); -const miguSource = new MiguSource(); -const sohuSource = new SohuSource(); -const leshiSource = new LeshiSource(); -const xiguaSource = new XiguaSource(); -const maiduiduiSource = new MaiduiduiSource(); -const aiyifanSource = new AiyifanSource(); -const hongguoSource = new HongguoSource(); -const animekoSource = new AnimekoSource(); -const otherSource = new OtherSource(); -const doubanSource = new DoubanSource(tencentSource, iqiyiSource, youkuSource, bilibiliSource, miguSource); -const tmdbSource = new TmdbSource(doubanSource); +// 所有弹幕源实例统一由 sources/registry.js 注册表管理并按依赖顺序实例化, +// 新增源只需在 registry.js 加一条配置,无需在此处 import / new / 维护 if/else 分发链。 +// 下列局部变量保留与原变量名一致,供文件内既有的源实例引用直接使用(一次性从注册表取用)。 +const kan360Source = getSourceByKey('360'); +const vodSource = getSourceByKey('vod'); +const renrenSource = getSourceByKey('renren'); +const hanjutvSource = getSourceByKey('hanjutv'); +const bahamutSource = getSourceByKey('bahamut'); +const dandanSource = getSourceByKey('dandan'); +const customSource = getSourceByKey('custom'); +const tencentSource = getSourceByKey('tencent'); +const youkuSource = getSourceByKey('youku'); +const iqiyiSource = getSourceByKey('iqiyi'); +const mangoSource = getSourceByKey('imgo'); +const bilibiliSource = getSourceByKey('bilibili'); +const miguSource = getSourceByKey('migu'); +const sohuSource = getSourceByKey('sohu'); +const leshiSource = getSourceByKey('leshi'); +const xiguaSource = getSourceByKey('xigua'); +const maiduiduiSource = getSourceByKey('maiduidui'); +const aiyifanSource = getSourceByKey('aiyifan'); +const hongguoSource = getSourceByKey('hongguo'); +const animekoSource = getSourceByKey('animeko'); +const otherSource = getSourceByKey('other'); +const doubanSource = getSourceByKey('douban'); +const tmdbSource = getSourceByKey('tmdb'); // 用于聚合请求的去重Map const PENDING_DANMAKU_REQUESTS = new Map(); @@ -388,14 +371,6 @@ function checkEpisodeSatisfied(animesList, querySeason, queryEpisode, requestAni * @param {string|null} preferSource 优选源 */ async function executeSourceHandlers(resultData, queryTitle, targetAnimesList, requestAnimeDetailsMap, targetSeason, preferAnimeId = null, preferSource = null) { - const { - vod: animesVodResults, 360: animes360, tmdb: animesTmdb, douban: animesDouban, renren: animesRenren, - hanjutv: animesHanjutv, bahamut: animesBahamut, dandan: animesDandan, custom: animesCustom, - tencent: animesTencent, youku: animesYouku, iqiyi: animesIqiyi, imgo: animesImgo, bilibili: animesBilibili, - migu: animesMigu, sohu: animesSohu, leshi: animesLeshi, xigua: animesXigua, maiduidui: animesMaiduidui, - aiyifan: animesAiyifan, hongguo: animesHongguo, animeko: animesAnimeko - } = resultData; - // 仅处理resultData中存在数据的源,避免将undefined传入handleAnimes const activeSourceKeys = globals.sourceOrderArr.filter(key => resultData[key] !== undefined); const sourceTasks = []; @@ -403,83 +378,15 @@ async function executeSourceHandlers(resultData, queryTitle, targetAnimesList, r for (const key of activeSourceKeys) { const isolatedAnimes = []; const isolatedDetailStore = new Map(); - - if (key === '360') { - // 处理360来源 - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: sourceLogContext.run(toLogSourceName(key), () => kan360Source.handleAnimes(animes360, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)) }); - } else if (key === 'vod') { - // 处理Vod来源(遍历所有VOD服务器的结果,依次在同一隔离容器中处理) - if (animesVodResults && Array.isArray(animesVodResults)) { - const vodPromise = sourceLogContext.run(key, () => (async () => { - for (const vodResult of animesVodResults) { - if (vodResult && vodResult.list && vodResult.list.length > 0) { - await vodSource.handleAnimes(vodResult.list, queryTitle, isolatedAnimes, vodResult.serverName, isolatedDetailStore, targetSeason); - } - } - })()); - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: vodPromise }); - } - } else if (key === 'tmdb') { - // 处理TMDB来源 - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: sourceLogContext.run(key, () => tmdbSource.handleAnimes(animesTmdb, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)) }); - } else if (key === 'douban') { - // 处理Douban来源 - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: sourceLogContext.run(key, () => doubanSource.handleAnimes(animesDouban, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)) }); - } else if (key === 'renren') { - // 处理Renren来源 - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: sourceLogContext.run(key, () => renrenSource.handleAnimes(animesRenren, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)) }); - } else if (key === 'hanjutv') { - // 处理Hanjutv来源 - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: sourceLogContext.run(key, () => hanjutvSource.handleAnimes(animesHanjutv, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)) }); - } else if (key === 'bahamut') { - // 处理Bahamut来源 - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: sourceLogContext.run(key, () => bahamutSource.handleAnimes(animesBahamut, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)) }); - } else if (key === 'dandan') { - // 处理弹弹play来源 - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: sourceLogContext.run(key, () => dandanSource.handleAnimes(animesDandan, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)) }); - } else if (key === 'custom') { - // 处理自定义弹幕源来源(handleAnimes签名不含detailStore和querySeason) - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: sourceLogContext.run(key, () => customSource.handleAnimes(animesCustom, queryTitle, isolatedAnimes)) }); - } else if (key === 'tencent') { - // 处理Tencent来源 - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: sourceLogContext.run(key, () => tencentSource.handleAnimes(animesTencent, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)) }); - } else if (key === 'youku') { - // 处理Youku来源 - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: sourceLogContext.run(key, () => youkuSource.handleAnimes(animesYouku, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)) }); - } else if (key === 'iqiyi') { - // 处理iQiyi来源 - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: sourceLogContext.run(key, () => iqiyiSource.handleAnimes(animesIqiyi, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)) }); - } else if (key === 'imgo') { - // 处理Mango来源 - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: sourceLogContext.run(toLogSourceName(key), () => mangoSource.handleAnimes(animesImgo, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)) }); - } else if (key === 'bilibili') { - // 处理Bilibili来源 - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: sourceLogContext.run(key, () => bilibiliSource.handleAnimes(animesBilibili, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)) }); - } else if (key === 'migu') { - // 处理Migu来源 - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: sourceLogContext.run(key, () => miguSource.handleAnimes(animesMigu, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)) }); - } else if (key === 'sohu') { - // 处理Sohu来源 - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: sourceLogContext.run(key, () => sohuSource.handleAnimes(animesSohu, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)) }); - } else if (key === 'leshi') { - // 处理Leshi来源 - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: sourceLogContext.run(key, () => leshiSource.handleAnimes(animesLeshi, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)) }); - } else if (key === 'xigua') { - // 处理Xigua来源 - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: sourceLogContext.run(key, () => xiguaSource.handleAnimes(animesXigua, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)) }); - } else if (key === 'maiduidui') { - // 处理Maiduidui来源 - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: sourceLogContext.run(key, () => maiduiduiSource.handleAnimes(animesMaiduidui, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)) }); - } else if (key === 'aiyifan') { - // 处理Aiyifan来源 - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: sourceLogContext.run(key, () => aiyifanSource.handleAnimes(animesAiyifan, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)) }); - } else if (key === 'hongguo') { - // 处理红果短剧来源 - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: sourceLogContext.run(key, () => hongguoSource.handleAnimes(animesHongguo, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)) }); - } else if (key === 'animeko') { - // 处理Animeko来源 - sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise: sourceLogContext.run(key, () => animekoSource.handleAnimes(animesAnimeko, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)) }); + const meta = getSourceMetaByKey(key); + if (!meta) { + log("warn", `[system] [searchAnime] 未注册的源键 "${key}",已跳过 handleAnimes`); + continue; } + const searchResult = resultData[key]; + // 统一通过注册表适配器调用 handleAnimes,消除各源签名差异(vod 遍历、custom 少参等) + const promise = sourceLogContext.run(meta.logName, () => meta.handleAdapter(meta.instance, searchResult, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)); + sourceTasks.push({ key, animes: isolatedAnimes, detailStore: isolatedDetailStore, promise }); } // 并发执行所有源的handleAnimes @@ -648,7 +555,7 @@ async function searchAnimeBody(url, preferAnimeId = null, preferSource = null, d } else if (source === 'bahamut') { titles.push(`【bahamut】 BahaSn${singleUrl.match(/sn=(\d+)/)?.[1] || '?'}`); } else { - const pt = await sourceLogContext.run(toLogSourceName(source), () => getPageTitle(stripLinkOffset(singleUrl).cleanUrl)); + const pt = await sourceLogContext.run(getLogNameByKey(source), () => getPageTitle(stripLinkOffset(singleUrl).cleanUrl)); titles.push(`【${source}】 ${pt}`); } } @@ -749,7 +656,7 @@ async function searchAnimeBody(url, preferAnimeId = null, preferSource = null, d pageTitle = '红果短剧'; } else { // 将源标识符统一映射到日志标签规范名称;取标题前剥离 @偏移 后缀,避免带偏移的链接请求失败 - pageTitle = await sourceLogContext.run(toLogSourceName(platform), () => getPageTitle(stripLinkOffset(queryTitle).cleanUrl)); + pageTitle = await sourceLogContext.run(getLogNameByKey(platform), () => getPageTitle(stripLinkOffset(queryTitle).cleanUrl)); } const links = [{ @@ -791,31 +698,16 @@ async function searchAnimeBody(url, preferAnimeId = null, preferSource = null, d // 存储各源搜索结果的容器,供S2+季度扩展逻辑读取 const resultData = {}; - // 源Key到对应搜索Promise的映射 + // 源Key到对应搜索Promise的映射:统一从注册表查实例与日志标签,新增源无需改此处分发 const sourceSearchMap = {}; for (const source of globals.sourceOrderArr) { - if (source === "360") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => kan360Source.search(queryTitle)); - else if (source === "vod") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => vodSource.search(queryTitle, preferAnimeId, preferSource)); - else if (source === "tmdb") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => tmdbSource.search(queryTitle)); - else if (source === "douban") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => doubanSource.search(queryTitle)); - else if (source === "renren") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => renrenSource.search(queryTitle)); - else if (source === "hanjutv") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => hanjutvSource.search(queryTitle)); - else if (source === "bahamut") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => bahamutSource.search(queryTitle)); - else if (source === "dandan") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => dandanSource.search(queryTitle)); - else if (source === "custom") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => customSource.search(queryTitle)); - else if (source === "tencent") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => tencentSource.search(queryTitle)); - else if (source === "youku") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => youkuSource.search(queryTitle)); - else if (source === "iqiyi") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => iqiyiSource.search(queryTitle)); - else if (source === "imgo") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => mangoSource.search(queryTitle)); - else if (source === "bilibili") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => bilibiliSource.search(queryTitle)); - else if (source === "migu") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => miguSource.search(queryTitle)); - else if (source === "sohu") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => sohuSource.search(queryTitle)); - else if (source === "leshi") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => leshiSource.search(queryTitle)); - else if (source === "xigua") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => xiguaSource.search(queryTitle)); - else if (source === "maiduidui") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => maiduiduiSource.search(queryTitle)); - else if (source === "aiyifan") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => aiyifanSource.search(queryTitle)); - else if (source === "hongguo") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => hongguoSource.search(queryTitle)); - else if (source === "animeko") sourceSearchMap[source] = sourceLogContext.run(toLogSourceName(source), () => animekoSource.search(queryTitle)); + const meta = getSourceMetaByKey(source); + if (!meta) { + log("warn", `[system] [LogVar-API] 未注册的源键 "${source}",已跳过`); + continue; + } + const args = meta.extraSearchArgs ? [queryTitle, preferAnimeId, preferSource] : [queryTitle]; + sourceSearchMap[source] = sourceLogContext.run(meta.logName, () => meta.instance.search(...args)); } // 构建逐源管道:每个源 search 完成后,通过 executeSourceHandlers 处理 handleAnimes @@ -2433,27 +2325,8 @@ async function fetchMergedComments(url, animeTitle, commentId) { } // 定义请求任务 - const fetchTask = sourceLogContext.run(toLogSourceName(sourceName), async () => { - let sourceInstance = null; - - if (sourceName === 'renren') sourceInstance = renrenSource; - else if (sourceName === 'hanjutv') sourceInstance = hanjutvSource; - else if (sourceName === 'bahamut') sourceInstance = bahamutSource; - else if (sourceName === 'dandan') sourceInstance = dandanSource; - else if (sourceName === 'tencent') sourceInstance = tencentSource; - else if (sourceName === 'youku') sourceInstance = youkuSource; - else if (sourceName === 'iqiyi') sourceInstance = iqiyiSource; - else if (sourceName === 'imgo') sourceInstance = mangoSource; - else if (sourceName === 'bilibili') sourceInstance = bilibiliSource; - else if (sourceName === 'migu') sourceInstance = miguSource; - else if (sourceName === 'sohu') sourceInstance = sohuSource; - else if (sourceName === 'leshi') sourceInstance = leshiSource; - else if (sourceName === 'xigua') sourceInstance = xiguaSource; - else if (sourceName === 'maiduidui') sourceInstance = maiduiduiSource; - else if (sourceName === 'aiyifan') sourceInstance = aiyifanSource; - else if (sourceName === 'hongguo') sourceInstance = hongguoSource; - else if (sourceName === 'animeko') sourceInstance = animekoSource; - // 如有新增允许的源合并,在此处添加 + const fetchTask = sourceLogContext.run(getLogNameByKey(sourceName), async () => { + const sourceInstance = getSourceByKey(sourceName); if (sourceInstance) { try { @@ -2666,20 +2539,10 @@ export async function getComment(path, queryFormat, segmentFlag, clientIp, inclu if (!urlPattern.test(url) && !isHongguoUrl) { // 剥离单源 source:id 前缀(如 bahamut:50709 → 50709),使各源拿到真实 ID,与合并路径分割逻辑一致 const sourceUrl = sanitizeUrl(commentUrl); - if (plat === "renren") { - danmus = await sourceLogContext.run('renren', () => renrenSource.getComments(sourceUrl, plat, segmentFlag)); - } else if (plat === "hanjutv") { - danmus = await sourceLogContext.run('hanjutv', () => hanjutvSource.getComments(sourceUrl, plat, segmentFlag)); - } else if (plat === "bahamut") { - danmus = await sourceLogContext.run('bahamut', () => bahamutSource.getComments(sourceUrl, plat, segmentFlag)); - } else if (plat === "dandan") { - danmus = await sourceLogContext.run('dandan', () => dandanSource.getComments(sourceUrl, plat, segmentFlag)); - } else if (plat === "custom") { - danmus = await sourceLogContext.run('custom', () => customSource.getComments(sourceUrl, plat, segmentFlag)); - } else if (plat === "animeko") { - danmus = await sourceLogContext.run('animeko', () => animekoSource.getComments(sourceUrl, plat, segmentFlag)); - } else if (plat === "hongguo") { - danmus = await sourceLogContext.run('hongguo', () => hongguoSource.getComments(sourceUrl, plat, segmentFlag)); + // plat 值即源调度键名,直接从注册表查实例;未注册则跳过 + const platSource = getSourceByKey(plat); + if (platSource) { + danmus = await sourceLogContext.run(plat, () => platSource.getComments(sourceUrl, plat, segmentFlag)); } } diff --git a/danmu_api/configs/globals.js b/danmu_api/configs/globals.js index 0f43b94..154cbf6 100644 --- a/danmu_api/configs/globals.js +++ b/danmu_api/configs/globals.js @@ -13,7 +13,7 @@ export const Globals = { accessedEnvVars: {}, // 静态常量 - VERSION: '1.20.9', + VERSION: '1.20.10', MAX_LOGS: 1000, // 日志存储,最多保存 1000 行 MAX_RECORDS: 100, // 请求记录最大数量 diff --git a/danmu_api/sources/registry.js b/danmu_api/sources/registry.js new file mode 100644 index 0000000..4d056eb --- /dev/null +++ b/danmu_api/sources/registry.js @@ -0,0 +1,194 @@ +// ===================== +// 源注册表 (Source Registry) +// ===================== +// 集中管理所有弹幕源的元数据与实例化,消除散落在 dandan-api.js / worker.test.js 中的 +// import + new + if/else 分发三段重复代码。 +// +// 新增一个源只需: +// 1. 在 sources/ 下新建 xxx.js 并 export default class XxxSource extends BaseSource +// 2. 在下方 SOURCE_REGISTRY 数组里加一条 { key, factory, ... } 配置 +// +// 所有"按 sourceKey 查实例"的分发点改为调用 getSourceByKey(key) 即可, +// 无需再改动 dandan-api.js 的 import 区、实例化区或 if/else 分发链。 +// ===================== + +import { globals } from '../configs/globals.js'; + +import Kan360Source from './kan360.js'; +import VodSource from './vod.js'; +import TmdbSource from './tmdb.js'; +import DoubanSource from './douban.js'; +import RenrenSource from './renren.js'; +import HanjutvSource from './hanjutv.js'; +import BahamutSource from './bahamut.js'; +import DandanSource from './dandan.js'; +import CustomSource from './custom.js'; +import TencentSource from './tencent.js'; +import IqiyiSource from './iqiyi.js'; +import MangoSource from './mango.js'; +import BilibiliSource from './bilibili.js'; +import MiguSource from './migu.js'; +import YoukuSource from './youku.js'; +import SohuSource from './sohu.js'; +import LeshiSource from './leshi.js'; +import XiguaSource from './xigua.js'; +import MaiduiduiSource from './maiduidui.js'; +import AiyifanSource from './aiyifan.js'; +import HongguoSource from './hongguo.js'; +import AnimekoSource from './animeko.js'; +import OtherSource from './other.js'; + +// 源注册表:每条记录描述一个源的调度身份与实例化方式。 +// 字段说明: +// key —— sourceOrderArr 中的调度键名(如 "360"、"imgo"、"tencent") +// logName —— 日志标签规范名称;为空时默认等于 key。处理 360→360kan、imgo→mango 这类别名 +// factory —— 实例工厂 (ctx) => instance。ctx 是已建好实例的 Map(key->instance),供有依赖的源取依赖 +// extraSearchArgs —— search 调用是否需要额外参数 (preferAnimeId, preferSource);仅 vod 为 true +// deps —— 该源依赖的其他源 key 列表(用于排序),无依赖为空 +// handleAdapter —— 把各源签名不一致的 handleAnimes 统一为 +// (searchResult, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason) 的适配器; +// 未提供时默认调用 instance.handleAnimes(searchResult, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason) +// 新增源若 handleAnimes 签名标准(5 参),可不提供 handleAdapter +const SOURCE_REGISTRY = [ + { key: '360', logName: '360kan', factory: () => new Kan360Source(), deps: [] }, + { key: 'vod', logName: '', factory: () => new VodSource(), deps: [], extraSearchArgs: true, + handleAdapter: async (instance, searchResult, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason) => { + // vod 源: search 返回多服务器结果数组,需逐个遍历并传入 serverName + if (searchResult && Array.isArray(searchResult)) { + for (const vodResult of searchResult) { + if (vodResult && vodResult.list && vodResult.list.length > 0) { + await instance.handleAnimes(vodResult.list, queryTitle, isolatedAnimes, vodResult.serverName, isolatedDetailStore, targetSeason); + } + } + } + } }, + { key: 'tmdb', logName: '', factory: (ctx) => new TmdbSource(ctx.get('douban')), deps: ['douban'] }, + { key: 'douban', logName: '', factory: (ctx) => new DoubanSource(ctx.get('tencent'), ctx.get('iqiyi'), ctx.get('youku'), ctx.get('bilibili'), ctx.get('migu')), deps: ['tencent', 'iqiyi', 'youku', 'bilibili', 'migu'] }, + { key: 'renren', logName: '', factory: () => new RenrenSource(), deps: [] }, + { key: 'hanjutv', logName: '', factory: () => new HanjutvSource(), deps: [] }, + { key: 'bahamut', logName: '', factory: () => new BahamutSource(), deps: [] }, + { key: 'dandan', logName: '', factory: () => new DandanSource(), deps: [] }, + { key: 'custom', logName: '', factory: () => new CustomSource(), deps: [], + handleAdapter: (instance, searchResult, queryTitle, isolatedAnimes) => instance.handleAnimes(searchResult, queryTitle, isolatedAnimes) }, + { key: 'tencent', logName: '', factory: () => new TencentSource(), deps: [] }, + { key: 'iqiyi', logName: '', factory: () => new IqiyiSource(), deps: [] }, + { key: 'imgo', logName: 'mango', factory: () => new MangoSource(), deps: [] }, + { key: 'bilibili', logName: '', factory: () => new BilibiliSource(), deps: [] }, + { key: 'migu', logName: '', factory: () => new MiguSource(), deps: [] }, + { key: 'youku', logName: '', factory: () => new YoukuSource(), deps: [] }, + { key: 'sohu', logName: '', factory: () => new SohuSource(), deps: [] }, + { key: 'leshi', logName: '', factory: () => new LeshiSource(), deps: [] }, + { key: 'xigua', logName: '', factory: () => new XiguaSource(), deps: [] }, + { key: 'maiduidui', logName: '', factory: () => new MaiduiduiSource(), deps: [] }, + { key: 'aiyifan', logName: '', factory: () => new AiyifanSource(), deps: [] }, + { key: 'hongguo', logName: '', factory: () => new HongguoSource(), deps: [] }, + { key: 'animeko', logName: '', factory: () => new AnimekoSource(), deps: [] }, + { key: 'other', logName: '', factory: () => new OtherSource(), deps: [] }, +]; + +// ---- 实例缓存(按 key 索引)---- +const instanceMap = new Map(); +const metaMap = new Map(); +let initialized = false; + +/** + * 按依赖拓扑顺序一次性构建所有源实例。 + * 无依赖源先建,有依赖源从已建实例中取依赖;同层按注册顺序。 + * 幂等:重复调用不会重复实例化。 + */ +export function initSources() { + if (initialized) return; + const built = new Set(); + + // 多趟扫描:每趟把"依赖已全部就绪"的源建出来,直到全部建完或无法推进 + let progress = true; + while (built.size < SOURCE_REGISTRY.length && progress) { + progress = false; + for (const entry of SOURCE_REGISTRY) { + if (built.has(entry.key)) continue; + const depsReady = (entry.deps || []).every(d => built.has(d)); + if (!depsReady) continue; + const instance = entry.factory(instanceMap); + const handleAdapter = entry.handleAdapter || ((inst, searchResult, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason) => + inst.handleAnimes(searchResult, queryTitle, isolatedAnimes, isolatedDetailStore, targetSeason)); + instanceMap.set(entry.key, instance); + metaMap.set(entry.key, { + key: entry.key, + logName: entry.logName || entry.key, + instance, + extraSearchArgs: !!entry.extraSearchArgs, + handleAdapter, + }); + built.add(entry.key); + progress = true; + } + } + + if (built.size < SOURCE_REGISTRY.length) { + const unresolved = SOURCE_REGISTRY.filter(e => !built.has(e.key)).map(e => e.key); + throw new Error(`[registry] 源实例化存在未解决的循环依赖: ${unresolved.join(', ')}`); + } + + initialized = true; +} + +/** 确保已初始化(懒初始化,供按需取实例的场景使用)*/ +function ensureInit() { + if (!initialized) initSources(); +} + +/** + * 按 sourceKey 取源实例。未注册返回 null。 + * @param {string} key sourceOrderArr 中的调度键名 + * @returns {BaseSource|null} + */ +export function getSourceByKey(key) { + ensureInit(); + return instanceMap.get(key) || null; +} + +/** + * 按 sourceKey 取源元数据 { key, logName, instance, extraSearchArgs }。未注册返回 null。 + * 用于分发点同时需要实例与日志标签的场景。 + */ +export function getSourceMetaByKey(key) { + ensureInit(); + return metaMap.get(key) || null; +} + +/** + * 按 sourceKey 取日志标签规范名称。未注册返回原值。 + * 兼容 http-util.js 旧 toLogSourceName 的查询需求。 + */ +export function getLogNameByKey(key) { + ensureInit(); + const meta = metaMap.get(key); + return meta ? meta.logName : key; +} + +/** + * 取所有已注册源的元数据列表,按注册顺序。 + * 用于需要遍历全部源的场景。 + */ +export function getAllSourceMetas() { + ensureInit(); + return SOURCE_REGISTRY.map(e => metaMap.get(e.key)); +} + +/** + * 取所有已注册源实例,按注册顺序。 + */ +export function getAllSourceInstances() { + ensureInit(); + return SOURCE_REGISTRY.map(e => instanceMap.get(e.key)); +} + +/** + * 判断某 sourceKey 是否已注册。 + */ +export function isRegisteredSource(key) { + return SOURCE_REGISTRY.some(e => e.key === key); +} + +// 模块加载时立即完成实例化,保证 import 完成后即可直接取用 +initSources(); diff --git a/danmu_api/utils/http-util.js b/danmu_api/utils/http-util.js index 61a7175..7d2100c 100644 --- a/danmu_api/utils/http-util.js +++ b/danmu_api/utils/http-util.js @@ -16,7 +16,9 @@ export function runWithHttpCache(fn) { // 源调度键名(sourceOrderArr)到日志标签规范名称的映射 // sourceOrderArr 中部分键名与对应源文件的标签命名不一致(如 360→360kan, imgo→mango) -// 此映射表统一转换,确保 HTTP 日志标签与源文件内部标签一致 +// 此映射表为本地 fallback,与 sources/registry.js 的 logName 字段保持一致。 +// 注意:http-util.js 是各源依赖的底层工具,不可反向 import registry(会形成 +// http-util → registry → 各源 → http-util 的静态循环依赖),故保留本地表。 const SOURCE_KEY_TO_LOG_NAME = { '360': '360kan', 'imgo': 'mango', diff --git a/danmu_api/worker.test.js b/danmu_api/worker.test.js index 24d25a6..8a9d3d7 100644 --- a/danmu_api/worker.test.js +++ b/danmu_api/worker.test.js @@ -15,23 +15,9 @@ import { getImdbepisodes } from "./utils/imdb-util.js"; import { getTMDBChineseTitle, getTmdbJpDetail, searchTmdbTitles } from "./utils/tmdb-util.js"; import { getDoubanDetail, getDoubanInfoByImdbId, searchDoubanTitles } from "./utils/douban-util.js"; import AIClient from './utils/ai-util.js'; -import RenrenSource from "./sources/renren.js"; -import HanjutvSource from "./sources/hanjutv.js"; -import BahamutSource from "./sources/bahamut.js"; -import TencentSource from "./sources/tencent.js"; -import IqiyiSource from "./sources/iqiyi.js"; -import MangoSource from "./sources/mango.js"; +import { getSourceByKey } from './sources/registry.js'; import BilibiliSource from "./sources/bilibili.js"; -import YoukuSource from "./sources/youku.js"; -import MiguSource from "./sources/migu.js"; -import SohuSource from "./sources/sohu.js"; -import LeshiSource from "./sources/leshi.js"; -import XiguaSource from "./sources/xigua.js"; -import MaiduiduiSource from "./sources/maiduidui.js"; -import AiyifanSource from "./sources/aiyifan.js"; -import HongguoSource, { parseHongguoPlayerUrl } from "./sources/hongguo.js"; -import AnimekoSource from "./sources/animeko.js"; -import OtherSource from "./sources/other.js"; +import { parseHongguoPlayerUrl } from "./sources/hongguo.js"; import { NodeHandler } from "./configs/handlers/node-handler.js"; import { VercelHandler } from "./configs/handlers/vercel-handler.js"; import { NetlifyHandler } from "./configs/handlers/netlify-handler.js"; @@ -165,23 +151,23 @@ const urlPrefix = "http://localhost:9321"; const token = "87654321"; test('worker.js API endpoints', async (t) => { - const renrenSource = new RenrenSource(); - const hanjutvSource = new HanjutvSource(); - const bahamutSource = new BahamutSource(); - const tencentSource = new TencentSource(); - const iqiyiSource = new IqiyiSource(); - const mangoSource = new MangoSource(); - const bilibiliSource = new BilibiliSource(); - const youkuSource = new YoukuSource(); - const miguSource = new MiguSource(); - const sohuSource = new SohuSource(); - const leshiSource = new LeshiSource(); - const xiguaSource = new XiguaSource(); - const maiduiduiSource = new MaiduiduiSource(); - const aiyifanSource = new AiyifanSource(); - const hongguoSource = new HongguoSource(); - const animekoSource = new AnimekoSource(); - const otherSource = new OtherSource(); + const renrenSource = getSourceByKey('renren'); + const hanjutvSource = getSourceByKey('hanjutv'); + const bahamutSource = getSourceByKey('bahamut'); + const tencentSource = getSourceByKey('tencent'); + const iqiyiSource = getSourceByKey('iqiyi'); + const mangoSource = getSourceByKey('imgo'); + const bilibiliSource = getSourceByKey('bilibili'); + const youkuSource = getSourceByKey('youku'); + const miguSource = getSourceByKey('migu'); + const sohuSource = getSourceByKey('sohu'); + const leshiSource = getSourceByKey('leshi'); + const xiguaSource = getSourceByKey('xigua'); + const maiduiduiSource = getSourceByKey('maiduidui'); + const aiyifanSource = getSourceByKey('aiyifan'); + const hongguoSource = getSourceByKey('hongguo'); + const animekoSource = getSourceByKey('animeko'); + const otherSource = getSourceByKey('other'); await t.test('GET / should return welcome message', async () => { const req = new MockRequest(urlPrefix, { method: 'GET' });