feat: 添加B站ss格式番剧链接支持 (#55)

* feat: 添加B站ss格式番剧链接支持

- 新增对 ss 格式番剧链接的识别和处理
- 使用 season_id 参数调用B站API获取番剧信息
- 默认获取第一集的弹幕数据
- 更新错误提示信息,包含 ss 格式说明

* feat: 添加 VOD 服务器请求超时机制

- 使用 AbortController 实现 HTTP 请求超时控制
- 新增 VOD_REQUEST_TIMEOUT 环境变量(默认 5000ms)
- 防止慢速或失效的采集站阻塞整个搜索过程
- 添加详细的超时错误日志"

* fix(tencent): 支持单页剧集分集提取

问题:
- 部分腾讯视频剧集采用单页展示所有集数
- 原逻辑在未找到tabs分页信息时直接返回空数组
- 导致这类剧集无法被缓存和使用

修改:
- 在getTencentEpisodes函数中新增单页处理分支
- 当tabs.length === 0时,尝试从初始API响应中提取分集
- 保持原有多页逻辑不变

影响范围:
- danmu_api/worker.js (getTencentEpisodes函数, 第1814-1896行)

* Update worker.js
This commit is contained in:
woleigedouer authored and GitHub committed 2025-10-18 11:49:22 +08:00
1 parent 6b3a4289fd
commit 58d6292948
3 files changed
+134 -40

No files matched your search

+6
View File
@@ -20,6 +20,12 @@ OTHER_SERVER=https://api.danmu.icu
# 默认值:vod@https://www.caiji.cyou
VOD_SERVERS=vod@https://www.caiji.cyou
# VOD 服务器请求超时时间(毫秒)
# 默认值:5000(5秒)
# 建议值:3000-10000
# 说明:防止慢速或失效的采集站阻塞整个搜索过程(单个请求超时)
VOD_REQUEST_TIMEOUT=5000
# ==================== Bilibili 配置 ====================
# Bilibili Cookie(用于访问需要登录的内容)
+1
View File
@@ -231,6 +231,7 @@ LogVar 弹幕 API 服务器
| TOKEN | 【可选】自定义用户token,不填默认为`87654321` |
| OTHER_SERVER | 【可选】兜底第三方弹幕服务器,不填默认为`https://api.danmu.icu` |
| VOD_SERVERS | 【可选】VOD服务器列表,支持多个服务器并发查询,格式:`名称@URL,名称@URL,...`,示例:`vod@https://www.caiji.cyou,vod2@https://zy.xmm.hk`,不填默认为`vod@https://www.caiji.cyou` |
| VOD_REQUEST_TIMEOUT | 【可选】VOD服务器单个请求超时时间(毫秒),防止慢速或失效的采集站阻塞搜索,默认为`5000`(5秒),建议值:`3000-10000` |
| BILIBILI_COOKIE | 【可选】b站cookie(填入后能抓取完整弹幕),如 `buvid3=E2BCA ... eao6; theme-avatar-tip-show=SHOWED`,请自行通过浏览器或抓包工具抓取,热心网友测试后,实际最少只需取 `SESSDATA=xxxx` 字段 |
| YOUKU_CONCURRENCY | 【可选】youku弹幕请求并发数,用于加快youku弹幕请求速度,不填默认为`8`,最高`16` |
| SOURCE_ORDER | 【可选】源排序,用于按源对返回资源的排序(注意:先后顺序会影响自动匹配最终的返回),默认是`360,vod,renren,hanjutv`,表示360数据排在最前,hanjutv数据排在最后,示例:`360,renren`:只返回360数据和renren数据,且360数据靠前;当前可选择的源字段有 `360,vod,tencent,renren,hanjutv,bahamut` |
+127 -40
View File
@@ -1,6 +1,6 @@
// 全局状态(Cloudflare 和 Vercel 都可能重用实例)
// ⚠️ 不是持久化存储,每次冷启动会丢失
const VERSION = "1.3.4";
const VERSION = "1.3.5";
let animes = [];
let episodeIds = [];
let episodeNum = 10001; // 全局变量,用于自增 ID
@@ -470,14 +470,22 @@ function getPreferAnimeId(title) {
async function httpGet(url, options) {
log("info", `[iOS模拟] HTTP GET: ${url}`);
// 设置超时时间(默认5秒)
const timeout = parseInt(process.env.VOD_REQUEST_TIMEOUT || '5000');
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
method: 'GET',
headers: {
...options.headers,
}
},
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@@ -559,6 +567,17 @@ async function httpGet(url, options) {
};
} catch (error) {
clearTimeout(timeoutId);
// 检查是否是超时错误
if (error.name === 'AbortError') {
log("error", `[iOS模拟] 请求超时:`, error.message);
log("error", '详细诊断:');
log("error", '- URL:', url);
log("error", '- 超时时间:', `${timeout}ms`);
throw new Error(`Request timeout after ${timeout}ms`);
}
log("error", `[iOS模拟] 请求失败:`, error.message);
log("error", '详细诊断:');
log("error", '- URL:', url);
@@ -1792,46 +1811,15 @@ async function getTencentEpisodes(cid) {
}
}
if (tabs.length === 0) {
log("info", "[Tencent] 未找到分页信息");
return [];
}
log("info", `[Tencent] 找到 ${tabs.length} 个分页`);
// 获取所有分页的分集
const allEpisodes = [];
for (const tab of tabs) {
if (!tab.page_context) continue;
const tabPayload = {
has_cache: 1,
page_params: {
req_from: "web_vsite",
page_id: "vsite_episode_list",
page_type: "detail_operation",
id_type: "1",
page_size: "",
cid: cid,
vid: "",
lid: "",
page_num: "",
page_context: tab.page_context,
detail_page_type: "1"
}
};
if (tabs.length === 0) {
log("info", "[Tencent] 未找到分页信息,尝试从初始响应中提取分集");
const tabResponse = await httpPost(episodesUrl, JSON.stringify(tabPayload), { headers });
if (!tabResponse || !tabResponse.data) continue;
const tabData = typeof tabResponse.data === "string" ? JSON.parse(tabResponse.data) : tabResponse.data;
if (tabData.ret !== 0 || !tabData.data) continue;
// 提取分集
if (tabData.data.module_list_datas) {
for (const moduleListData of tabData.data.module_list_datas) {
// 尝试直接从第一次响应中提取分集(单页情况)
if (data.data && data.data.module_list_datas) {
for (const moduleListData of data.data.module_list_datas) {
for (const moduleData of moduleListData.module_datas) {
if (moduleData.item_data_lists && moduleData.item_data_lists.item_datas) {
for (const item of moduleData.item_data_lists.item_datas) {
@@ -1847,6 +1835,64 @@ async function getTencentEpisodes(cid) {
}
}
}
if (allEpisodes.length === 0) {
log("info", "[Tencent] 初始响应中也未找到分集信息");
return [];
}
log("info", `[Tencent] 从初始响应中提取到 ${allEpisodes.length} 集`);
} else {
log("info", `[Tencent] 找到 ${tabs.length} 个分页`);
// 获取所有分页的分集
for (const tab of tabs) {
if (!tab.page_context) continue;
const tabPayload = {
has_cache: 1,
page_params: {
req_from: "web_vsite",
page_id: "vsite_episode_list",
page_type: "detail_operation",
id_type: "1",
page_size: "",
cid: cid,
vid: "",
lid: "",
page_num: "",
page_context: tab.page_context,
detail_page_type: "1"
}
};
const tabResponse = await httpPost(episodesUrl, JSON.stringify(tabPayload), { headers });
if (!tabResponse || !tabResponse.data) continue;
const tabData = typeof tabResponse.data === "string" ? JSON.parse(tabResponse.data) : tabResponse.data;
if (tabData.ret !== 0 || !tabData.data) continue;
// 提取分集
if (tabData.data.module_list_datas) {
for (const moduleListData of tabData.data.module_list_datas) {
for (const moduleData of moduleListData.module_datas) {
if (moduleData.item_data_lists && moduleData.item_data_lists.item_datas) {
for (const item of moduleData.item_data_lists.item_datas) {
if (item.item_params && item.item_params.vid && item.item_params.is_trailer !== "1") {
allEpisodes.push({
vid: item.item_params.vid,
title: item.item_params.title,
unionTitle: item.item_params.union_title || item.item_params.title
});
}
}
}
}
}
}
}
}
log("info", `[Tencent] 共获取 ${allEpisodes.length} 集`);
@@ -2325,7 +2371,7 @@ async function fetchBilibili(inputUrl) {
return [];
}
// 番剧
// 番剧 - ep格式
} else if (inputUrl.includes("bangumi/") && inputUrl.includes("ep")) {
try {
const epid = path.slice(-1)[0].slice(2);
@@ -2364,8 +2410,49 @@ async function fetchBilibili(inputUrl) {
return [];
}
// 番剧 - ss格式
} else if (inputUrl.includes("bangumi/") && inputUrl.includes("ss")) {
try {
const ssid = path.slice(-1)[0].slice(2).split('?')[0]; // 移除可能的查询参数
const ssInfoUrl = `${api_epid_cid}?season_id=${ssid}`;
log("info", `获取番剧信息: season_id=${ssid}`);
const res = await httpGet(ssInfoUrl, {
headers: {
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
},
});
const data = typeof res.data === "string" ? JSON.parse(res.data) : res.data;
if (data.code !== 0) {
log("error", "获取番剧视频信息失败:", data.message);
return [];
}
// 检查是否有episodes数据
if (!data.result.episodes || data.result.episodes.length === 0) {
log("error", "番剧没有可用的集数");
return [];
}
// 默认获取第一集的弹幕
const firstEpisode = data.result.episodes[0];
title = firstEpisode.share_copy;
cid = firstEpisode.cid;
duration = firstEpisode.duration / 1000;
danmakuUrl = `https://comment.bilibili.com/${cid}.xml`;
log("info", `使用第一集: ${title}, cid=${cid}`);
} catch (error) {
log("error", "请求番剧视频信息失败:", error);
return [];
}
} else {
log("error", "不支持的B站视频网址,仅支持普通视频(av,bv)、剧集视频(ep)");
log("error", "不支持的B站视频网址,仅支持普通视频(av,bv)、剧集视频(ep,ss)");
return [];
}
log("info", danmakuUrl, cid, aid, duration);