fix: 修复特殊字符标题搜索匹配失败 (#337)

* fix: 修复特殊字符标题搜索匹配失败

改动详情:

- 使用 URL.searchParams 构造内部 /search/anime 搜索地址
- 避免标题中的 & 等特殊字符被当作查询参数分隔符截断
- 补充回归测试,验证 keyword、season、episode 参数保持正确

* refactor: 简化搜索地址构造调用
This commit is contained in:
Wo254992 authored and GitHub committed 2026-06-06 12:18:25 +08:00
1 parent aad97a0f88
commit 81a320177e
2 files changed
+37 -3

No files matched your search

+17 -2
View File
@@ -1402,6 +1402,21 @@ export async function extractTitleSeasonEpisode(cleanFileName) {
return {title, season, episode, year};
}
export function buildSearchAnimeUrl(baseUrl, keyword, season, episode) {
const searchUrl = new URL(baseUrl);
const apiPrefix = searchUrl.pathname.replace(/\/(?:match|search\/episodes)$/, '');
searchUrl.pathname = `${apiPrefix}/search/anime`;
searchUrl.search = '';
searchUrl.searchParams.set('keyword', keyword || '');
if (season !== undefined) {
searchUrl.searchParams.set('season', season || '');
}
if (episode !== undefined) {
searchUrl.searchParams.set('episode', episode || '');
}
return searchUrl;
}
// Extracted function for POST /api/v2/match
export async function matchAnime(url, req, clientIp) {
try {
@@ -1460,7 +1475,7 @@ export async function matchAnime(url, req, clientIp) {
const targetPlatform = dynamicPlatformOrder.length > 0 ? dynamicPlatformOrder[0] : null;
const requestAnimeDetailsMap = new Map();
let originSearchUrl = new URL(req.url.replace("/match", `/search/anime?keyword=${title}&season=${season || ''}&episode=${episode || ''}`));
let originSearchUrl = buildSearchAnimeUrl(req.url, title, season, episode);
const searchRes = await searchAnime(originSearchUrl, preferAnimeId, preferSource, requestAnimeDetailsMap, targetPlatform);
const searchData = await searchRes.json();
log("info", `[system] [Match] searchData: ${searchData.animes}`);
@@ -1564,7 +1579,7 @@ export async function searchEpisodes(url) {
}
// 先搜索动漫
let searchUrl = new URL(`/search/anime?keyword=${anime}`, url.origin);
let searchUrl = buildSearchAnimeUrl(url, anime);
const requestAnimeDetailsMap = new Map();
const searchRes = await searchAnime(searchUrl, null, null, requestAnimeDetailsMap);
+20 -1
View File
@@ -5,7 +5,7 @@ dotenv.config();
import test from 'node:test';
import assert from 'node:assert';
import { handleRequest } from './worker.js';
import { extractTitleSeasonEpisode, getBangumi, getComment, matchAnime, searchAnime } from "./apis/dandan-api.js";
import { extractTitleSeasonEpisode, getBangumi, getComment, matchAnime, searchAnime, buildSearchAnimeUrl } from "./apis/dandan-api.js";
import { getRedisKey, pingRedis, setRedisKey, setRedisKeyWithExpiry } from "./utils/redis-util.js";
import { getLocalRedisKey, setLocalRedisKey, setLocalRedisKeyWithExpiry } from "./utils/local-redis-util.js";
import { getImdbepisodes } from "./utils/imdb-util.js";
@@ -206,6 +206,25 @@ test('worker.js API endpoints', async (t) => {
assert.equal(seenRedirectMode, 'manual');
});
await t.test('buildSearchAnimeUrl should preserve special characters in keyword', async () => {
const searchUrl = buildSearchAnimeUrl(`${urlPrefix}/api/v2/match`, 'Love & Death', 1, 2);
assert.equal(searchUrl.pathname, '/api/v2/search/anime');
assert.equal(searchUrl.searchParams.get('keyword'), 'Love & Death');
assert.equal(searchUrl.searchParams.get('season'), '1');
assert.equal(searchUrl.searchParams.get('episode'), '2');
assert.equal(searchUrl.searchParams.has(' Death'), false);
});
await t.test('buildSearchAnimeUrl should derive /search/anime from /search/episodes requests', async () => {
const searchUrl = buildSearchAnimeUrl(`${urlPrefix}/api/v2/search/episodes?anime=Love%20%26%20Death&episode=2`, 'Love & Death');
assert.equal(searchUrl.pathname, '/api/v2/search/anime');
assert.equal(searchUrl.searchParams.get('keyword'), 'Love & Death');
assert.equal(searchUrl.searchParams.has('season'), false);
assert.equal(searchUrl.searchParams.has('episode'), false);
});
// 测试标题解析
await t.test('PARSE TitleSeasonEpisode', async () => {
let title, season, episode;