feat(dandan): 弹弹302关联链接仅分发给已在 SOURCE_ORDER 开启的源 (#489)
关联链接的对应平台只要已接入采集源就会实时拉取,与 SOURCE_ORDER 表达的可选源范围无关。现要求该源已在 SOURCE_ORDER 中开启才分发,未开启的源与已独立选择的合并源一样跳过并记录日志。README、config/.env.example 与 envs.js 中 DANDANPLAY_ACCOUNT 的说明同步为「并把同一请求下发的弹弹关联链接分发给对应平台源实时拉取(需开启对应源)」。 worker.test.js 新增用例:仅开启 bilibili 时只拉取 bilibili、仅开启 tencent 时分发目标随之切换、两个关联源均未开启时不拉取、已开启但被独立选择的合并源覆盖时同样跳过。
This commit is contained in:
1 parent
e8851c5bc5
commit
d91079ebc1
5 files changed
+94
-12
No files matched your search
@@ -513,7 +513,7 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数
|
||||
| AI_API_KEY | 【可选】AI服务的API密钥,用于身份验证,默认为空,需手动填写 |
|
||||
| AI_MATCH_PROMPT | 【可选】AI匹配提示词,用于自定义AI匹配行为,不填提供默认提示词,提示词如下 |
|
||||
| USE_BANGUMI_DATA | 【可选】[Bangumi Data](https://github.com/bangumi-data/bangumi-data) 加速匹配开关,默认值:`false`(关闭),开启后将动画元数据缓存至本地或内存中给源调用,提升动画源的检索与匹配速度并解锁隐藏/区域番剧(本地和Docker部署使用时请先挂载.cache目录获得最佳体验,云部署使用时会将数据缓存至临时内存中如果体验不佳请关闭) |
|
||||
| DANDANPLAY_ACCOUNT | 【可选】弹弹play账号(dandan 源获取弹幕使用)。与密码同时填写后自动开启:[NipaPlay](https://github.com/AimesSoft/NipaPlay-Reload) 中转弹弹play服务端获取弹幕,并把同一请求下发的弹弹302关联链接分发给已接入的对应平台源实时拉取,最终弹幕为两者合并去重后的结果。注意:关联链接指向的平台视频若已下架将无法通过自有链路补取;关联含巴哈姆特平台时需确保能够连通巴哈 |
|
||||
| DANDANPLAY_ACCOUNT | 【可选】弹弹play账号(dandan 源获取弹幕使用)。与密码同时填写后自动开启:[NipaPlay](https://github.com/AimesSoft/NipaPlay-Reload) 中转弹弹play服务端获取弹幕,并把同一请求下发的弹弹关联链接分发给对应平台源实时拉取(需开启对应源),最终弹幕为两者合并去重后的结果。注意:关联链接指向的平台视频若已下架将无法通过自有链路补取;关联含巴哈姆特平台时需确保能够连通巴哈 |
|
||||
| DANDANPLAY_PASSWORD | 【可选】弹弹play密码(dandan 源获取弹幕使用),与账号同时填写后自动开启,UI 配置页提供连通性测试 |
|
||||
|
||||
```regex
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ YOUKU_CONCURRENCY=8
|
||||
# 弹弹play账号(dandan 源获取弹幕使用)
|
||||
# 说明:
|
||||
# - 与密码同时填写后自动开启,无需额外开关
|
||||
# - 开启后 dandan 源改由 NipaPlay 中转弹弹play服务端获取弹幕,并把同一请求下发的弹弹302关联链接分发给已接入的对应平台源实时拉取,最终弹幕为两者合并去重后的结果
|
||||
# - 开启后 dandan 源改由 NipaPlay 中转弹弹play服务端获取弹幕,并把同一请求下发的弹弹关联链接分发给对应平台源实时拉取(需开启对应源),最终弹幕为两者合并去重后的结果
|
||||
# - 注意:关联链接指向的平台视频若已下架将无法通过自有链路补取;关联含巴哈姆特平台时需确保能够连通巴哈
|
||||
# DANDANPLAY_ACCOUNT=
|
||||
|
||||
|
||||
@@ -712,7 +712,7 @@ export class Envs {
|
||||
'BILIBILI_COOKIE': { category: 'source', type: 'text', description: 'B站Cookie' },
|
||||
'DOUBAN_COOKIE': { category: 'source', type: 'text', description: '豆瓣Cookie' },
|
||||
'YOUKU_CONCURRENCY': { category: 'source', type: 'number', description: '优酷并发配置,默认8', min: 1, max: 16 },
|
||||
'DANDANPLAY_ACCOUNT': { category: 'source', type: 'text', description: '弹弹play账号(dandan 源获取弹幕使用)。\n与密码同时填写后自动开启,无需额外开关。\n开启后 dandan 源改由 NipaPlay 中转弹弹play服务端获取弹幕,并把同一请求下发的弹弹302关联链接分发给已接入的对应平台源实时拉取:\n最终弹幕为 NipaPlay 中转弹弹play服务端弹幕与自有链路弹幕合并去重后的结果。\n注意:关联链接指向的平台视频若已下架将无法通过自有链路补取;关联含巴哈姆特平台时需确保能够连通巴哈' },
|
||||
'DANDANPLAY_ACCOUNT': { category: 'source', type: 'text', description: '弹弹play账号(dandan 源获取弹幕使用)。\n与密码同时填写后自动开启,无需额外开关。\n开启后 dandan 源改由 NipaPlay 中转弹弹play服务端获取弹幕,并把同一请求下发的弹弹关联链接分发给对应平台源实时拉取(需开启对应源):\n最终弹幕为 NipaPlay 中转弹弹play服务端弹幕与自有链路弹幕合并去重后的结果。\n注意:关联链接指向的平台视频若已下架将无法通过自有链路补取;关联含巴哈姆特平台时需确保能够连通巴哈' },
|
||||
'DANDANPLAY_PASSWORD': { category: 'source', type: 'text', description: '弹弹play密码(dandan 源获取弹幕使用)。\n点击编辑界面的测试连通性按钮可验证账号与 NipaPlay 中转弹弹play服务端是否可用' },
|
||||
|
||||
// 匹配配置
|
||||
|
||||
@@ -546,7 +546,8 @@ export default class DandanSource extends BaseSource {
|
||||
|
||||
try {
|
||||
// 配置弹弹play账号后经 NipaPlay 中转弹弹play服务端取弹幕,并把同一请求下发的弹弹302关联链接分发给
|
||||
// 对应平台源实时拉取,两部分由去重阶段按来源合并;未配置账号或 NipaPlay 中转弹弹play服务端不可用时回退弹弹原生弹幕。
|
||||
// 已在 SOURCE_ORDER 开启的对应平台源实时拉取,两部分由去重阶段按来源合并;
|
||||
// 未配置账号或 NipaPlay 中转弹弹play服务端不可用时回退弹弹原生弹幕。
|
||||
const nipaplay = await fetchNipaplayDanmaku(id);
|
||||
if (!nipaplay) return await fetchDandanComments(id);
|
||||
|
||||
@@ -618,13 +619,15 @@ const SOURCE_TO_PLATFORM = {
|
||||
};
|
||||
|
||||
// 汇总跨平台实时弹幕,复用核心路由同款链接解析(gamer→sn)与各源既有 formatComments,
|
||||
// 使每源入参与核心路由一致;按平台标识跳过已独立选择的合并源以免重复;每条弹幕标记实时拉取
|
||||
// 来源,供 convertToDanmakuJson 组装 [来源&平台] 标签,并让去重阶段按真实来源统计重复弹幕;
|
||||
// 使每源入参与核心路由一致;仅分发已在 SOURCE_ORDER 开启且未被已独立选择的合并源覆盖的平台;
|
||||
// 每条弹幕标记实时拉取来源,供 convertToDanmakuJson 组装 [来源&平台] 标签,并让去重阶段按真实来源统计重复弹幕;
|
||||
// 同平台多个链接串行、间隔 1 秒请求,与手动解析链接防风控一致。
|
||||
async function getRelatedDanmuViaNipaplay(links, coveredSources) {
|
||||
if (!links) return [];
|
||||
// 关联链接的源需已在 SOURCE_ORDER 中开启,与搜索可选源保持一致
|
||||
const enabledSources = new Set(globals.sourceOrderArr);
|
||||
const summary = Object.entries(links)
|
||||
.filter(([, arr]) => arr && arr.length)
|
||||
.filter(([p, arr]) => arr && arr.length && enabledSources.has(p))
|
||||
.map(([p, arr]) => `${SOURCE_TO_PLATFORM[p] || p}×${arr.length}`)
|
||||
.join(', ');
|
||||
if (summary) log("info", `[dandan] 弹弹302关联链接分发目标: ${summary}`);
|
||||
@@ -636,14 +639,20 @@ async function getRelatedDanmuViaNipaplay(links, coveredSources) {
|
||||
tencent: tencentSource,
|
||||
imgo: mangoSource,
|
||||
};
|
||||
// 收集待拉取任务(按平台标识分组以便同平台串行),已独立选择的合并源跳过。
|
||||
// 收集待拉取任务(按平台标识分组以便同平台串行),未在 SOURCE_ORDER 开启的源与已独立选择的合并源跳过。
|
||||
const pending = [];
|
||||
const skipped = [];
|
||||
const skippedDisabled = [];
|
||||
const skippedCovered = [];
|
||||
for (const [platform, linksOfPlatform] of Object.entries(links)) {
|
||||
if (!linksOfPlatform || linksOfPlatform.length === 0) continue;
|
||||
const platformLabel = SOURCE_TO_PLATFORM[platform];
|
||||
if (!platformLabel || coveredSources.has(platform) || coveredSources.has(platformLabel)) {
|
||||
if (platformLabel) skipped.push(platformLabel);
|
||||
if (!platformLabel) continue;
|
||||
if (!enabledSources.has(platform)) {
|
||||
skippedDisabled.push(platformLabel);
|
||||
continue;
|
||||
}
|
||||
if (coveredSources.has(platform) || coveredSources.has(platformLabel)) {
|
||||
skippedCovered.push(platformLabel);
|
||||
continue;
|
||||
}
|
||||
const sourceInstance = sourceMap[platform];
|
||||
@@ -662,7 +671,8 @@ async function getRelatedDanmuViaNipaplay(links, coveredSources) {
|
||||
});
|
||||
}
|
||||
}
|
||||
if (skipped.length) log("info", `[dandan] 弹弹302关联分发跳过已合并源(避免重复拉取): ${skipped.join(', ')}`);
|
||||
if (skippedCovered.length) log("info", `[dandan] 弹弹302关联分发跳过已合并源(避免重复拉取): ${skippedCovered.join(', ')}`);
|
||||
if (skippedDisabled.length) log("info", `[dandan] 弹弹302关联分发跳过未在 SOURCE_ORDER 开启的源: ${skippedDisabled.join(', ')}`);
|
||||
// 同平台串行、间隔 1 秒,不同平台并行(防风控)。
|
||||
const groups = new Map();
|
||||
for (const task of pending) {
|
||||
|
||||
@@ -3406,6 +3406,78 @@ test('dandan formatComments 按实时拉取标记区分处理', () => {
|
||||
assert.strictEqual(dandan.formatComments([native])[0].p, '12.34,1,25,a16777215,0', '原生弹幕执行颜色转换');
|
||||
});
|
||||
|
||||
test('dandan 关联链接分发仅限已在 SOURCE_ORDER 开启的源', async () => {
|
||||
const location = 'https://x.test/redirect?urls=https://www.bilibili.com/video/BV1xx|https://v.qq.com/x/cover/abc.html&shift=0,0';
|
||||
const originalOrder = Globals.envs.sourceOrderArr;
|
||||
const originalAccount = Globals.envs.dandanplayAccount;
|
||||
const originalPassword = Globals.envs.dandanplayPassword;
|
||||
const originalBilibiliGet = BilibiliSource.prototype.getEpisodeDanmu;
|
||||
const originalBilibiliFormat = BilibiliSource.prototype.formatComments;
|
||||
const originalTencentGet = TencentSource.prototype.getEpisodeDanmu;
|
||||
const originalTencentFormat = TencentSource.prototype.formatComments;
|
||||
const pulled = [];
|
||||
|
||||
// 网关登录应答、评论接口回传 302 关联链接、原生弹幕地址应答
|
||||
const gatewayFetch = async (url) => {
|
||||
const target = String(url);
|
||||
if (target.endsWith('/api/v2/login')) {
|
||||
return mockJsonResponse({ success: true, token: 'mock-token', tokenExpireTime: '2099-01-01T00:00:00Z' });
|
||||
}
|
||||
if (target.includes('/api/v2/comment/')) {
|
||||
return { ok: false, status: 302, url: target, headers: new Headers({ location }), text: async () => '' };
|
||||
}
|
||||
return mockJsonResponse({ comments: [] });
|
||||
};
|
||||
|
||||
try {
|
||||
Globals.envs.dandanplayAccount = 'account@example.com';
|
||||
Globals.envs.dandanplayPassword = 'password';
|
||||
BilibiliSource.prototype.getEpisodeDanmu = async () => {
|
||||
pulled.push('bilibili');
|
||||
return [{ cid: 1, p: '1.00,1,25,16777215,0', t: 1, m: '来自B站' }];
|
||||
};
|
||||
BilibiliSource.prototype.formatComments = (list) => list;
|
||||
TencentSource.prototype.getEpisodeDanmu = async () => { pulled.push('tencent'); return []; };
|
||||
TencentSource.prototype.formatComments = (list) => list;
|
||||
|
||||
await withMockFetch(gatewayFetch, async () => {
|
||||
// 仅开启 bilibili:只拉取 bilibili,跳过未开启的 tencent
|
||||
Globals.envs.sourceOrderArr = ['bilibili'];
|
||||
const onlyBilibili = await new DandanSource().getEpisodeDanmu('ep-1');
|
||||
assert.deepStrictEqual(pulled, ['bilibili'], '仅分发已开启的源');
|
||||
assert.strictEqual(onlyBilibili.length, 1, '分发结果来自已开启的源');
|
||||
assert.strictEqual(onlyBilibili[0].realTimeSource, 'bilibili1', '标记实时拉取来源');
|
||||
|
||||
// 仅开启 tencent:分发目标随之切换
|
||||
pulled.length = 0;
|
||||
Globals.envs.sourceOrderArr = ['tencent'];
|
||||
await new DandanSource().getEpisodeDanmu('ep-2');
|
||||
assert.deepStrictEqual(pulled, ['tencent'], '开启源变化后分发目标随之切换');
|
||||
|
||||
// 两个关联源都未开启:全部跳过
|
||||
pulled.length = 0;
|
||||
Globals.envs.sourceOrderArr = ['douban'];
|
||||
const none = await new DandanSource().getEpisodeDanmu('ep-3');
|
||||
assert.deepStrictEqual(pulled, [], '未开启关联源时不拉取');
|
||||
assert.deepStrictEqual(none, [], '未开启关联源时无关联弹幕');
|
||||
|
||||
// 源已开启但已被独立选择的合并源覆盖:同样跳过,避免重复拉取
|
||||
pulled.length = 0;
|
||||
Globals.envs.sourceOrderArr = ['bilibili', 'tencent'];
|
||||
await new DandanSource().getEpisodeDanmu('ep-4', ['bilibili:123']);
|
||||
assert.deepStrictEqual(pulled, ['tencent'], '已开启但被合并源覆盖的平台仍跳过');
|
||||
});
|
||||
} finally {
|
||||
Globals.envs.sourceOrderArr = originalOrder;
|
||||
Globals.envs.dandanplayAccount = originalAccount;
|
||||
Globals.envs.dandanplayPassword = originalPassword;
|
||||
BilibiliSource.prototype.getEpisodeDanmu = originalBilibiliGet;
|
||||
BilibiliSource.prototype.formatComments = originalBilibiliFormat;
|
||||
TencentSource.prototype.getEpisodeDanmu = originalTencentGet;
|
||||
TencentSource.prototype.formatComments = originalTencentFormat;
|
||||
}
|
||||
});
|
||||
|
||||
test('fongmi-api season aware scoring', () => {
|
||||
// 季号提取: SxxExx / 第x季 / Season N / 2x05; 综艺日期与纯集数不误判
|
||||
assert.equal(extractFongmiSeasonNumber('人生切割术 S02E05'), 2);
|
||||
|
||||
Reference in new issue
Block a user