diff --git a/.env.example b/.env.example index 7067163..ea4962d 100644 --- a/.env.example +++ b/.env.example @@ -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(用于访问需要登录的内容) diff --git a/README.md b/README.md index 030292f..97ed01e 100644 --- a/README.md +++ b/README.md @@ -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` | diff --git a/danmu_api/worker.js b/danmu_api/worker.js index 2b2b379..9a9b7d4 100644 --- a/danmu_api/worker.js +++ b/danmu_api/worker.js @@ -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);