修复剧集搜索详情丢失 (#242)

变更内容:为 searchEpisodes 增加请求级详情缓存,避免广义搜索结果被全局 100 条上限裁剪后丢失

变更内容:扩展 searchAnime 的搜索缓存结构,在命中缓存时同步回填详情数据

变更内容:抽取 bangumi 详情构建逻辑并复用,保持 searchAnime 和 searchEpisodes 返回结果一致
This commit is contained in:
Celestials316 authored and GitHub committed 2026-03-18 23:39:06 +08:00
1 parent faa570273f
commit 7cff1c991b
3 files changed
+111 -47

No files matched your search

+85 -44
View File
@@ -232,18 +232,28 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null
queryTitle = simplifiedTitle;
}
// 检查搜索缓存
const cachedResults = getSearchCache(queryTitle);
if (cachedResults !== null) {
return jsonResponse({
errorCode: 0,
success: true,
errorMessage: "",
animes: cachedResults,
});
const previousRequestAnimeDetailsMap = globals.requestAnimeDetailsMap;
const requestAnimeDetailsMap = previousRequestAnimeDetailsMap instanceof Map
? previousRequestAnimeDetailsMap
: new Map();
if (!(previousRequestAnimeDetailsMap instanceof Map)) {
globals.requestAnimeDetailsMap = requestAnimeDetailsMap;
}
const curAnimes = [];
try {
// 检查搜索缓存
const cachedResults = getSearchCache(queryTitle, requestAnimeDetailsMap);
if (cachedResults !== null) {
return jsonResponse({
errorCode: 0,
success: true,
errorMessage: "",
animes: cachedResults,
});
}
const curAnimes = [];
// 链接弹幕解析
const urlRegex = /^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,6}(:\d+)?(\/[^\s]*)?$/;
@@ -479,29 +489,34 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null
curAnimes.push(...validAnimes);
}
// 如果有新的anime获取到,则更新本地缓存
if (globals.localCacheValid && curAnimes.length !== 0) {
await updateLocalCaches();
}
// 如果有新的anime获取到,则更新redis
if (globals.redisValid && curAnimes.length !== 0) {
await updateRedisCaches();
}
if (globals.localRedisValid && curAnimes.length !== 0) {
await updateLocalRedisCaches();
}
// 如果有新的anime获取到,则更新本地缓存
if (globals.localCacheValid && curAnimes.length !== 0) {
await updateLocalCaches();
}
// 如果有新的anime获取到,则更新redis
if (globals.redisValid && curAnimes.length !== 0) {
await updateRedisCaches();
}
if (globals.localRedisValid && curAnimes.length !== 0) {
await updateLocalRedisCaches();
}
// 缓存搜索结果
if (curAnimes.length > 0) {
setSearchCache(queryTitle, curAnimes);
}
// 缓存搜索结果
if (curAnimes.length > 0) {
setSearchCache(queryTitle, curAnimes, requestAnimeDetailsMap);
}
return jsonResponse({
errorCode: 0,
success: true,
errorMessage: "",
animes: curAnimes,
});
return jsonResponse({
errorCode: 0,
success: true,
errorMessage: "",
animes: curAnimes,
});
} finally {
if (!(previousRequestAnimeDetailsMap instanceof Map)) {
globals.requestAnimeDetailsMap = previousRequestAnimeDetailsMap;
}
}
}
function filterSameEpisodeTitle(filteredTmpEpisodes) {
@@ -1145,8 +1160,17 @@ export async function searchEpisodes(url) {
// 先搜索动漫
let searchUrl = new URL(`/search/anime?keyword=${anime}`, url.origin);
const searchRes = await searchAnime(searchUrl);
const searchData = await searchRes.json();
const previousRequestAnimeDetailsMap = globals.requestAnimeDetailsMap;
const requestAnimeDetailsMap = new Map();
globals.requestAnimeDetailsMap = requestAnimeDetailsMap;
let searchData;
try {
const searchRes = await searchAnime(searchUrl);
searchData = await searchRes.json();
} finally {
globals.requestAnimeDetailsMap = previousRequestAnimeDetailsMap;
}
if (!searchData.success || !searchData.animes || searchData.animes.length === 0) {
log("info", "No anime found for the given title");
@@ -1163,9 +1187,20 @@ export async function searchEpisodes(url) {
// 遍历所有找到的动漫,获取它们的集数信息
for (const animeItem of searchData.animes) {
const bangumiUrl = new URL(`/bangumi/${animeItem.bangumiId}`, url.origin);
const bangumiRes = await getBangumi(bangumiUrl.pathname);
const bangumiData = await bangumiRes.json();
const detailAnime =
requestAnimeDetailsMap.get(String(animeItem.bangumiId)) ||
requestAnimeDetailsMap.get(String(animeItem.animeId)) ||
globals.animes.find(a => String(a.bangumiId) === String(animeItem.bangumiId)) ||
globals.animes.find(a => String(a.animeId) === String(animeItem.animeId));
let bangumiData = null;
if (detailAnime) {
bangumiData = buildBangumiData(detailAnime, animeItem.bangumiId);
} else {
const bangumiUrl = new URL(`/bangumi/${animeItem.bangumiId}`, url.origin);
const bangumiRes = await getBangumi(bangumiUrl.pathname);
bangumiData = await bangumiRes.json();
}
if (bangumiData.success && bangumiData.bangumi && bangumiData.bangumi.episodes) {
let filteredEpisodes = bangumiData.bangumi.episodes;
@@ -1241,7 +1276,11 @@ export async function getBangumi(path) {
404
);
}
log("info", `Fetched details for anime ID: ${idParam}`);
return jsonResponse(buildBangumiData(anime, idParam));
}
function buildBangumiData(anime, idParam = "") {
log("info", `Fetched details for anime ID: ${idParam || anime.bangumiId}`);
// 构建 episodes 列表
let episodesList = [];
@@ -1265,11 +1304,13 @@ export async function getBangumi(path) {
// 如果过滤后没有有效剧集,返回错误
if (episodesList.length === 0) {
log("warn", `[getBangumi] No valid episodes after filtering for anime ID ${idParam}`);
return jsonResponse(
{ errorCode: 404, success: false, errorMessage: "No valid episodes after filtering", bangumi: null },
404
);
log("warn", `[getBangumi] No valid episodes after filtering for anime ID ${idParam || anime.bangumiId}`);
return {
errorCode: 404,
success: false,
errorMessage: "No valid episodes after filtering",
bangumi: null
};
}
// 重新排序episodeNumber
@@ -1301,12 +1342,12 @@ export async function getBangumi(path) {
episodes: episodesList,
});
return jsonResponse({
return {
errorCode: 0,
success: true,
errorMessage: "",
bangumi: bangumi
});
};
}
/**
+1
View File
@@ -24,6 +24,7 @@ export const Globals = {
episodeNum: 10001, // 全局变量,用于自增 ID
logBuffer: [],
requestHistory: new Map(), // 记录每个 IP 地址的请求历史
requestAnimeDetailsMap: null, // 当前请求内的详情缓存,避免搜索结果被全局上限裁剪后丢失
localCacheValid: false, // 本地缓存是否生效
localCacheInitialized: false, // 本地缓存是否已初始化
redisValid: false, // redis是否生效
+25 -3
View File
@@ -48,18 +48,34 @@ export function isSearchCacheValid(keyword) {
}
// 获取搜索缓存
export function getSearchCache(keyword) {
export function getSearchCache(keyword, detailsMap = null) {
if (isSearchCacheValid(keyword)) {
log("info", `Using search cache for "${keyword}"`);
return globals.searchCache.get(keyword).results;
const cached = globals.searchCache.get(keyword);
if (detailsMap instanceof Map && Array.isArray(cached.details)) {
cached.details.forEach(anime => {
detailsMap.set(String(anime.bangumiId), anime);
detailsMap.set(String(anime.animeId), anime);
});
}
return cached.results;
}
return null;
}
// 设置搜索缓存
export function setSearchCache(keyword, results) {
export function setSearchCache(keyword, results, detailsMap = null) {
const details = detailsMap instanceof Map
? Array.from(new Map(
Array.from(detailsMap.values()).map(anime => [String(anime.bangumiId || anime.animeId), anime])
).values())
: [];
globals.searchCache.set(keyword, {
results: results,
details: details,
timestamp: Date.now()
});
@@ -213,6 +229,12 @@ export function addAnime(anime) {
// 创建新的 anime 副本
const animeCopy = Anime.fromJson({ ...anime, links: newLinks });
// 当前请求内额外保留一份详情,避免被全局数量上限裁剪后丢失
if (globals.requestAnimeDetailsMap instanceof Map) {
globals.requestAnimeDetailsMap.set(String(animeCopy.bangumiId), animeCopy);
globals.requestAnimeDetailsMap.set(String(animeCopy.animeId), animeCopy);
}
// 检查是否已存在相同 animeId 的 anime
const existingAnimeIndex = globals.animes.findIndex(a => a.animeId === anime.animeId);