refactor: 引入源注册表,新增源只需加文件+注册一行 (#467)

* 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 分发链全部消除。

* 修改小版本号
This commit is contained in:
huangxd- authored and GitHub committed 2026-08-30 11:15:42 +08:00
1 parent f77b6b9328
commit 04a8a0cffa
5 files changed
+271 -226

No files matched your search

+54 -191
View File
@@ -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));
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ export const Globals = {
accessedEnvVars: {},
// 静态常量
VERSION: '1.20.9',
VERSION: '1.20.10',
MAX_LOGS: 1000, // 日志存储,最多保存 1000 行
MAX_RECORDS: 100, // 请求记录最大数量
+194
View File
@@ -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();
+3 -1
View File
@@ -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',
+19 -33
View File
@@ -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' });