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:
1 parent
6b3a4289fd
commit
58d6292948
3 files changed
+134
-40
No files matched your search
@@ -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(用于访问需要登录的内容)
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user