From 81a320177efa2d2afa1311cb5e94e40f9fd7af3d Mon Sep 17 00:00:00 2001 From: Wo254992 <110774618+Wo254992@users.noreply.github.com> Date: Sat, 6 Jun 2026 12:18:25 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=89=B9=E6=AE=8A?= =?UTF-8?q?=E5=AD=97=E7=AC=A6=E6=A0=87=E9=A2=98=E6=90=9C=E7=B4=A2=E5=8C=B9?= =?UTF-8?q?=E9=85=8D=E5=A4=B1=E8=B4=A5=20(#337)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 修复特殊字符标题搜索匹配失败 改动详情: - 使用 URL.searchParams 构造内部 /search/anime 搜索地址 - 避免标题中的 & 等特殊字符被当作查询参数分隔符截断 - 补充回归测试,验证 keyword、season、episode 参数保持正确 * refactor: 简化搜索地址构造调用 --- danmu_api/apis/dandan-api.js | 19 +++++++++++++++++-- danmu_api/worker.test.js | 21 ++++++++++++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/danmu_api/apis/dandan-api.js b/danmu_api/apis/dandan-api.js index ef91ddd..7321d72 100644 --- a/danmu_api/apis/dandan-api.js +++ b/danmu_api/apis/dandan-api.js @@ -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); diff --git a/danmu_api/worker.test.js b/danmu_api/worker.test.js index cb9aabd..e71d6e0 100644 --- a/danmu_api/worker.test.js +++ b/danmu_api/worker.test.js @@ -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;