perf(search): 重构源多季场景匹配逻辑,大幅降低无效请求提升匹配速度;为人人源新增多端接口与智能路由,改善区域连通性问题 (#303)

* perf(api): 增加季参数透传与缓存隔离机制

- 在 `matchAnime` 重定向时提取 season 参数并隐式传递给 `searchAnime`。
- 修改所有源站 `handleAnimes` 调用,追加 `querySeason` 参数下发。
- 重构搜索缓存的 Key 生成规则,引入 `_S{season}` 后缀,实现按季度的缓存空间隔离,解决换季搜索时的旧缓存穿透 Bug。

* feat(utils): 升级季度混合匹配算法

- 升级 `titleMatches` 函数签名,新增 `parsedSeason` 参数支持。
- 统一向外部源站暴露 `getExplicitSeasonNumber` 与 `extractSeasonNumberFromAnimeTitle` 解析函数。

* perf(sources): 实现详情请求前置拦截与短路回退

* perf(sources): 实现详情请求前置拦截与短路回退

* perf(sources): 实现详情请求前置拦截与短路回退

* perf(sources): 实现详情请求前置拦截与短路回退

* perf(sources): 实现详情请求前置拦截与短路回退

* perf(sources): 实现详情请求前置拦截与短路回退

* perf(sources): 实现详情请求前置拦截与短路回退

* perf(sources): 实现详情请求前置拦截与短路回退

* perf(sources): 实现详情请求前置拦截与短路回退

* perf(sources): 实现详情请求前置拦截与短路回退

* perf(sources): 实现详情请求前置拦截与短路回退

* perf(sources): 实现详情请求前置拦截与短路回退

* perf(sources): 实现详情请求前置拦截与短路回退

* perf(sources): 实现详情请求前置拦截与短路回退

* perf(sources): 实现详情请求前置拦截与短路回退

* perf(sources): 实现详情请求前置拦截与短路回退

* perf(sources): 透传querySeason

* perf(sources): 实现详情请求前置拦截与短路回退

* perf(sources): 实现详情请求前置拦截与短路回退

* perf(sources): 实现详情请求前置拦截与短路回退

* perf(api): 补全 `querySeason` 参数下发

* feat(test): 增加 Bangumi Data 本地检索与季度参数的拦截检索测试

* fix(sources): 修复韩剧TV定义了多余的参数导致缓存错误赋值无法参与match匹配

* perf(sources): 人人源新增 Win 端接口与智能路由,改善区域连通性问题

https://github.com/huangxd-/danmu_api/issues/295
wni 接口的详情接口疑似存在验证密钥,暂时没能力获取目前只添加了它的搜索和弹幕接口

* Fix TV_DANMU_HOST URL in renren.js

* feat(utils): 新增 `validStatusCodes` 参数配置,允许外部主动接管特定的非 2xx 状态码

配合修复人人无弹幕状态码是404导致重试

* perf(sources): 修复人人源在“剧集无弹幕”时返回 HTTP 404导致无效重试的问题

* perf(sources): Animeko弹幕获取阶段引入人人一样的智能路由机制

* perf(sources): 使用曲线救国的方式补全人人 win 详情接口,同时增加 mac 接口,并调整了函数整体结构提高可读性

复杂化致歉()但是现在拥有了四个接口,完善的智能接口回退,希望这是最后一次更改这个源(

* perf(sources): 挂载 WIN 接口的别名,并支持识别会员提示弹窗用于智能偏移时长

https://github.com/huangxd-/danmu_api/issues/305

* perf(sources): 调整日志输出

* feat(utils): 修复合并后源的别名列表丢失

* perf(sources): 完善接口降级逻辑,防止误触发

Bug修呀修

* perf(api): 清理合并导致的 search 接口多余 links

* 1.19.5
This commit is contained in:
Elegy233 authored and GitHub committed 2026-05-16 09:31:11 +08:00
1 parent 8e3d208c2f
commit ffe4c68293
26 files changed
+1606 -490

No files matched your search

+37 -27
View File
@@ -219,7 +219,9 @@ export function matchSeason(anime, queryTitle, season) {
// Extracted function for GET /api/v2/search/anime
export async function searchAnime(url, preferAnimeId = null, preferSource = null, detailStore = null) {
let queryTitle = url.searchParams.get("keyword");
log("info", `Search anime with keyword: ${queryTitle}`);
let querySeason = url.searchParams.get("season");
querySeason = querySeason ? parseInt(querySeason, 10) : null;
log("info", `Search anime with keyword: ${queryTitle}, target season: ${querySeason}`);
// 关键字为空直接返回,不用多余查询
if (queryTitle === "") {
@@ -239,9 +241,10 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null
}
const requestAnimeDetailsMap = detailStore instanceof Map ? detailStore : new Map();
const cacheKey = querySeason !== null ? `${queryTitle}_S${querySeason}` : queryTitle;
// 检查搜索缓存
const cachedResults = getSearchCache(queryTitle, requestAnimeDetailsMap);
const cachedResults = getSearchCache(cacheKey, requestAnimeDetailsMap);
if (cachedResults !== null) {
return jsonResponse({
errorCode: 0,
@@ -317,11 +320,14 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null
await updateLocalRedisCaches();
}
// 构造响应 DTO:剥离合并产生的 links,确保接口纯净
const responseAnimes = curAnimes.map(({ links, ...pureAnime }) => pureAnime);
return jsonResponse({
errorCode: 0,
success: true,
errorMessage: "",
animes: curAnimes,
animes: responseAnimes,
});
}
@@ -376,73 +382,73 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null
for (const key of globals.sourceOrderArr) {
if (key === '360') {
// 等待处理360来源
await kan360Source.handleAnimes(animes360, queryTitle, curAnimes, requestAnimeDetailsMap);
await kan360Source.handleAnimes(animes360, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
} else if (key === 'vod') {
// 等待处理Vod来源(遍历所有VOD服务器的结果)
if (animesVodResults && Array.isArray(animesVodResults)) {
for (const vodResult of animesVodResults) {
if (vodResult && vodResult.list && vodResult.list.length > 0) {
await vodSource.handleAnimes(vodResult.list, queryTitle, curAnimes, vodResult.serverName, requestAnimeDetailsMap);
await vodSource.handleAnimes(vodResult.list, queryTitle, curAnimes, vodResult.serverName, requestAnimeDetailsMap, querySeason);
}
}
}
} else if (key === 'tmdb') {
// 等待处理TMDB来源
await tmdbSource.handleAnimes(animesTmdb, queryTitle, curAnimes, requestAnimeDetailsMap);
await tmdbSource.handleAnimes(animesTmdb, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
} else if (key === 'douban') {
// 等待处理Douban来源
await doubanSource.handleAnimes(animesDouban, queryTitle, curAnimes, requestAnimeDetailsMap);
await doubanSource.handleAnimes(animesDouban, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
} else if (key === 'renren') {
// 等待处理Renren来源
await renrenSource.handleAnimes(animesRenren, queryTitle, curAnimes, requestAnimeDetailsMap);
await renrenSource.handleAnimes(animesRenren, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
} else if (key === 'hanjutv') {
// 等待处理Hanjutv来源
await hanjutvSource.handleAnimes(animesHanjutv, queryTitle, curAnimes, requestAnimeDetailsMap);
await hanjutvSource.handleAnimes(animesHanjutv, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
} else if (key === 'bahamut') {
// 等待处理Bahamut来源
await bahamutSource.handleAnimes(animesBahamut, queryTitle, curAnimes, requestAnimeDetailsMap);
await bahamutSource.handleAnimes(animesBahamut, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
} else if (key === 'dandan') {
// 等待处理弹弹play来源
await dandanSource.handleAnimes(animesDandan, queryTitle, curAnimes, requestAnimeDetailsMap);
await dandanSource.handleAnimes(animesDandan, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
} else if (key === 'custom') {
// 等待处理自定义弹幕源来源
await customSource.handleAnimes(animesCustom, queryTitle, curAnimes, requestAnimeDetailsMap);
} else if (key === 'tencent') {
// 等待处理Tencent来源
await tencentSource.handleAnimes(animesTencent, queryTitle, curAnimes, requestAnimeDetailsMap);
await tencentSource.handleAnimes(animesTencent, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
} else if (key === 'youku') {
// 等待处理Youku来源
await youkuSource.handleAnimes(animesYouku, queryTitle, curAnimes, requestAnimeDetailsMap);
await youkuSource.handleAnimes(animesYouku, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
} else if (key === 'iqiyi') {
// 等待处理iQiyi来源
await iqiyiSource.handleAnimes(animesIqiyi, queryTitle, curAnimes, requestAnimeDetailsMap);
await iqiyiSource.handleAnimes(animesIqiyi, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
} else if (key === 'imgo') {
// 等待处理Mango来源
await mangoSource.handleAnimes(animesImgo, queryTitle, curAnimes, requestAnimeDetailsMap);
await mangoSource.handleAnimes(animesImgo, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
} else if (key === 'bilibili') {
// 等待处理Bilibili来源
await bilibiliSource.handleAnimes(animesBilibili, queryTitle, curAnimes, requestAnimeDetailsMap);
await bilibiliSource.handleAnimes(animesBilibili, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
} else if (key === 'migu') {
// 等待处理Migu来源
await miguSource.handleAnimes(animesMigu, queryTitle, curAnimes, requestAnimeDetailsMap);
await miguSource.handleAnimes(animesMigu, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
} else if (key === 'sohu') {
// 等待处理Sohu来源
await sohuSource.handleAnimes(animesSohu, queryTitle, curAnimes, requestAnimeDetailsMap);
await sohuSource.handleAnimes(animesSohu, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
} else if (key === 'leshi') {
// 等待处理Leshi来源
await leshiSource.handleAnimes(animesLeshi, queryTitle, curAnimes, requestAnimeDetailsMap);
await leshiSource.handleAnimes(animesLeshi, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
} else if (key === 'xigua') {
// 等待处理Xigua来源
await xiguaSource.handleAnimes(animesXigua, queryTitle, curAnimes, requestAnimeDetailsMap);
await xiguaSource.handleAnimes(animesXigua, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
} else if (key === 'maiduidui') {
// 等待处理Maiduidui来源
await maiduiduiSource.handleAnimes(animesMaiduidui, queryTitle, curAnimes, requestAnimeDetailsMap);
await maiduiduiSource.handleAnimes(animesMaiduidui, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
} else if (key === 'aiyifan') {
// 等待处理Aiyifan来源
await aiyifanSource.handleAnimes(animesAiyifan, queryTitle, curAnimes, requestAnimeDetailsMap);
await aiyifanSource.handleAnimes(animesAiyifan, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
} else if (key === 'animeko') {
// 等待处理Animeko来源
await animekoSource.handleAnimes(animesAnimeko, queryTitle, curAnimes, requestAnimeDetailsMap);
await animekoSource.handleAnimes(animesAnimeko, queryTitle, curAnimes, requestAnimeDetailsMap, querySeason);
}
}
} catch (error) {
@@ -509,16 +515,20 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null
await updateLocalRedisCaches();
}
// 构造响应 DTO:剥离合并产生的 links,确保接口纯净
const responseAnimes = curAnimes.map(({ links, ...pureAnime }) => pureAnime);
// 缓存搜索结果
if (curAnimes.length > 0) {
setSearchCache(queryTitle, curAnimes, requestAnimeDetailsMap);
if (responseAnimes.length > 0) {
const cacheKey = querySeason !== null ? `${queryTitle}_S${querySeason}` : queryTitle;
setSearchCache(cacheKey, responseAnimes, requestAnimeDetailsMap);
}
return jsonResponse({
errorCode: 0,
success: true,
errorMessage: "",
animes: curAnimes,
animes: responseAnimes,
});
}
@@ -1090,7 +1100,7 @@ export async function matchAnime(url, req, clientIp) {
log("info", `prefer animeId: ${preferAnimeId} from ${preferSource}`);
const requestAnimeDetailsMap = new Map();
let originSearchUrl = new URL(req.url.replace("/match", `/search/anime?keyword=${title}`));
let originSearchUrl = new URL(req.url.replace("/match", `/search/anime?keyword=${title}&season=${season || ''}`));
const searchRes = await searchAnime(originSearchUrl, preferAnimeId, preferSource, requestAnimeDetailsMap);
const searchData = await searchRes.json();
log("info", `searchData: ${searchData.animes}`);
+1 -1
View File
@@ -13,7 +13,7 @@ export const Globals = {
accessedEnvVars: {},
// 静态常量
VERSION: '1.19.4',
VERSION: '1.19.5',
MAX_LOGS: 1000, // 日志存储,最多保存 1000 行
MAX_RECORDS: 100, // 请求记录最大数量
+29 -11
View File
@@ -5,7 +5,7 @@ import { convertToAsciiSum } from "../utils/codec-util.js";
import { hexToInt } from "../utils/danmu-util.js";
import { generateValidStartDate } from "../utils/time-util.js";
import { addAnime, removeEarliestAnime } from "../utils/cache-util.js";
import { titleMatches } from "../utils/common-util.js";
import { titleMatches, getExplicitSeasonNumber, extractSeasonNumberFromAnimeTitle } from "../utils/common-util.js";
import { simplized } from "../utils/zh-util.js";
import { globals } from '../configs/globals.js';
import { AiyifanSigningProvider } from '../utils/aiyifan-util.js';
@@ -308,14 +308,14 @@ export default class AiyifanSource extends BaseSource {
}
/**
* 处理animes结果
* @param {Array} sourceAnimes - 源动漫数据
* @param {string} queryTitle - 查询标题
* @param {Array} curAnimes - 当前动漫列表
* @param {any} detailStore - 详情存储
* @returns {Promise<Array>} 处理后的动漫列表
* 处理搜索结果
* @param {Array} sourceAnimes 原始数据
* @param {string} queryTitle 关键词
* @param {Array} curAnimes 结果池
* @param {Map} detailStore 详情缓存
* @param {number|null} querySeason 目标季度
*/
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null) {
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null, querySeason = null) {
const tmpAnimes = [];
if (!sourceAnimes || !Array.isArray(sourceAnimes)) {
@@ -323,9 +323,27 @@ export default class AiyifanSource extends BaseSource {
return [];
}
const processPromises = sourceAnimes
.filter(anime => titleMatches(anime.title, queryTitle))
.map(async (anime) => {
// 基础标题与季度匹配过滤
let filteredAnimes = sourceAnimes.filter(anime => titleMatches(anime.title, queryTitle, querySeason));
// 提取搜索词中的明确季度信息或使用传入的季度参数
const resolvedQuerySeason = querySeason !== null ? querySeason : getExplicitSeasonNumber(queryTitle);
// 初始列表预过滤机制:若用户指定了季度,优先检查结果中是否已包含匹配项
if (resolvedQuerySeason !== null) {
const seasonFiltered = filteredAnimes.filter(anime => {
const s = extractSeasonNumberFromAnimeTitle(anime.title).season;
return s === resolvedQuerySeason || (resolvedQuerySeason === 1 && s === null);
});
// 如果已命中目标,减少详情请求量
if (seasonFiltered.length > 0) {
filteredAnimes = seasonFiltered;
log("info", `[Aiyifan] 结果已命中目标季(第${resolvedQuerySeason}季),跳过非目标季相关请求`);
}
}
const processPromises = filteredAnimes.map(async (anime) => {
try {
// 获取剧集列表
const eps = await this.getEpisodes(anime.mediaId);
+65 -23
View File
@@ -5,13 +5,19 @@ import { httpGet, httpPost } from "../utils/http-util.js";
import { addAnime, removeEarliestAnime } from "../utils/cache-util.js";
import { simplized } from "../utils/zh-util.js";
import { SegmentListResponse } from '../models/dandan-model.js';
import { titleMatches } from "../utils/common-util.js";
import { titleMatches, getExplicitSeasonNumber, extractSeasonNumberFromAnimeTitle } from "../utils/common-util.js";
import { searchBangumiData } from '../utils/bangumi-data-util.js';
// =====================
// 获取Animeko弹幕(https://github.com/open-ani/animeko)
// =====================
// 接口健康状态缓存 (全局共享,跨请求持久化,实现业务级智能路由)
// 链路层级规范: Danmu (GLOBAL -> CN)
const API_HEALTH = {
danmu: 'GLOBAL'
};
/**
* Animeko 源适配器 (基于 Bangumi API V0)
* 提供深度元数据搜索、结果过滤及条目关系检测功能
@@ -333,7 +339,6 @@ export default class AnimekoSource extends BaseSource {
});
// 1. 结构校验:确保 resp.data.data 存在且为数组
// 对应您的 JSON: resp.data 存在,resp.data.data 是 [] (数组),校验通过
if (!resp || !resp.data || !Array.isArray(resp.data.data)) {
if (offset === 0) {
log("info", `[Animeko] Subject ${subjectId} 无剧集数据或响应异常`);
@@ -344,7 +349,6 @@ export default class AnimekoSource extends BaseSource {
const currentBatch = resp.data.data;
// 2. 空数据校验:如果没有数据,停止
// 对应您的 JSON: data 为 [],length 为 0,在此处 break 退出
if (currentBatch.length === 0) {
break;
}
@@ -386,12 +390,14 @@ export default class AnimekoSource extends BaseSource {
}
/**
* 处理并存储番剧及剧集信息
* @param {Array} sourceAnimes 搜索到的番剧列表
* @param {string} queryTitle 原始查询标题
* @param {Array} curAnimes 当前缓存的番剧列表
* 处理搜索结果
* @param {Array} sourceAnimes 原始数据
* @param {string} queryTitle 关键词
* @param {Array} curAnimes 结果池
* @param {Map} detailStore 详情缓存
* @param {number|null} querySeason 目标季度
*/
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null) {
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null, querySeason = null) {
const tmpAnimes = [];
if (!sourceAnimes || !Array.isArray(sourceAnimes)) {
@@ -399,7 +405,27 @@ export default class AnimekoSource extends BaseSource {
return [];
}
const processAnimekoAnimes = await Promise.all(sourceAnimes.map(async (anime) => {
let filteredAnimes = sourceAnimes;
// 提取搜索词中的明确季度信息或使用传入的季度参数
const resolvedQuerySeason = querySeason !== null ? querySeason : getExplicitSeasonNumber(queryTitle);
// 初始列表预过滤机制:若用户指定了季度,优先检查结果中是否已包含匹配项
if (resolvedQuerySeason !== null) {
const seasonFiltered = filteredAnimes.filter(anime => {
const titleToCheck = anime.name_cn || anime.name;
const s = extractSeasonNumberFromAnimeTitle(titleToCheck).season;
return s === resolvedQuerySeason || (resolvedQuerySeason === 1 && s === null);
});
// 如果已命中目标,减少详情请求量
if (seasonFiltered.length > 0) {
filteredAnimes = seasonFiltered;
log("info", `[Animeko] 结果已命中目标季(第${resolvedQuerySeason}季),跳过非目标季相关请求`);
}
}
const processAnimekoAnimes = await Promise.all(filteredAnimes.map(async (anime) => {
try {
const eps = await this.getEpisodes(anime.id);
let links = [];
@@ -488,8 +514,11 @@ export default class AnimekoSource extends BaseSource {
return [];
}
const HOST_GLOBAL = "https://danmaku-global.myani.org";
const HOST_CN = "https://danmaku-cn.myani.org";
const endpoints = {
'GLOBAL': "https://danmaku-global.myani.org",
'CN': "https://danmaku-cn.myani.org"
};
const tiers = ['GLOBAL', 'CN'];
// 定义内部通用请求函数
const fetchDanmu = async (hostUrl) => {
@@ -508,22 +537,35 @@ export default class AnimekoSource extends BaseSource {
}
};
// 2. 优先尝试 Global 节点
let danmuList = await fetchDanmu(HOST_GLOBAL);
let danmuList = null;
let currentTierIndex = tiers.indexOf(API_HEALTH.danmu);
if (currentTierIndex === -1) currentTierIndex = 0;
// 3. 如果失败,降级尝试 CN 节点
if (!danmuList) {
log("info", `[Animeko] Global 节点获取失败/无数据,降级尝试 CN 节点... ID:${realId}`);
danmuList = await fetchDanmu(HOST_CN);
// 智能路由检测与降级回路
for (let i = currentTierIndex; i < tiers.length; i++) {
const tier = tiers[i];
const hostUrl = endpoints[tier];
log("info", `[Animeko] 尝试使用 ${tier} 节点获取弹幕`);
danmuList = await fetchDanmu(hostUrl);
if (danmuList && Array.isArray(danmuList)) {
// 记录当前健康的节点层级
if (API_HEALTH.danmu !== tier) {
log("info", `[Animeko] 弹幕域节点健康状态更新: ${API_HEALTH.danmu} -> ${tier}`);
API_HEALTH.danmu = tier;
}
log("info", `[Animeko] 成功获取弹幕,共 ${danmuList.length} 条 (${tier}节点)`);
return danmuList;
} else {
log("info", `[Animeko] ${tier} 节点获取失败/无数据,触发降级`);
}
}
// 4. 返回结果或空数组
if (danmuList) {
log("info", `[Animeko] 成功获取弹幕,共 ${danmuList.length} 条`);
return danmuList;
}
// 所有节点轮换完毕仍未获取到数据,重置健康状态
log("info", `[Animeko] 弹幕域所有降级节点均失败,重置健康状态至 GLOBAL 节点`);
API_HEALTH.danmu = 'GLOBAL';
log("error", "[Animeko] 所有节点尝试均失败,无法获取弹幕");
return [];
}
+14 -7
View File
@@ -241,12 +241,19 @@ export default class BahamutSource extends BaseSource {
}
}
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null) {
/**
* 处理搜索结果
* @param {Array} sourceAnimes 原始数据
* @param {string} queryTitle 关键词
* @param {Array} curAnimes 结果池
* @param {Map|null} detailStore 详情缓存
* @param {number|null} querySeason 目标季度
*/
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null, querySeason = null) {
const tmpAnimes = [];
// 使用正则判断原始搜索词是否包含日文平假名或片假名
const isJapaneseKeyword = /[\u3040-\u309F\u30A0-\u30FF]/.test(queryTitle);
queryTitle = traditionalized(queryTitle);
// 巴哈姆特搜索辅助函数
@@ -337,23 +344,23 @@ export default class BahamutSource extends BaseSource {
const cnAlias = filtered.length > 0 ? filtered[0]._tmdbCnAlias : null;
smartTitleReplace(filtered, cnAlias);
// 提取搜索词中的明确季度信息
const querySeason = getExplicitSeasonNumber(queryTitle);
// 提取搜索词中的明确季度信息或使用传入的季度参数
const resolvedQuerySeason = querySeason !== null ? querySeason : getExplicitSeasonNumber(queryTitle);
// 初始列表预过滤机制:若用户指定了季度,优先检查初始结果中是否已包含匹配项
let matchedAnimes = filtered;
if (querySeason !== null) {
if (resolvedQuerySeason !== null) {
const seasonFiltered = filtered.filter(anime => {
const titleToCheck = anime._displayTitle || anime.title;
const s = extractSeasonNumberFromAnimeTitle(titleToCheck).season;
return s === querySeason || (querySeason === 1 && s === null);
return s === resolvedQuerySeason || (resolvedQuerySeason === 1 && s === null);
});
// 如果已命中目标,减少详情请求量
if (seasonFiltered.length > 0) {
matchedAnimes = seasonFiltered;
log("info", `[Bahamut] 结果已命中目标季(第${querySeason}季),跳过非目标季相关请求`);
log("info", `[Bahamut] 结果已命中目标季(第${resolvedQuerySeason}季),跳过非目标季相关请求`);
}
}
+37 -6
View File
@@ -5,7 +5,7 @@ import { httpGet, httpGetWithStreamCheck } from "../utils/http-util.js";
import { parseDanmakuBase64, md5, convertToAsciiSum } from "../utils/codec-util.js";
import { generateValidStartDate } from "../utils/time-util.js";
import { addAnime, removeEarliestAnime } from "../utils/cache-util.js";
import { titleMatches } from "../utils/common-util.js";
import { titleMatches, getExplicitSeasonNumber, extractSeasonNumberFromAnimeTitle } from "../utils/common-util.js";
import { SegmentListResponse } from '../models/dandan-model.js';
import { simplized } from "../utils/zh-util.js";
import { getTmdbJaOriginalTitle, smartTitleReplace } from "../utils/tmdb-util.js";
@@ -553,7 +553,15 @@ export default class BilibiliSource extends BaseSource {
return [];
}
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null) {
/**
* 处理搜索结果
* @param {Array} sourceAnimes 原始数据
* @param {string} queryTitle 关键词
* @param {Array} curAnimes 结果池
* @param {Map} detailStore 详情缓存
* @param {number|null} querySeason 目标季度
*/
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null, querySeason = null) {
const tmpAnimes = [];
// 添加错误处理,确保sourceAnimes是数组
@@ -577,10 +585,33 @@ export default class BilibiliSource extends BaseSource {
const cnAlias = sourceAnimes.length > 0 ? sourceAnimes[0]._tmdbCnAlias : null;
smartTitleReplace(sourceAnimes, cnAlias);
const processPromises = sourceAnimes
// 港澳台资源不做严格标题匹配,其他资源根据当前标题或别名池(已包含原标题和 org_title)验证查询匹配度
.filter(anime => anime.isOversea || titleMatches(anime.title, queryTitle) || (anime.aliases && anime.aliases.some(alias => titleMatches(alias, queryTitle))))
.map(async (anime) => {
// 基础标题与季度匹配过滤
// 港澳台资源不做严格标题匹配,其他资源根据当前标题或别名池(已包含原标题和 org_title)验证查询匹配度
let filteredAnimes = sourceAnimes.filter(anime =>
anime.isOversea ||
titleMatches(anime.title, queryTitle, querySeason) ||
(anime.aliases && anime.aliases.some(alias => titleMatches(alias, queryTitle, querySeason)))
);
// 提取搜索词中的明确季度信息或使用传入的季度参数
const resolvedQuerySeason = querySeason !== null ? querySeason : getExplicitSeasonNumber(queryTitle);
// 初始列表预过滤机制:若用户指定了季度,优先检查结果中是否已包含匹配项
if (resolvedQuerySeason !== null) {
const seasonFiltered = filteredAnimes.filter(anime => {
const titleToCheck = anime._displayTitle || anime.title;
const s = extractSeasonNumberFromAnimeTitle(titleToCheck).season;
return s === resolvedQuerySeason || (resolvedQuerySeason === 1 && s === null);
});
// 如果已命中目标,减少详情请求量
if (seasonFiltered.length > 0) {
filteredAnimes = seasonFiltered;
log("info", `[Bilibili] 结果已命中目标季(第${resolvedQuerySeason}季),跳过非目标季相关请求`);
}
}
const processPromises = filteredAnimes.map(async (anime) => {
try {
let links = [];
+19 -13
View File
@@ -290,8 +290,15 @@ export default class DandanSource extends BaseSource {
return intersection / union;
}
// 处理并转换番剧信息
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null) {
/**
* 处理搜索结果
* @param {Array} sourceAnimes 原始数据
* @param {string} queryTitle 关键词
* @param {Array} curAnimes 结果池
* @param {Map|null} detailStore 详情缓存
* @param {number|null} querySeason 目标季度
*/
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null, querySeason = null) {
const tmpAnimes = [];
// 添加错误处理,确保sourceAnimes是数组
@@ -314,25 +321,25 @@ export default class DandanSource extends BaseSource {
const existingIds = new Set();
const queue = [];
// 提取搜索词中的明确季度信息
const querySeason = getExplicitSeasonNumber(queryTitle);
// 提取搜索词中的明确季度信息或使用传入的季度参数
const resolvedQuerySeason = querySeason !== null ? querySeason : getExplicitSeasonNumber(queryTitle);
// 初始列表预过滤机制:若用户指定了季度,优先检查初始结果中是否已包含匹配项
let matchedAnimes = sourceAnimes;
let isTargetFoundInInitial = false;
if (querySeason !== null) {
if (resolvedQuerySeason !== null) {
const filtered = sourceAnimes.filter(anime => {
const titleToCheck = anime._displayTitle || anime.animeTitle;
const s = extractSeasonNumberFromAnimeTitle(titleToCheck).season;
return s === querySeason || (querySeason === 1 && s === null);
return s === resolvedQuerySeason || (resolvedQuerySeason === 1 && s === null);
});
// 如果已命中目标,减少详情请求量
if (filtered.length > 0) {
matchedAnimes = filtered;
isTargetFoundInInitial = true;
log("info", `[Dandan] 结果已命中目标季(第${querySeason}季),跳过非目标季相关请求`);
log("info", `[Dandan] 结果已命中目标季(第${resolvedQuerySeason}季),跳过非目标季相关请求`);
}
}
@@ -387,8 +394,7 @@ export default class DandanSource extends BaseSource {
if (anime.isRelated || anime.isTmdbSource) {
// 相关作品及TMDB原名搜索结果逻辑:仅执行单纯的季度过滤,跳过常规标题匹配,防止标题差异导致误判
const querySeason = getExplicitSeasonNumber(queryTitle);
if (querySeason !== null) {
if (resolvedQuerySeason !== null) {
let titleSeason = null;
for (const t of allTitles) {
if (!t) continue;
@@ -398,9 +404,9 @@ export default class DandanSource extends BaseSource {
break;
}
}
if (querySeason > 1) {
isMatch = (titleSeason || 1) === querySeason;
} else if (querySeason === 1) {
if (resolvedQuerySeason > 1) {
isMatch = (titleSeason || 1) === resolvedQuerySeason;
} else if (resolvedQuerySeason === 1) {
isMatch = titleSeason === null || titleSeason === 1;
}
} else {
@@ -408,7 +414,7 @@ export default class DandanSource extends BaseSource {
}
} else {
// 初始数据源逻辑:执行严密的完整标题及季度双重校验
isMatch = allTitles.some(t => t && titleMatches(t, queryTitle));
isMatch = allTitles.some(t => t && titleMatches(t, queryTitle, resolvedQuerySeason));
}
// 丢弃不符合拦截策略的条目,停止后续构建流程
+41 -8
View File
@@ -1,6 +1,7 @@
import BaseSource from './base.js';
import { log } from "../utils/log-util.js";
import { getDoubanDetail, searchDoubanTitles, searchDoubanTitlesByPublic } from "../utils/douban-util.js";
import { titleMatches, getExplicitSeasonNumber, extractSeasonNumberFromAnimeTitle } from "../utils/common-util.js";
// =====================
// 获取豆瓣源播放链接
@@ -112,7 +113,15 @@ export default class DoubanSource extends BaseSource {
async getEpisodes(id) {}
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null) {
/**
* 处理搜索结果
* @param {Array} sourceAnimes 原始数据
* @param {string} queryTitle 关键词
* @param {Array} curAnimes 结果池
* @param {Map} detailStore 详情缓存
* @param {number|null} querySeason 目标季度
*/
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null, querySeason = null) {
const doubanAnimes = [];
// 添加错误处理,确保sourceAnimes是数组
@@ -121,7 +130,31 @@ export default class DoubanSource extends BaseSource {
return [];
}
const processDoubanAnimes = await Promise.allSettled(sourceAnimes.map(async (anime) => {
// 基础标题与季度匹配过滤
let filteredAnimes = sourceAnimes.filter(anime => {
const titleToCheck = anime?.target?.title || "";
return titleMatches(titleToCheck, queryTitle, querySeason);
});
// 提取搜索词中的明确季度信息或使用传入的季度参数
const resolvedQuerySeason = querySeason !== null ? querySeason : getExplicitSeasonNumber(queryTitle);
// 初始列表预过滤机制:若用户指定了季度,优先检查结果中是否已包含匹配项
if (resolvedQuerySeason !== null) {
const seasonFiltered = filteredAnimes.filter(anime => {
const titleToCheck = anime?.target?.title || "";
const s = extractSeasonNumberFromAnimeTitle(titleToCheck).season;
return s === resolvedQuerySeason || (resolvedQuerySeason === 1 && s === null);
});
// 如果已命中目标,减少详情请求量
if (seasonFiltered.length > 0) {
filteredAnimes = seasonFiltered;
log("info", `[Douban] 结果已命中目标季(第${resolvedQuerySeason}季),跳过非目标季相关请求`);
}
}
const processDoubanAnimes = await Promise.allSettled(filteredAnimes.map(async (anime) => {
try {
if (anime?.layout !== "subject") return;
const doubanId = anime.target_id;
@@ -180,7 +213,7 @@ export default class DoubanSource extends BaseSource {
if (cid) {
tmpAnimes[0].provider = "tencent";
tmpAnimes[0].mediaId = cid;
await this.tencentSource.handleAnimes(tmpAnimes, response.data?.title, doubanAnimes, detailStore)
await this.tencentSource.handleAnimes(tmpAnimes, response.data?.title, doubanAnimes, detailStore, querySeason)
}
break;
}
@@ -189,7 +222,7 @@ export default class DoubanSource extends BaseSource {
if (tvid) {
tmpAnimes[0].provider = "iqiyi";
tmpAnimes[0].mediaId = anime?.type_name === '电影' ? `movie_${tvid}` : tvid;
await this.iqiyiSource.handleAnimes(tmpAnimes, response.data?.title, doubanAnimes, detailStore)
await this.iqiyiSource.handleAnimes(tmpAnimes, response.data?.title, doubanAnimes, detailStore, querySeason)
}
break;
}
@@ -198,7 +231,7 @@ export default class DoubanSource extends BaseSource {
if (showId) {
tmpAnimes[0].provider = "youku";
tmpAnimes[0].mediaId = showId;
await this.youkuSource.handleAnimes(tmpAnimes, response.data?.title, doubanAnimes, detailStore)
await this.youkuSource.handleAnimes(tmpAnimes, response.data?.title, doubanAnimes, detailStore, querySeason)
}
break;
}
@@ -207,7 +240,7 @@ export default class DoubanSource extends BaseSource {
if (seasonId) {
tmpAnimes[0].provider = "bilibili";
tmpAnimes[0].mediaId = `ss${seasonId}`;
await this.bilibiliSource.handleAnimes(tmpAnimes, response.data?.title, doubanAnimes, detailStore)
await this.bilibiliSource.handleAnimes(tmpAnimes, response.data?.title, doubanAnimes, detailStore, querySeason)
}
break;
}
@@ -221,7 +254,7 @@ export default class DoubanSource extends BaseSource {
if (epId) {
tmpAnimes[0].provider = "migu";
tmpAnimes[0].mediaId = `https://v3-sc.miguvideo.com/program/v4/cont/content-info/${epId}/1`;
await this.miguSource.handleAnimes(tmpAnimes, response.data?.title, doubanAnimes, detailStore)
await this.miguSource.handleAnimes(tmpAnimes, response.data?.title, doubanAnimes, detailStore, querySeason)
}
break;
}
@@ -241,4 +274,4 @@ export default class DoubanSource extends BaseSource {
async getEpisodeDanmu(id) {}
formatComments(comments) {}
}
}
+32 -5
View File
@@ -5,7 +5,7 @@ import { httpGet } from "../utils/http-util.js";
import { convertToAsciiSum } from "../utils/codec-util.js";
import { generateValidStartDate } from "../utils/time-util.js";
import { addAnime, removeEarliestAnime } from "../utils/cache-util.js";
import { titleMatches } from "../utils/common-util.js";
import { titleMatches, getExplicitSeasonNumber, extractSeasonNumberFromAnimeTitle } from "../utils/common-util.js";
import { SegmentListResponse } from '../models/dandan-model.js';
import {
buildHanjutvSearchHeaders,
@@ -661,7 +661,16 @@ export default class HanjutvSource extends BaseSource {
// ── 番剧处理 ─────────────────────────────────────────────────
async handleAnimes(sourceAnimes, queryTitle, curAnimes, extra = null, detailStore = null) {
/**
* 处理搜索结果
* @param {Array} sourceAnimes 原始数据
* @param {string} queryTitle 关键词
* @param {Array} curAnimes 结果池
* @param {any} extra 额外信息
* @param {Map} detailStore 详情缓存
* @param {number|null} querySeason 目标季度
*/
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null, querySeason = null) {
if (!Array.isArray(sourceAnimes)) {
log("error", "[Hanjutv] sourceAnimes is not a valid array");
return [];
@@ -669,10 +678,28 @@ export default class HanjutvSource extends BaseSource {
const tmpAnimes = [];
// 基础标题与季度匹配过滤
let filteredAnimes = sourceAnimes.filter(s => titleMatches(s.name, queryTitle, querySeason));
// 提取搜索词中的明确季度信息或使用传入的季度参数
const resolvedQuerySeason = querySeason !== null ? querySeason : getExplicitSeasonNumber(queryTitle);
// 初始列表预过滤机制:若用户指定了季度,优先检查结果中是否已包含匹配项
if (resolvedQuerySeason !== null) {
const seasonFiltered = filteredAnimes.filter(anime => {
const s = extractSeasonNumberFromAnimeTitle(anime.name).season;
return s === resolvedQuerySeason || (resolvedQuerySeason === 1 && s === null);
});
// 如果已命中目标,减少详情请求量
if (seasonFiltered.length > 0) {
filteredAnimes = seasonFiltered;
log("info", `[Hanjutv] 结果已命中目标季(第${resolvedQuerySeason}季),跳过非目标季相关请求`);
}
}
await Promise.all(
sourceAnimes
.filter(s => titleMatches(s.name, queryTitle))
.map(async (anime) => {
filteredAnimes.map(async (anime) => {
try {
const payload = await this.buildAnimePayload(anime);
if (!payload || !payload.summary || !Array.isArray(payload.links) || payload.links.length === 0) return;
+28 -10
View File
@@ -1,7 +1,7 @@
import BaseSource from './base.js';
import { log } from "../utils/log-util.js";
import { buildQueryString, httpGet} from "../utils/http-util.js";
import { printFirst200Chars, titleMatches } from "../utils/common-util.js";
import { printFirst200Chars, titleMatches, getExplicitSeasonNumber, extractSeasonNumberFromAnimeTitle } from "../utils/common-util.js";
import { md5, convertToAsciiSum, decodeHtmlEntities } from "../utils/codec-util.js";
import { generateValidStartDate } from "../utils/time-util.js";
import { addAnime, removeEarliestAnime } from "../utils/cache-util.js";
@@ -644,13 +644,14 @@ export default class IqiyiSource extends BaseSource {
}
/**
* 处理搜索结果并格式化为 DanDanPlay 格式
* @param {Array} sourceAnimes - 搜索结果数组
* @param {string} queryTitle - 搜索关键词
* @param {Array} curAnimes - 当前动漫列表
* @returns {Promise<void>}
* 处理搜索结果
* @param {Array} sourceAnimes 原始数据
* @param {string} queryTitle 关键词
* @param {Array} curAnimes 结果池
* @param {Map|null} detailStore 详情缓存
* @param {number|null} querySeason 目标季度
*/
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null) {
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null, querySeason = null) {
const tmpAnimes = [];
// 添加错误处理,确保sourceAnimes是数组
@@ -659,9 +660,26 @@ export default class IqiyiSource extends BaseSource {
return [];
}
const processIqiyiAnimes = await Promise.all(sourceAnimes
.filter(s => titleMatches(s.title, queryTitle))
.map(async (anime) => {
let filteredAnimes = sourceAnimes.filter(s => titleMatches(s.title, queryTitle, querySeason));
// 提取搜索词中的明确季度信息或使用传入的季度参数
const resolvedQuerySeason = querySeason !== null ? querySeason : getExplicitSeasonNumber(queryTitle);
// 初始列表预过滤机制:若用户指定了季度,优先检查初始结果中是否已包含匹配项
if (resolvedQuerySeason !== null) {
const seasonFiltered = filteredAnimes.filter(anime => {
const s = extractSeasonNumberFromAnimeTitle(anime.title).season;
return s === resolvedQuerySeason || (resolvedQuerySeason === 1 && s === null);
});
// 如果已命中目标,减少详情请求量
if (seasonFiltered.length > 0) {
filteredAnimes = seasonFiltered;
log("info", `[iQiyi] 结果已命中目标季(第${resolvedQuerySeason}季),跳过非目标季相关请求`);
}
}
const processIqiyiAnimes = await Promise.all(filteredAnimes.map(async (anime) => {
try {
const eps = await this.getEpisodes(anime.mediaId);
+31 -5
View File
@@ -4,7 +4,7 @@ import { log } from "../utils/log-util.js";
import { httpGet } from "../utils/http-util.js";
import { generateValidStartDate } from "../utils/time-util.js";
import { addAnime, removeEarliestAnime } from "../utils/cache-util.js";
import { titleMatches } from "../utils/common-util.js";
import { titleMatches, getExplicitSeasonNumber, extractSeasonNumberFromAnimeTitle } from "../utils/common-util.js";
// =====================
// 获取360看源播放链接
@@ -212,7 +212,15 @@ export default class Kan360Source extends BaseSource {
async getEpisodes(id) {}
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null) {
/**
* 处理搜索结果
* @param {Array} sourceAnimes 原始数据
* @param {string} queryTitle 关键词
* @param {Array} curAnimes 结果池
* @param {Map} detailStore 详情缓存
* @param {number|null} querySeason 目标季度
*/
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null, querySeason = null) {
const tmpAnimes = [];
// 添加错误处理,确保sourceAnimes是数组
@@ -221,9 +229,27 @@ export default class Kan360Source extends BaseSource {
return [];
}
const process360Animes = await Promise.all(sourceAnimes
.filter(anime => titleMatches(anime.titleTxt, queryTitle))
.map(async (anime) => {
// 基础标题与季度匹配过滤
let filteredAnimes = sourceAnimes.filter(anime => titleMatches(anime.titleTxt, queryTitle, querySeason));
// 提取搜索词中的明确季度信息或使用传入的季度参数
const resolvedQuerySeason = querySeason !== null ? querySeason : getExplicitSeasonNumber(queryTitle);
// 初始列表预过滤机制:若用户指定了季度,优先检查结果中是否已包含匹配项
if (resolvedQuerySeason !== null) {
const seasonFiltered = filteredAnimes.filter(anime => {
const s = extractSeasonNumberFromAnimeTitle(anime.titleTxt).season;
return s === resolvedQuerySeason || (resolvedQuerySeason === 1 && s === null);
});
// 如果已命中目标,减少详情请求量
if (seasonFiltered.length > 0) {
filteredAnimes = seasonFiltered;
log("info", `[360kan] 结果已命中目标季(第${resolvedQuerySeason}季),跳过非目标季相关请求`);
}
}
const process360Animes = await Promise.all(filteredAnimes.map(async (anime) => {
try {
let links = [];
if (anime.cat_name === "电影") {
+31 -5
View File
@@ -5,7 +5,7 @@ import { httpGet, buildQueryString } from "../utils/http-util.js";
import { convertToAsciiSum } from "../utils/codec-util.js";
import { generateValidStartDate } from "../utils/time-util.js";
import { addAnime, removeEarliestAnime } from "../utils/cache-util.js";
import { printFirst200Chars, titleMatches } from "../utils/common-util.js";
import { printFirst200Chars, titleMatches, getExplicitSeasonNumber, extractSeasonNumberFromAnimeTitle } from "../utils/common-util.js";
import { SegmentListResponse } from '../models/dandan-model.js';
// 媒体类型映射
@@ -494,7 +494,15 @@ export default class LeshiSource extends BaseSource {
return episodes;
}
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null) {
/**
* 处理搜索结果
* @param {Array} sourceAnimes 原始数据
* @param {string} queryTitle 关键词
* @param {Array} curAnimes 结果池
* @param {Map} detailStore 详情缓存
* @param {number|null} querySeason 目标季度
*/
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null, querySeason = null) {
const tmpAnimes = [];
// 添加错误处理,确保sourceAnimes是数组
@@ -503,10 +511,28 @@ export default class LeshiSource extends BaseSource {
return [];
}
// 基础标题与季度匹配过滤
let filteredAnimes = sourceAnimes.filter(s => titleMatches(s.title, queryTitle, querySeason));
// 提取搜索词中的明确季度信息或使用传入的季度参数
const resolvedQuerySeason = querySeason !== null ? querySeason : getExplicitSeasonNumber(queryTitle);
// 初始列表预过滤机制:若用户指定了季度,优先检查结果中是否已包含匹配项
if (resolvedQuerySeason !== null) {
const seasonFiltered = filteredAnimes.filter(anime => {
const s = extractSeasonNumberFromAnimeTitle(anime.title).season;
return s === resolvedQuerySeason || (resolvedQuerySeason === 1 && s === null);
});
// 如果已命中目标,减少详情请求量
if (seasonFiltered.length > 0) {
filteredAnimes = seasonFiltered;
log("info", `[Leshi] 结果已命中目标季(第${resolvedQuerySeason}季),跳过非目标季相关请求`);
}
}
// 使用 map 和 async 时需要返回 Promise 数组,并等待所有 Promise 完成
const processLeshiAnimes = await Promise.all(sourceAnimes
.filter(s => titleMatches(s.title, queryTitle))
.map(async (anime) => {
const processLeshiAnimes = await Promise.all(filteredAnimes.map(async (anime) => {
try {
const eps = await this.getEpisodes(anime.mediaId);
let links = [];
+31 -5
View File
@@ -6,7 +6,7 @@ import { convertToAsciiSum, md5 } from "../utils/codec-util.js";
import { hexToInt } from "../utils/danmu-util.js";
import { generateValidStartDate } from "../utils/time-util.js";
import { addAnime, removeEarliestAnime } from "../utils/cache-util.js";
import { printFirst200Chars, titleMatches } from "../utils/common-util.js";
import { printFirst200Chars, titleMatches, getExplicitSeasonNumber, extractSeasonNumberFromAnimeTitle } from "../utils/common-util.js";
import { SegmentListResponse } from '../models/dandan-model.js';
// =====================
@@ -194,7 +194,15 @@ class MaiduiduiSource extends BaseSource {
}
}
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null) {
/**
* 处理搜索结果
* @param {Array} sourceAnimes 原始数据
* @param {string} queryTitle 关键词
* @param {Array} curAnimes 结果池
* @param {Map} detailStore 详情缓存
* @param {number|null} querySeason 目标季度
*/
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null, querySeason = null) {
const tmpAnimes = [];
// 添加错误处理,确保sourceAnimes是数组
@@ -203,10 +211,28 @@ class MaiduiduiSource extends BaseSource {
return [];
}
// 基础标题与季度匹配过滤
let filteredAnimes = sourceAnimes.filter(s => titleMatches(s.name, queryTitle, querySeason));
// 提取搜索词中的明确季度信息或使用传入的季度参数
const resolvedQuerySeason = querySeason !== null ? querySeason : getExplicitSeasonNumber(queryTitle);
// 初始列表预过滤机制:若用户指定了季度,优先检查结果中是否已包含匹配项
if (resolvedQuerySeason !== null) {
const seasonFiltered = filteredAnimes.filter(anime => {
const s = extractSeasonNumberFromAnimeTitle(anime.name).season;
return s === resolvedQuerySeason || (resolvedQuerySeason === 1 && s === null);
});
// 如果已命中目标,减少详情请求量
if (seasonFiltered.length > 0) {
filteredAnimes = seasonFiltered;
log("info", `[Maiduidui] 结果已命中目标季(第${resolvedQuerySeason}季),跳过非目标季相关请求`);
}
}
// 使用 map 和 async 时需要返回 Promise 数组,并等待所有 Promise 完成
const processMaiduiduiAnimes = await Promise.all(sourceAnimes
.filter(s => titleMatches(s.name, queryTitle))
.map(async (anime) => {
const processMaiduiduiAnimes = await Promise.all(filteredAnimes.map(async (anime) => {
try {
const eps = await this.getEpisodes(anime.url);
let links = [];
+30 -5
View File
@@ -2,7 +2,7 @@ import BaseSource from './base.js';
import { globals } from '../configs/globals.js';
import { log } from "../utils/log-util.js";
import { httpGet} from "../utils/http-util.js";
import { printFirst200Chars, titleMatches } from "../utils/common-util.js";
import { printFirst200Chars, titleMatches, getExplicitSeasonNumber, extractSeasonNumberFromAnimeTitle } from "../utils/common-util.js";
import { time_to_second, generateValidStartDate } from "../utils/time-util.js";
import { rgbToInt } from "../utils/danmu-util.js";
import { convertToAsciiSum } from "../utils/codec-util.js";
@@ -407,7 +407,15 @@ export default class MangoSource extends BaseSource {
return episodeInfos;
}
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null) {
/**
* 处理搜索结果
* @param {Array} sourceAnimes 原始数据
* @param {string} queryTitle 关键词
* @param {Array} curAnimes 结果池
* @param {Map|null} detailStore 详情缓存
* @param {number|null} querySeason 目标季度
*/
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null, querySeason = null) {
const tmpAnimes = [];
// 添加错误处理,确保sourceAnimes是数组
@@ -416,9 +424,26 @@ export default class MangoSource extends BaseSource {
return [];
}
const processMangoAnimes = await Promise.all(sourceAnimes
.filter(s => titleMatches(s.title, queryTitle))
.map(async (anime) => {
let filteredAnimes = sourceAnimes.filter(s => titleMatches(s.title, queryTitle, querySeason));
// 提取搜索词中的明确季度信息或使用传入的季度参数
const resolvedQuerySeason = querySeason !== null ? querySeason : getExplicitSeasonNumber(queryTitle);
// 初始列表预过滤机制:若用户指定了季度,优先检查初始结果中是否已包含匹配项
if (resolvedQuerySeason !== null) {
const seasonFiltered = filteredAnimes.filter(anime => {
const s = extractSeasonNumberFromAnimeTitle(anime.title).season;
return s === resolvedQuerySeason || (resolvedQuerySeason === 1 && s === null);
});
// 如果已命中目标,减少详情请求量
if (seasonFiltered.length > 0) {
filteredAnimes = seasonFiltered;
log("info", `[Mango] 结果已命中目标季(第${resolvedQuerySeason}季),跳过非目标季相关请求`);
}
}
const processMangoAnimes = await Promise.all(filteredAnimes.map(async (anime) => {
try {
// 电影类型专门处理
if (anime.type === "电影") {
+32 -5
View File
@@ -7,7 +7,7 @@ import { hexToInt } from "../utils/danmu-util.js";
import { generateValidStartDate, time_to_second } from "../utils/time-util.js";
import { addAnime, removeEarliestAnime } from "../utils/cache-util.js";
import { decrypt } from "../utils/migu-util.js";
import { printFirst200Chars, titleMatches } from "../utils/common-util.js";
import { printFirst200Chars, titleMatches, getExplicitSeasonNumber, extractSeasonNumberFromAnimeTitle } from "../utils/common-util.js";
import { SegmentListResponse } from '../models/dandan-model.js';
// =====================
@@ -181,7 +181,15 @@ class MiguSource extends BaseSource {
}
}
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null) {
/**
* 处理搜索结果
* @param {Array} sourceAnimes 原始数据
* @param {string} queryTitle 关键词
* @param {Array} curAnimes 结果池
* @param {Map} detailStore 详情缓存
* @param {number|null} querySeason 目标季度
*/
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null, querySeason = null) {
const tmpAnimes = [];
// 添加错误处理,确保sourceAnimes是数组
@@ -190,10 +198,29 @@ class MiguSource extends BaseSource {
return [];
}
// 基础标题与季度匹配过滤
let filteredAnimes = sourceAnimes.filter(s => titleMatches(s.name || s.title, queryTitle, querySeason));
// 提取搜索词中的明确季度信息或使用传入的季度参数
const resolvedQuerySeason = querySeason !== null ? querySeason : getExplicitSeasonNumber(queryTitle);
// 初始列表预过滤机制:若用户指定了季度,优先检查结果中是否已包含匹配项
if (resolvedQuerySeason !== null) {
const seasonFiltered = filteredAnimes.filter(anime => {
const titleToCheck = anime.name || anime.title;
const s = extractSeasonNumberFromAnimeTitle(titleToCheck).season;
return s === resolvedQuerySeason || (resolvedQuerySeason === 1 && s === null);
});
// 如果已命中目标,减少详情请求量
if (seasonFiltered.length > 0) {
filteredAnimes = seasonFiltered;
log("info", `[Migu] 结果已命中目标季(第${resolvedQuerySeason}季),跳过非目标季相关请求`);
}
}
// 使用 map 和 async 时需要返回 Promise 数组,并等待所有 Promise 完成
const processMiguAnimes = await Promise.all(sourceAnimes
.filter(s => titleMatches(s.name || s.title, queryTitle))
.map(async (anime) => {
const processMiguAnimes = await Promise.all(filteredAnimes.map(async (anime) => {
try {
const eps = await this.getEpisodes(anime.url || anime.mediaId);
let links = [];
File diff suppressed because it is too large. Load diff
+31 -5
View File
@@ -5,7 +5,7 @@ import { httpGet, buildQueryString } from "../utils/http-util.js";
import { convertToAsciiSum } from "../utils/codec-util.js";
import { generateValidStartDate } from "../utils/time-util.js";
import { addAnime, removeEarliestAnime } from "../utils/cache-util.js";
import { printFirst200Chars, titleMatches } from "../utils/common-util.js";
import { printFirst200Chars, titleMatches, getExplicitSeasonNumber, extractSeasonNumberFromAnimeTitle } from "../utils/common-util.js";
import { SegmentListResponse } from '../models/dandan-model.js';
// =====================
@@ -240,7 +240,15 @@ export default class SohuSource extends BaseSource {
}
}
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null) {
/**
* 处理搜索结果
* @param {Array} sourceAnimes 原始数据
* @param {string} queryTitle 关键词
* @param {Array} curAnimes 结果池
* @param {Map|null} detailStore 详情缓存
* @param {number|null} querySeason 目标季度
*/
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null, querySeason = null) {
const tmpAnimes = [];
// 添加错误处理,确保sourceAnimes是数组
@@ -249,10 +257,28 @@ export default class SohuSource extends BaseSource {
return [];
}
// 基础标题与季度匹配过滤
let filteredAnimes = sourceAnimes.filter(s => titleMatches(s.title, queryTitle, querySeason));
// 提取搜索词中的明确季度信息或使用传入的季度参数
const resolvedQuerySeason = querySeason !== null ? querySeason : getExplicitSeasonNumber(queryTitle);
// 初始列表预过滤机制:若用户指定了季度,优先检查结果中是否已包含匹配项
if (resolvedQuerySeason !== null) {
const seasonFiltered = filteredAnimes.filter(anime => {
const s = extractSeasonNumberFromAnimeTitle(anime.title).season;
return s === resolvedQuerySeason || (resolvedQuerySeason === 1 && s === null);
});
// 如果已命中目标,减少详情请求量
if (seasonFiltered.length > 0) {
filteredAnimes = seasonFiltered;
log("info", `[Sohu] 结果已命中目标季(第${resolvedQuerySeason}季),跳过非目标季相关请求`);
}
}
// 使用 map 和 async 时需要返回 Promise 数组,并等待所有 Promise 完成
const processSohuAnimes = await Promise.all(sourceAnimes
.filter(s => titleMatches(s.title, queryTitle))
.map(async (anime) => {
const processSohuAnimes = await Promise.all(filteredAnimes.map(async (anime) => {
try {
const eps = await this.getEpisodes(anime.mediaId);
let links = [];
+34 -8
View File
@@ -5,7 +5,7 @@ import { httpGet, httpPost } from "../utils/http-util.js";
import { convertToAsciiSum } from "../utils/codec-util.js";
import { generateValidStartDate } from "../utils/time-util.js";
import { addAnime, removeEarliestAnime } from "../utils/cache-util.js";
import { printFirst200Chars, titleMatches } from "../utils/common-util.js";
import { printFirst200Chars, titleMatches, getExplicitSeasonNumber, extractSeasonNumberFromAnimeTitle } from "../utils/common-util.js";
import { SegmentListResponse } from '../models/dandan-model.js';
// =====================
@@ -31,9 +31,9 @@ export default class TencentSource extends BaseSource {
return aliases;
}
titleOrAliasMatches(anime, queryTitle) {
if (titleMatches(anime.title, queryTitle)) return true;
return Array.isArray(anime.aliases) && anime.aliases.some(alias => titleMatches(alias, queryTitle));
titleOrAliasMatches(anime, queryTitle, querySeason = null) {
if (titleMatches(anime.title, queryTitle, querySeason)) return true;
return Array.isArray(anime.aliases) && anime.aliases.some(alias => titleMatches(alias, queryTitle, querySeason));
}
/**
@@ -415,7 +415,15 @@ export default class TencentSource extends BaseSource {
}
}
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null) {
/**
* 处理搜索结果
* @param {Array} sourceAnimes 原始数据
* @param {string} queryTitle 关键词
* @param {Array} curAnimes 结果池
* @param {Map|null} detailStore 详情缓存
* @param {number|null} querySeason 目标季度
*/
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null, querySeason = null) {
const tmpAnimes = [];
// 添加错误处理,确保sourceAnimes是数组
@@ -424,10 +432,28 @@ export default class TencentSource extends BaseSource {
return [];
}
// 基础标题、别名与季度匹配过滤
let filteredAnimes = sourceAnimes.filter(s => this.titleOrAliasMatches(s, queryTitle, querySeason));
// 提取搜索词中的明确季度信息或使用传入的季度参数
const resolvedQuerySeason = querySeason !== null ? querySeason : getExplicitSeasonNumber(queryTitle);
// 初始列表预过滤机制:若用户指定了季度,优先检查结果中是否已包含匹配项
if (resolvedQuerySeason !== null) {
const seasonFiltered = filteredAnimes.filter(anime => {
const s = extractSeasonNumberFromAnimeTitle(anime.title).season;
return s === resolvedQuerySeason || (resolvedQuerySeason === 1 && s === null);
});
// 如果已命中目标,减少详情请求量
if (seasonFiltered.length > 0) {
filteredAnimes = seasonFiltered;
log("info", `[Tencent] 结果已命中目标季(第${resolvedQuerySeason}季),跳过非目标季相关请求`);
}
}
// 使用 map 和 async 时需要返回 Promise 数组,并等待所有 Promise 完成
const processTencentAnimes = await Promise.all(sourceAnimes
.filter(s => this.titleOrAliasMatches(s, queryTitle))
.map(async (anime) => {
const processTencentAnimes = await Promise.all(filteredAnimes.map(async (anime) => {
try {
const eps = await this.getEpisodes(anime.mediaId);
let links = [];
+11 -3
View File
@@ -118,11 +118,19 @@ export default class TmdbSource extends BaseSource {
async getEpisodes(id) {}
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null) {
return this.doubanSource.handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore);
/**
* 转发搜索结果处理至豆瓣源
* @param {Array} sourceAnimes 原始数据
* @param {string} queryTitle 关键词
* @param {Array} curAnimes 结果池
* @param {Map|null} detailStore 详情缓存
* @param {number|null} querySeason 目标季度
*/
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null, querySeason = null) {
return this.doubanSource.handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore, querySeason);
}
async getEpisodeDanmu(id) {}
formatComments(comments) {}
}
}
+32 -5
View File
@@ -4,7 +4,7 @@ import { log } from "../utils/log-util.js";
import { httpGet } from "../utils/http-util.js";
import { generateValidStartDate } from "../utils/time-util.js";
import { addAnime, removeEarliestAnime } from "../utils/cache-util.js";
import { printFirst200Chars, titleMatches, sanitizeSearchKeyword } from "../utils/common-util.js";
import { printFirst200Chars, titleMatches, sanitizeSearchKeyword, getExplicitSeasonNumber, extractSeasonNumberFromAnimeTitle } from "../utils/common-util.js";
// =====================
// 获取vod源播放链接
@@ -143,7 +143,16 @@ export default class VodSource extends BaseSource {
async getEpisodes(id) {}
async handleAnimes(sourceAnimes, queryTitle, curAnimes, vodName, detailStore = null) {
/**
* 处理搜索结果
* @param {Array} sourceAnimes 原始数据
* @param {string} queryTitle 关键词
* @param {Array} curAnimes 结果池
* @param {string} vodName VOD资源站名称
* @param {Map|null} detailStore 详情缓存
* @param {number|null} querySeason 目标季度
*/
async handleAnimes(sourceAnimes, queryTitle, curAnimes, vodName, detailStore = null, querySeason = null) {
const tmpAnimes = [];
// 添加错误处理,确保sourceAnimes是数组
@@ -152,9 +161,27 @@ export default class VodSource extends BaseSource {
return [];
}
const processVodAnimes = await Promise.all(sourceAnimes
.filter(anime => titleMatches(anime.vod_name, queryTitle))
.map(async (anime) => {
// 基础标题与季度匹配过滤
let filteredAnimes = sourceAnimes.filter(anime => titleMatches(anime.vod_name, queryTitle, querySeason));
// 提取搜索词中的明确季度信息或使用传入的季度参数
const resolvedQuerySeason = querySeason !== null ? querySeason : getExplicitSeasonNumber(queryTitle);
// 初始列表预过滤机制:若用户指定了季度,优先检查结果中是否已包含匹配项
if (resolvedQuerySeason !== null) {
const seasonFiltered = filteredAnimes.filter(anime => {
const s = extractSeasonNumberFromAnimeTitle(anime.vod_name).season;
return s === resolvedQuerySeason || (resolvedQuerySeason === 1 && s === null);
});
// 如果已命中目标,减少详情请求量
if (seasonFiltered.length > 0) {
filteredAnimes = seasonFiltered;
log("info", `[VOD] 结果已命中目标季(第${resolvedQuerySeason}季),跳过非目标季相关请求`);
}
}
const processVodAnimes = await Promise.all(filteredAnimes.map(async (anime) => {
try {
let vodPlayFromList = anime.vod_play_from.split("$$$");
vodPlayFromList = vodPlayFromList.map(item => {
+31 -5
View File
@@ -5,7 +5,7 @@ import { httpGet, buildQueryString } from "../utils/http-util.js";
import { convertToAsciiSum } from "../utils/codec-util.js";
import { generateValidStartDate } from "../utils/time-util.js";
import { addAnime, removeEarliestAnime } from "../utils/cache-util.js";
import { printFirst200Chars, titleMatches } from "../utils/common-util.js";
import { printFirst200Chars, titleMatches, getExplicitSeasonNumber, extractSeasonNumberFromAnimeTitle } from "../utils/common-util.js";
import { SegmentListResponse } from '../models/dandan-model.js';
// =====================
@@ -181,7 +181,15 @@ class XiguaSource extends BaseSource {
}
}
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null) {
/**
* 处理搜索结果
* @param {Array} sourceAnimes 原始数据
* @param {string} queryTitle 关键词
* @param {Array} curAnimes 结果池
* @param {Map|null} detailStore 详情缓存
* @param {number|null} querySeason 目标季度
*/
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null, querySeason = null) {
const tmpAnimes = [];
// 添加错误处理,确保sourceAnimes是数组
@@ -190,10 +198,28 @@ class XiguaSource extends BaseSource {
return [];
}
// 基础标题与季度匹配过滤
let filteredAnimes = sourceAnimes.filter(s => titleMatches(s.name, queryTitle, querySeason));
// 提取搜索词中的明确季度信息或使用传入的季度参数
const resolvedQuerySeason = querySeason !== null ? querySeason : getExplicitSeasonNumber(queryTitle);
// 初始列表预过滤机制:若用户指定了季度,优先检查结果中是否已包含匹配项
if (resolvedQuerySeason !== null) {
const seasonFiltered = filteredAnimes.filter(anime => {
const s = extractSeasonNumberFromAnimeTitle(anime.name).season;
return s === resolvedQuerySeason || (resolvedQuerySeason === 1 && s === null);
});
// 如果已命中目标,减少详情请求量
if (seasonFiltered.length > 0) {
filteredAnimes = seasonFiltered;
log("info", `[Xigua] 结果已命中目标季(第${resolvedQuerySeason}季),跳过非目标季相关请求`);
}
}
// 使用 map 和 async 时需要返回 Promise 数组,并等待所有 Promise 完成
const processXiguaAnimes = await Promise.all(sourceAnimes
.filter(s => titleMatches(s.name, queryTitle))
.map(async (anime) => {
const processXiguaAnimes = await Promise.all(filteredAnimes.map(async (anime) => {
try {
const albumId = anime.url.split('/').pop();
const eps = await this.getEpisodes(albumId);
+31 -5
View File
@@ -2,7 +2,7 @@ import BaseSource from './base.js';
import { globals } from '../configs/globals.js';
import { log } from "../utils/log-util.js";
import { buildQueryString, httpGet, httpPost } from "../utils/http-util.js";
import { printFirst200Chars, titleMatches } from "../utils/common-util.js";
import { printFirst200Chars, titleMatches, getExplicitSeasonNumber, extractSeasonNumberFromAnimeTitle } from "../utils/common-util.js";
import { md5, convertToAsciiSum } from "../utils/codec-util.js";
import { generateValidStartDate } from "../utils/time-util.js";
import { addAnime, removeEarliestAnime } from "../utils/cache-util.js";
@@ -205,7 +205,15 @@ export default class YoukuSource extends BaseSource {
return data;
}
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null) {
/**
* 处理搜索结果
* @param {Array} sourceAnimes 原始数据
* @param {string} queryTitle 关键词
* @param {Array} curAnimes 结果池
* @param {Map|null} detailStore 详情缓存
* @param {number|null} querySeason 目标季度
*/
async handleAnimes(sourceAnimes, queryTitle, curAnimes, detailStore = null, querySeason = null) {
const tmpAnimes = [];
// 添加错误处理,确保sourceAnimes是数组
@@ -214,9 +222,27 @@ export default class YoukuSource extends BaseSource {
return [];
}
const processYoukuAnimes = await Promise.all(sourceAnimes
.filter(s => titleMatches(s.title, queryTitle))
.map(async (anime) => {
// 基础标题与季度匹配过滤
let filteredAnimes = sourceAnimes.filter(s => titleMatches(s.title, queryTitle, querySeason));
// 提取搜索词中的明确季度信息或使用传入的季度参数
const resolvedQuerySeason = querySeason !== null ? querySeason : getExplicitSeasonNumber(queryTitle);
// 初始列表预过滤机制:若用户指定了季度,优先检查结果中是否已包含匹配项
if (resolvedQuerySeason !== null) {
const seasonFiltered = filteredAnimes.filter(anime => {
const s = extractSeasonNumberFromAnimeTitle(anime.title).season;
return s === resolvedQuerySeason || (resolvedQuerySeason === 1 && s === null);
});
// 如果已命中目标,减少详情请求量
if (seasonFiltered.length > 0) {
filteredAnimes = seasonFiltered;
log("info", `[Youku] 结果已命中目标季(第${resolvedQuerySeason}季),跳过非目标季相关请求`);
}
}
const processYoukuAnimes = await Promise.all(filteredAnimes.map(async (anime) => {
try {
const eps = await this.getEpisodes(anime.mediaId);
+3 -2
View File
@@ -260,9 +260,10 @@ export function getExplicitSeasonNumber(text) {
* 标题匹配路由函数:支持严格模式,或 宽松模式下的"包含+相似度"混合策略
* @param {string} title - 动漫标题
* @param {string} query - 搜索关键词
* @param {number|null} parsedSeason - 解析出的目标季度
* @returns {boolean} 是否匹配
*/
export function titleMatches(title, query) {
export function titleMatches(title, query, parsedSeason = null) {
// 策略1:严格模式仅允许头部或完全匹配
if (globals.strictTitleMatch) return strictTitleMatch(title, query);
@@ -281,7 +282,7 @@ export function titleMatches(title, query) {
if (qList.some(kw => t.includes(kw))) return true;
// 季度特征校验 (针对策略3的宽松相似度,防止字符集混淆导致季度错乱)
const querySeason = getExplicitSeasonNumber(query);
const querySeason = parsedSeason !== null ? parsedSeason : getExplicitSeasonNumber(query);
if (querySeason !== null) {
const titleSeason = getExplicitSeasonNumber(title);
+8 -4
View File
@@ -25,6 +25,8 @@ function linkSignal(externalSignal, internalController) {
export async function httpGet(url, options = {}) {
// 从 options 中获取重试次数,默认为 0
const maxRetries = parseInt(options.retries || '0', 10) || 0;
// 提取允许放行的特定状态码白名单
const validStatusCodes = Array.isArray(options.validStatusCodes) ? options.validStatusCodes : [];
let lastError;
// 执行请求,包含重试逻辑
@@ -71,7 +73,8 @@ export async function httpGet(url, options = {}) {
clearTimeout(timeoutId);
if (!response.ok) {
// 非 2xx 且不在白名单内的状态码抛出异常
if (!response.ok && !validStatusCodes.includes(response.status)) {
throw new Error(`HTTP error! status: ${response.status}`);
}
@@ -216,6 +219,7 @@ export async function httpGet(url, options = {}) {
export async function httpPost(url, body, options = {}) {
// 从 options 中获取重试次数,默认为 0
const maxRetries = parseInt(options.retries || '0', 10) || 0;
const validStatusCodes = Array.isArray(options.validStatusCodes) ? options.validStatusCodes : [];
let lastError;
// 执行请求,包含重试逻辑
@@ -267,8 +271,7 @@ export async function httpPost(url, body, options = {}) {
const data = await response.text();
if (!response.ok) {
if (!response.ok && !validStatusCodes.includes(response.status)) {
log("error", `[请求模拟] response data: `, data);
throw new Error(`HTTP error! status: ${response.status}`);
}
@@ -349,6 +352,7 @@ async function httpRequestMethod(method, url, body, options = {}) {
log("info", `[请求模拟] HTTP ${method}: ${url}`);
const { headers = {} } = options;
const validStatusCodes = Array.isArray(options.validStatusCodes) ? options.validStatusCodes : [];
const fetchOptions = {
method,
@@ -373,7 +377,7 @@ async function httpRequestMethod(method, url, body, options = {}) {
const response = await fetch(url, fetchOptions);
const textData = await response.text();
if (!response.ok) {
if (!response.ok && !validStatusCodes.includes(response.status)) {
log("error", `[请求模拟] response data: `, textData);
throw new Error(`HTTP error! status: ${response.status}`);
}
+28 -4
View File
@@ -1780,7 +1780,7 @@ function fastCloneAnime(anime) {
/**
* 执行单个主源的合并任务
* 包含:寻找匹配、ID生成、链接映射、集数补全、跨源合集时序接管、共识差精准对齐、番外专项制导
* 包含:寻找匹配、ID生成、链接映射、集数补全、跨源合集时序接管、共识差精准对齐、番外专项制导、原子化元数据合并
* @param {Object} params - 任务参数对象
* @returns {Promise<Anime|null>} 合并后的新对象或 null
*/
@@ -1797,14 +1797,20 @@ async function processMergeTask(params) {
return null;
}
const cachedPAnime = globals.animes.find(a => String(a.animeId) === String(pAnime.animeId));
if (!cachedPAnime?.links) {
// 以传入的最新对象为元数据基准进行克隆,确保包含数据流传递过程中的全量最新元数据
let derivedAnime = fastCloneAnime(pAnime);
if (!derivedAnime.links) {
const cachedPAnime = globals.animes.find(a => String(a.animeId) === String(pAnime.animeId));
derivedAnime.links = cachedPAnime?.links ? fastCloneAnime(cachedPAnime.links) : [];
}
if (!derivedAnime.links || derivedAnime.links.length === 0) {
log("warn", `${logPrefix} 主源数据不完整,跳过: ${pAnime.animeTitle}`);
return null;
}
const logTitleA = pAnime.animeTitle.replace(RegexStore.Clean.FROM_SUFFIX, '');
let derivedAnime = fastCloneAnime(cachedPAnime);
const actualMergedSources = [];
const contentSignatureParts = [pAnime.animeId];
@@ -2168,6 +2174,24 @@ async function processMergeTask(params) {
link.url = mutation.newUrl;
link.title = mutation.newTitle;
}
// 处理别名
const getBaseTitle = (t) => {
if (!t) return '';
return t.replace(RegexStore.Clean.YEAR_TAG, '')
.replace(RegexStore.Clean.SOURCE_TAG, '')
.replace(RegexStore.Clean.FROM_SUFFIX, '')
.trim();
};
const currentPrimaryBase = getBaseTitle(derivedAnime.animeTitle);
derivedAnime.aliases = [...new Set([
...(derivedAnime.aliases || []),
...(derivedMatch.aliases || []),
...(match.aliases || []),
getBaseTitle(derivedMatch.animeTitle)
])].filter(alias => alias && alias !== currentPrimaryBase);
log("info", `${logPrefix} 关联成功: [${currentPrimarySource}] ${logTitleA} <-> [${secSource}] ${logTitleB} (本次合并 ${mergedCount} 集)`);
if (mappingEntries.length > 0) {
mappingEntries.sort((a, b) => a.idx - b.idx);
+42
View File
@@ -40,6 +40,7 @@ import { addAnime, addEpisode } from "./utils/cache-util.js";
import { convertToAsciiSum } from "./utils/codec-util.js";
import { handleDanmusLike } from "./utils/danmu-util.js";
import { Segment, SegmentListResponse } from "./models/dandan-model.js"
import { initBangumiData, searchBangumiData, clearBangumiDataCache } from "./utils/bangumi-data-util.js";
// Mock Request class for testing
class MockRequest {
@@ -1559,6 +1560,47 @@ test('worker.js API endpoints', async (t) => {
// assert(res, `Expected res is true, but got ${res}`);
// });
// // 测试 Bangumi Data 本地检索功能与数据结构解析
// await t.test('searchBangumiData', async () => {
// const originalUseBangumiData = Globals.getConfig().useBangumiData;
// Globals.getConfig().useBangumiData = true;
// try {
// // 确保 Bangumi Data 核心数据源加载至内存
// await initBangumiData('node', true);
// const keyword = '间谍过家家';
// const targetSites = ['gamer', 'gamer_hk'];
// // 执行本地内存级检索
// const results = await searchBangumiData(keyword, targetSites);
// assert(Array.isArray(results), `Expected Array.isArray(results) to be true, but got ${typeof results}`);
// assert(results.length > 0, `Expected results.length > 0, but got ${results.length}`);
// if (results.length > 0) {
// assert(results[0].title !== undefined, `Expected results[0].title !== undefined`);
// assert(results[0].siteId !== undefined, `Expected results[0].siteId !== undefined`);
// }
// } finally {
// clearBangumiDataCache();
// Globals.getConfig().useBangumiData = originalUseBangumiData;
// }
// });
// // 测试带有季度参数的精确拦截与检索机制
// await t.test('searchAnimeWithSeason', async () => {
// const config = Globals.getConfig();
// const originalSourceOrderArr = Array.isArray(config.sourceOrderArr) ? [...config.sourceOrderArr] : config.sourceOrderArr;
// config.sourceOrderArr = ['360','iqiyi','dandan','animeko'];
// try {
// // 构造带有 season 参数的 URL 请求对象以模拟 match 接口的内部下发
// const targetUrl = new URL('http://localhost/search/anime?keyword=间谍过家家&season=2');
// const response = await searchAnime(targetUrl);
// const data = await parseResponse(response);
// assert.equal(data.success, true);
// assert(Array.isArray(data.animes), `Expected Array.isArray(data.animes) to be true`);
// assert(data.animes.length > 0, `Expected data.animes.length > 0, but got ${data.animes.length}`);
// } finally {
// config.sourceOrderArr = originalSourceOrderArr;
// }
// });
});
// // 测试本地 Redis 功能