Files
danmu_api/forward/forward-widget.test.js
huangxd- afc8b8119f chore: 更新 README 项目结构说明并调整 forward 测试用例参数以同步文档与测试数据
在 README 项目结构中补充 .pr_agent.toml、pr_agent.yml 和 icons.js 的说明;将 forward 测试的 sourceOrder 从 hongguo 改为 douban,并更新测试标题为“早春晴朗”,使文档记录与新增配置/资源一致,测试样例保持最新。
2026-10-03 07:12:39 +08:00

356 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import zlib from 'zlib';
// 模拟 iOS JavaScriptBridge 的 Widget 对象 - 模拟 iOS 环境
global.Widget = {
http: {
get: async (url, options) => {
console.log(`[iOS模拟] HTTP GET: ${url}`);
try {
const response = await fetch(url, {
method: 'GET',
headers: {
...options.headers,
// 'User-Agent': 'ForwardWidgets/1.0.0'
}
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
let data;
if (options.base64Data) {
console.log("base64模式");
// 先拿二进制
const arrayBuffer = await response.arrayBuffer();
const uint8Array = new Uint8Array(arrayBuffer);
// 转换为 Base64
let binary = '';
const chunkSize = 0x8000; // 分块防止大文件卡死
for (let i = 0; i < uint8Array.length; i += chunkSize) {
let chunk = uint8Array.subarray(i, i + chunkSize);
binary += String.fromCharCode.apply(null, chunk);
}
data = btoa(binary); // 得到 base64 字符串
} else if (options.zlibMode) {
console.log("zlib模式")
data = await response.arrayBuffer();
// 使用 zlib 解压数据
const buffer = Buffer.from(data); // 将二进制数据转成 Buffer(Node.js 中使用)
let decompressedData;
try {
decompressedData = zlib.inflateSync(buffer); // 使用同步的 inflate 解压数据
} catch (e) {
console.error("[iOS模拟] 解压缩失败", e);
throw e;
}
// 将解压的数据转回字符串
const decodedData = decompressedData.toString('utf-8');
data = decodedData; // 更新解压后的数据
} else {
data = await response.text();
}
let parsedData;
try {
parsedData = JSON.parse(data); // 尝试将文本解析为 JSON
} catch (e) {
parsedData = data; // 如果解析失败,保留原始文本
}
if (verbose) {
console.log(`[iOS模拟] API响应:`, JSON.stringify(parsedData, null, 2));
}
// 获取所有 headers,但特别处理 set-cookie
const headers = {};
let setCookieValues = [];
// 遍历 headers 条目
for (const [key, value] of response.headers.entries()) {
if (key.toLowerCase() === 'set-cookie') {
setCookieValues.push(value);
} else {
headers[key] = value;
}
}
// 如果存在 set-cookie 头,将其合并为分号分隔的字符串
if (setCookieValues.length > 0) {
headers['set-cookie'] = setCookieValues.join(';');
}
// 模拟 iOS 环境:返回 { data: ... } 结构
return {
data: parsedData,
status: response.status,
headers: headers
};
} catch (error) {
console.error(`[iOS模拟] 请求失败:`, error.message);
throw error;
}
},
post: async (url, body, options = {}) => {
console.log(`[iOS模拟] HTTP POST: ${url}`);
// 处理请求头、body 和其他参数
const { headers = {}, params, allow_redirects = true } = options;
const fetchOptions = {
method: 'POST',
headers: {
...headers,
// 'Content-Type': 'application/json', // 默认使用 JSON 格式
// 'User-Agent': 'ForwardWidgets/1.0.0'
},
body: body
};
if (!allow_redirects) {
fetchOptions.redirect = 'manual'; // 禁止重定向
}
try {
const response = await fetch(url, fetchOptions);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.text();
let parsedData;
try {
parsedData = JSON.parse(data); // 尝试将文本解析为 JSON
} catch (e) {
parsedData = data; // 如果解析失败,保留原始文本
}
if (verbose) {
console.log(`[iOS模拟] API响应:`, JSON.stringify(parsedData, null, 2));
}
// 模拟 iOS 环境:返回 { data: ... } 结构
return {
data: parsedData,
status: response.status,
headers: Object.fromEntries(response.headers.entries())
};
} catch (error) {
console.error(`[iOS模拟] 请求失败:`, error.message);
throw error;
}
}
},
// 新增 storage 模拟
storage: {
_store: {}, // 内部存储对象
get: (key) => {
// console.log(`[iOS模拟] storage.get: ${key}`);
return Widget.storage._store[key] ?? null;
},
set: (key, value) => {
// console.log(`[iOS模拟] storage.set: ${key} = ${JSON.stringify(value)}`);
Widget.storage._store[key] = value;
},
remove: (key) => {
// console.log(`[iOS模拟] storage.remove: ${key}`);
delete Widget.storage._store[key];
},
clear: () => {
console.log(`[iOS模拟] storage.clear`);
Widget.storage._store = {};
},
},
};
// 模拟 WidgetMetadata
global.WidgetMetadata = {
id: "forward.danmu",
title: "弹幕",
version: "1.0.0",
description: "获取弹幕数据"
};
// 配置变量
const verbose = false; // 设置为 true 时打印详细结果
// 加载 forward-widget.js 模块
async function runTest() {
try {
const module = await import('./forward-widget.js');
// const module = await import('../dist/logvar-danmu.js');
// 将模块导出的函数添加到全局作用域,以便测试函数可以访问它们
global.searchDanmu = module.searchDanmu;
global.getDetailById = module.getDetailById;
global.getCommentsById = module.getCommentsById;
global.getDanmuWithSegmentTime = module.getDanmuWithSegmentTime;
// 运行测试
await testNewFlow();
} catch (error) {
console.error('Failed to load module:', error);
process.exitCode = 1;
}
}
async function testNewFlow() {
console.log('=== 测试新的弹幕获取链路 ===\n');
console.log(`[配置] 详细输出: ${verbose ? '开启' : '关闭'}\n`);
try {
const commonParams = {
type: 'tv',
tmdbId: '242762',
season: 1,
episode: 1,
airDate: '2025-07-18',
episodeName: '第2期上:首次大约会!温柔医生为爱冲锋',
sourceOrder: 'douban',
otherServer: 'https://api.danmu.icu',
customSourceApiUrl: '',
vodServers: '金蝉@https://zy.jinchancaiji.com,789@https://www.caiji.cyou,听风@https://gctf.tfdh.top',
vodReturnMode: 'fastest',
vodRequestTimeout: 10000,
bilibiliCookie: '',
doubanCookie: '',
platformOrder: [],
episodeTitleFilter: '',
enableAnimeEpisodeFilter: 'false',
strictTitleMatch: 'false',
animeTitleFilter: '',
animeTitleSimplified: 'false',
blockedWords: '',
groupMinute: 1,
danmuLimit: 0,
danmuSimplifiedTraditional: 'false',
danmuOffset: '',
convertTopBottomToScroll: 'false',
convertColor: 'default',
colorPool: '16777215,16744319,16752762,16774799,9498256,8388564,8900346,14204888,16758465',
likeSwitch: 'true',
proxyUrl: '',
tmdbApiKey: '',
hongguoMergeAllEpisodes: 'false',
};
// 测试自动获取分片弹幕
console.log('🔍 测试自动获取分片弹幕');
const searchRes = await searchDanmu({
// title: "https://m.v.qq.com/x/m/play?cid=53q0eh78q97e4d1&vid=x00174aq5no&ptag=hippySearch&pageType=long",
// title: "https://v.qq.com/x/cover/53q0eh78q97e4d1/x00174aq5no.html",
// title: "https://v.qq.com/x/cover/mzc002009y0nzq8/f4101bay23t.html",
// title: "https://m.iqiyi.com/v_1ftv9n1m3bg.html",
// title: "https://www.iqiyi.com/v_1ftv9n1m3bg.html",
// title: "https://m.youku.com/alipay_video/id_cbff0b0703e54d659628.html?spm=a2hww.12518357.drawer4.2",
// title: "https://v.youku.com/v_show/id_XNjQ3ODMyNjU3Mg==.html",
// title: "https://m.mgtv.com/b/771610/23300622.html?fpa=0&fpos=0",
// title: "https://www.mgtv.com/b/771610/23300622.html",
// title: "https://m.bilibili.com/bangumi/play/ep1231564",
// title: "https://www.bilibili.com/bangumi/play/ep1231564",
// title: "https://www.bilibili.com/video/av170001?p=2",
// title: "https://www.bilibili.com/video/BV17x411w7KC?p=3",
title: '早春晴朗',
...commonParams,
});
if (verbose) {
console.log('✅ 搜索结果:', JSON.stringify(searchRes, null, 2));
} else {
console.log(`✅ 搜索结果: 找到 ${searchRes.animes ? searchRes.animes.length : 0} 个动漫`);
}
if (!searchRes.animes || searchRes.animes.length === 0) {
throw new Error('搜索结果为空,未进入弹幕获取链路');
}
if (searchRes.animes && searchRes.animes.length > 0) {
const anime = searchRes.animes[0];
console.log(`📺 找到动漫: ${anime.animeTitle} (ID: ${anime.animeId})`);
// 获取弹幕评论
console.log('💬 获取弹幕评论...');
const bangumi = await getDetailById({
animeId: anime.animeId,
...commonParams,
});
const targetEpisode = Number(commonParams.episode);
const selectedEpisode = bangumi.find((item) => {
const explicitNumber = Number(item.episodeNumber);
if (Number.isFinite(explicitNumber) && explicitNumber === targetEpisode) return true;
const titleMatch = String(item.episodeTitle || '').match(/第\s*(\d+)\s*[集期]/);
return titleMatch && Number(titleMatch[1]) === targetEpisode;
}) || bangumi[targetEpisode - 1];
if (!selectedEpisode) {
throw new Error(`未找到第 ${targetEpisode} 集,可用剧集数: ${bangumi.length}`);
}
console.log(`🎯 选择剧集: ${selectedEpisode.episodeTitle} (ID: ${selectedEpisode.episodeId})`);
const commentId = selectedEpisode.episodeId;
const segmentList = await getCommentsById({
commentId: commentId,
...commonParams,
});
if (verbose) {
console.log('✅ 弹幕分片:', JSON.stringify(segmentList, null, 2));
} else {
console.log(`✅ 弹幕分片: 获取到 ${segmentList ? segmentList.length : 0} 个分片`);
}
if (!Array.isArray(segmentList) || segmentList.length === 0) {
throw new Error(`第 ${targetEpisode} 集没有可用弹幕分片`);
}
if (segmentList.length > 0) {
const preferredSegmentTime = 450;
const selectedSegment = segmentList.find((item) => (
preferredSegmentTime >= Number(item.segment_start)
&& preferredSegmentTime < Number(item.segment_end)
)) || segmentList[0];
const segmentStart = Number(selectedSegment.segment_start);
const segmentEnd = Number(selectedSegment.segment_end);
const segmentTime = preferredSegmentTime >= segmentStart && preferredSegmentTime < segmentEnd
? preferredSegmentTime
: segmentStart + Math.max(0, segmentEnd - segmentStart) / 2;
console.log(`🎯 选择弹幕分片: ${segmentStart}-${segmentEnd}s,请求时间点 ${segmentTime}s`);
const comments = await getDanmuWithSegmentTime({
segmentTime,
...commonParams,
});
if (verbose) {
console.log('✅ 弹幕评论:', JSON.stringify(comments, null, 2));
} else {
console.log(`✅ 弹幕评论: 获取到 ${comments && comments.comments ? comments.comments.length : 0} 条弹幕`);
}
if (!comments || !Array.isArray(comments.comments) || comments.comments.length === 0) {
throw new Error(`分片 ${segmentStart}-${segmentEnd}s 未获取到弹幕`);
}
}
}
console.log('\n' + '='.repeat(50) + '\n');
console.log('\n=== 所有测试完成 ===');
} catch (error) {
console.error('❌ 测试失败:', error);
process.exitCode = 1;
}
}
// 启动测试
runTest();