fix(tencent): 支持单 cid 综艺的多季分集获取 (#399)
综艺单 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 排序。
This commit is contained in:
1 parent
9d31e85dd1
commit
c9112ee7cf
1 file changed
+137
-8
@@ -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++) {
|
||||
|
||||
Reference in new issue
Block a user