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:
1 parent
8e3d208c2f
commit
ffe4c68293
26 files changed
+1606
-490
No files matched your search
@@ -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}`);
|
||||
|
||||
@@ -13,7 +13,7 @@ export const Globals = {
|
||||
accessedEnvVars: {},
|
||||
|
||||
// 静态常量
|
||||
VERSION: '1.19.4',
|
||||
VERSION: '1.19.5',
|
||||
MAX_LOGS: 1000, // 日志存储,最多保存 1000 行
|
||||
MAX_RECORDS: 100, // 请求记录最大数量
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
|
||||
@@ -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}季),跳过非目标季相关请求`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
|
||||
// 丢弃不符合拦截策略的条目,停止后续构建流程
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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 === "电影") {
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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 === "电影") {
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
+897
-313
File diff suppressed because it is too large.
Load diff
@@ -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 = [];
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 功能
|
||||
|
||||
Reference in new issue
Block a user