From c9112ee7cfbf859fd2d4dafc7d17c8287b5a7376 Mon Sep 17 00:00:00 2001 From: Elegy233 <82034747+wan0ge@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:36:51 +0800 Subject: [PATCH] =?UTF-8?q?fix(tencent):=20=E6=94=AF=E6=8C=81=E5=8D=95=20c?= =?UTF-8?q?id=20=E7=BB=BC=E8=89=BA=E7=9A=84=E5=A4=9A=E5=AD=A3=E5=88=86?= =?UTF-8?q?=E9=9B=86=E8=8E=B7=E5=8F=96=20(#399)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 综艺单 cid 多季内容收在 videoInfo.episodeSites/playSites 的 chapterInfo 下, tabs 为空时仅取默认季分集,分季内容不完整。 - 按分页 tabs 下发的 page_context 结构重建各季请求参数:chapterInfo 自带的 pageContext 缺少 cid/page_num/req_type 等必填项,服务端忽略并回落默认季; 复用 tabs 路径的字段结构以切到对应季。 - 子分类式章节仅取“正片”子分类(标题为“正片”,作为 chapter_name 发往分页接口), 与 tabs 路径以 chapter_name=正片 标识正片的方式一致,避免引入花絮/直播回放/直拍。 - 按响应 module_params.has_next(字符串或布尔)逐页翻页取全分集;章节内容不按标题过滤, 仅排除 is_trailer=1 的预告片,加更/彩蛋/抢鲜看等章内内容与主集一并保留。 - 多季按标题年份与春夏秋冬播出季序排序后合并,跨季按 vid 去重,单季内按 episode_on_chapter 排序。 --- danmu_api/sources/tencent.js | 145 +++++++++++++++++++++++++++++++++-- 1 file changed, 137 insertions(+), 8 deletions(-) diff --git a/danmu_api/sources/tencent.js b/danmu_api/sources/tencent.js index b2f3f16..477abc2 100644 --- a/danmu_api/sources/tencent.js +++ b/danmu_api/sources/tencent.js @@ -244,6 +244,10 @@ export default class TencentSource extends BaseSource { for (const item of itemList) { const filtered = this.filterTencentSearchItem(item, keyword); if (filtered) { + // 综艺等分季内容将各季收在 videoInfo.episodeSites/playSites 的 chapterInfo 下, + // 提取每季的 pageContext 供 getEpisodes 逐季补全分集 + const chapters = this.extractChapterContexts(item.videoInfo); + if (chapters.length > 0) filtered.chapterContexts = chapters; results.push(filtered); } } @@ -257,7 +261,50 @@ export default class TencentSource extends BaseSource { } } - async getEpisodes(id) { + // 从 videoInfo 的 episodeSites/playSites 中提取各季章节标题,用于综艺等多季内容逐季补全分集 + extractChapterContexts(videoInfo) { + const chapters = []; + if (!videoInfo) return chapters; + const sites = [...(videoInfo.episodeSites || []), ...(videoInfo.playSites || [])]; + for (const site of sites) { + if (site && site.chapterInfo && Array.isArray(site.chapterInfo.chapters)) { + for (const ch of site.chapterInfo.chapters) { + if (ch && ch.title) { + chapters.push({ title: ch.title }); + } + } + } + } + return chapters; + } + + // 子分类式章节仅取“正片”子分类:该值直接作为请求参数 chapter_name 发往分页接口, + // 与 tabs 分页路径以 chapter_name=正片 标识正片的方式一致;其余子分类(花絮/直播回放/彩蛋等)不请求。 + isZhengpianChapter(ch) { + return !!ch && ch.title === "正片"; + } + + // 将季标题(如 2021冬)转为可排序的时间键,用于多季按时间先后排列; + // 取标题中首个四位年份为高位,并以 春=1/夏=2/秋=3/冬=4 的播出季序为低位。 + seasonOrderKey(title) { + if (!title) return 0; + const yearMatch = title.match(/(?:19|20)\d{2}/); + const year = yearMatch ? parseInt(yearMatch[0], 10) : 0; + const seasonWeight = { "春": 1, "夏": 2, "秋": 3, "冬": 4 }; + let season = 0; + for (const name of Object.keys(seasonWeight)) { + if (title.includes(name)) { season = seasonWeight[name]; break; } + } + return year * 10 + season; + } + + // 按分页 tabs 下发的 page_context 结构构造章节请求参数; + // chapterInfo 自带的 pageContext 缺少 cid/page_num/req_type 等必填项,服务端会忽略并回落默认视图 + buildChapterPageContext(cid, chapterName, pageNum) { + return `lid=&cid=${cid}&page_num=${pageNum}&page_size=30&id_type=1&req_type=6&req_from=web_vsite&req_from_second_type=&detail_page_type=1&year=&tab_type=4&chapter_name=${chapterName}&is_nocopyright=false`; + } + + async getEpisodes(id, chapterContexts = []) { try { log("info", `[tencent] 获取分集列表: cid=${id}`); @@ -324,6 +371,7 @@ export default class TencentSource extends BaseSource { // 获取所有分页的分集 const allEpisodes = []; + const seenVids = new Set(); if (tabs.length === 0) { log("info", "[tencent] 未找到分页信息,尝试从初始响应中提取分集"); @@ -335,11 +383,14 @@ export default class TencentSource extends BaseSource { 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 - }); + if (!seenVids.has(item.item_params.vid)) { + seenVids.add(item.item_params.vid); + allEpisodes.push({ + vid: item.item_params.vid, + title: item.item_params.title, + unionTitle: item.item_params.union_title || item.item_params.title + }); + } } } } @@ -347,12 +398,90 @@ export default class TencentSource extends BaseSource { } } + // 综艺等分季内容:各季收在 chapterInfo 下,逐季分页请求并合并 + if (chapterContexts.length > 0) { + // 章节本身为子分类(正片/花絮)时仅取正片子分类;否则按季遍历 + const zhengpianChapters = chapterContexts.filter((ch) => this.isZhengpianChapter(ch)); + const targetChapters = zhengpianChapters.length > 0 ? zhengpianChapters : chapterContexts; + // 按季遍历时按时间先后排序;仅取正片子分类时保持原顺序 + const orderedChapters = zhengpianChapters.length > 0 + ? targetChapters + : targetChapters.slice().sort((a, b) => this.seasonOrderKey(a.title) - this.seasonOrderKey(b.title)); + for (const ch of orderedChapters) { + try { + const seasonEpisodes = []; + let pageNum = 0; + let hasNext = true; + while (hasNext) { + const chapterPayload = { + has_cache: 1, + page_params: { + req_from: "web_vsite", + page_id: "vsite_episode_list", + page_type: "detail_operation", + id_type: "1", + page_size: "", + cid: id, + vid: "", + lid: "", + page_num: "", + page_context: this.buildChapterPageContext(id, ch.title, pageNum), + detail_page_type: "1" + } + }; + const chapterResponse = await httpPost(episodesUrl, JSON.stringify(chapterPayload), { headers }); + if (!chapterResponse || !chapterResponse.data) break; + const chapterData = typeof chapterResponse.data === "string" ? JSON.parse(chapterResponse.data) : chapterResponse.data; + if (chapterData.ret !== 0 || !chapterData.data) break; + hasNext = false; + let pageItemCount = 0; + if (chapterData.data.module_list_datas) { + for (const moduleListData of chapterData.data.module_list_datas) { + for (const moduleData of moduleListData.module_datas) { + const hasNextFlag = moduleData.module_params && moduleData.module_params.has_next; + if (hasNextFlag === true || hasNextFlag === "true") hasNext = true; + 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") { + pageItemCount++; + seasonEpisodes.push({ + vid: item.item_params.vid, + title: item.item_params.title, + unionTitle: item.item_params.union_title || item.item_params.title, + order: parseInt(item.item_params.episode_on_chapter, 10) || 0 + }); + } + } + } + } + } + } + if (pageItemCount === 0) break; + pageNum++; + } + // 单季内按集序号排序,再合并(跨季按 vid 去重) + seasonEpisodes.sort((a, b) => a.order - b.order); + for (const ep of seasonEpisodes) { + if (seenVids.has(ep.vid)) continue; + seenVids.add(ep.vid); + allEpisodes.push({ + vid: ep.vid, + title: ep.title, + unionTitle: ep.unionTitle + }); + } + } catch (error) { + log("error", `[tencent] 分季分集请求失败: ${error.message}`); + } + } + } + if (allEpisodes.length === 0) { log("info", "[tencent] 初始响应中也未找到分集信息"); return []; } - log("info", `[tencent] 从初始响应中提取到 ${allEpisodes.length} 集`); + log("info", `[tencent] 共获取 ${allEpisodes.length} 集(含多季合并)`); } else { log("info", `[tencent] 找到 ${tabs.length} 个分页`); @@ -455,7 +584,7 @@ export default class TencentSource extends BaseSource { // 使用 map 和 async 时需要返回 Promise 数组,并等待所有 Promise 完成 const processTencentAnimes = await Promise.all(filteredAnimes.map(async (anime) => { try { - const eps = await this.getEpisodes(anime.mediaId); + const eps = await this.getEpisodes(anime.mediaId, anime.chapterContexts); let links = []; for (let i = 0; i < eps.length; i++) {