From a6a0babd225024a9a80433c0345254657ee95ea6 Mon Sep 17 00:00:00 2001 From: huangxd- <649924757@qq.com> Date: Mon, 9 Feb 2026 23:22:24 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=8E=AF=E5=A2=83=E5=8F=98?= =?UTF-8?q?=E9=87=8FANIME=5FTITLE=5FSIMPLIFIED=EF=BC=8C=E5=9C=A8=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E6=97=B6=E5=B0=86=E7=B9=81=E4=BD=93=E5=89=A7=E5=90=8D?= =?UTF-8?q?=E6=A0=87=E9=A2=98=E8=87=AA=E5=8A=A8=E8=BD=AC=E6=8D=A2=E4=B8=BA?= =?UTF-8?q?=E7=AE=80=E4=BD=93=EF=BC=8C=E9=80=82=E7=94=A8=E4=BA=8E=E7=B9=81?= =?UTF-8?q?=E4=BD=93=E6=A0=87=E9=A2=98=E6=90=9C=E7=B4=A2=20(#187)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 修复搜索到的标题中带有括号导致自动匹配季不正确的问题 * 增加环境变量ANIME_TITLE_SIMPLIFIED,在搜索时将繁体剧名标题自动转换为简体,适用于繁体标题搜索,默认值:false(不转换) * 接口调试ui界面上的/api/v2/search/episodes接口增加episode,用于指定集 --- README.md | 1 + config/.env.example | 5 +++++ danmu_api/apis/dandan-api.js | 29 ++++++++++++++++++++++++++--- danmu_api/configs/envs.js | 2 ++ danmu_api/configs/globals.js | 2 +- danmu_api/ui/js/apitest.js | 3 ++- forward/forward-widget.js | 34 +++++++++++++++++++++++++--------- forward/forward-widget.test.js | 1 + 8 files changed, 63 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 5b9bebd..8dfb04d 100644 --- a/README.md +++ b/README.md @@ -381,6 +381,7 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数 | STRICT_TITLE_MATCH | 【可选】是否启用严格标题匹配模式,默认为`false`(宽松模糊匹配),启用后只匹配标题开头或完全匹配的结果。例如:搜索"遮天"时,`false`会匹配"古惑仔3之只手遮天",`true`只匹配"遮天"、"遮天 第一季"等。可选值:`true`、`false` | | TITLE_TO_CHINESE | 【可选】是否在match自动匹配时将外语标题转换成中文标题,适用于网盘没有刮削的资源,默认值:false(不转换),说明:需配合TMDB_API_KEY使用 | | TITLE_MAPPING_TABLE | 【可选】剧名映射表,用于自动匹配时替换标题进行搜索,格式:原始标题->映射标题;原始标题->映射标题;... ,例如:"唐朝诡事录->唐朝诡事录之西行;国色芳华->锦绣芳华" | +| ANIME_TITLE_SIMPLIFIED | 【可选】是否在搜索时将繁体剧名标题自动转换为简体,适用于繁体标题搜索,默认值:false(不转换),可选值:`true`、`false` | | BLOCKED_WORDS | 【可选】弹幕屏蔽词列表,默认为空,示例如下 | | GROUP_MINUTE | 【可选】合并去重分钟数,表示按n分钟分组后对弹幕合并去重,默认为1,最大值为30,0表示不去重 | | DANMU_LIMIT | 【可选】等间隔采样限制弹幕总数,单位为k,即千:默认 0,表示不限制弹幕数,若改为5,弹幕总数在超过5000的情况下会将弹幕数控制在5000 | diff --git a/config/.env.example b/config/.env.example index 41cee13..6afb817 100644 --- a/config/.env.example +++ b/config/.env.example @@ -296,3 +296,8 @@ MAX_LAST_SELECT_MAP=100 # 示例:TITLE_MAPPING_TABLE: "唐朝诡事录->唐朝诡事录之西行;国色芳华->锦绣芳华" # 默认值:空(不启用标题映射) TITLE_MAPPING_TABLE= + +# 是否在搜索时将繁体剧名标题自动转换为简体,适用于繁体标题搜索 +# 默认值:false(不转换) +# 可选值:true, false +ANIME_TITLE_SIMPLIFIED=false diff --git a/danmu_api/apis/dandan-api.js b/danmu_api/apis/dandan-api.js index 465cf86..2d8251d 100644 --- a/danmu_api/apis/dandan-api.js +++ b/danmu_api/apis/dandan-api.js @@ -1,6 +1,7 @@ import { globals } from '../configs/globals.js'; import { getPageTitle, jsonResponse, httpGet } from '../utils/http-util.js'; import { log } from '../utils/log-util.js' +import { simplized } from '../utils/zh-util.js'; import { setRedisKey, updateRedisCaches } from "../utils/redis-util.js"; import { setCommentCache, addAnime, findAnimeIdByCommentId, findTitleById, findUrlById, getCommentCache, getPreferAnimeId, @@ -80,7 +81,8 @@ export function matchSeason(anime, queryTitle, season) { const normalizedQueryTitle = normalizeSpaces(queryTitle); if (normalizedAnimeTitle.includes(normalizedQueryTitle)) { - const title = normalizedAnimeTitle.split("(")[0].trim(); + const match = normalizedAnimeTitle.match(/^(.*?)\(\d{4}\)/); + const title = match ? match[1].trim() : normalizedAnimeTitle.split("(")[0].trim(); if (title.startsWith(normalizedQueryTitle)) { const afterTitle = title.substring(normalizedQueryTitle.length).trim(); if (afterTitle === '' && season === 1) { @@ -105,7 +107,7 @@ export function matchSeason(anime, queryTitle, season) { // Extracted function for GET /api/v2/search/anime export async function searchAnime(url, preferAnimeId = null, preferSource = null) { - const queryTitle = url.searchParams.get("keyword"); + let queryTitle = url.searchParams.get("keyword"); log("info", `Search anime with keyword: ${queryTitle}`); // 关键字为空直接返回,不用多余查询 @@ -118,6 +120,13 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null }); } + // 如果启用了搜索关键字繁转简,则进行转换 + if (globals.animeTitleSimplified) { + const simplifiedTitle = simplized(queryTitle); + log("info", `searchAnime converted traditional to simplified: ${queryTitle} -> ${simplifiedTitle}`); + queryTitle = simplifiedTitle; + } + // 检查搜索缓存 const cachedResults = getSearchCache(queryTitle); if (cachedResults !== null) { @@ -795,6 +804,13 @@ export async function matchAnime(url, req) { } } + // 如果启用了搜索关键字繁转简,则进行转换 + if (globals.animeTitleSimplified) { + const simplifiedTitle = simplized(title); + log("info", `matchAnime converted traditional to simplified: ${title} -> ${simplifiedTitle}`); + title = simplifiedTitle; + } + // 获取prefer animeIdgetPreferAnimeId const [preferAnimeId, preferSource] = getPreferAnimeId(title); log("info", `prefer animeId: ${preferAnimeId} from ${preferSource}`); @@ -871,9 +887,16 @@ export async function matchAnime(url, req) { // Extracted function for GET /api/v2/search/episodes export async function searchEpisodes(url) { - const anime = url.searchParams.get("anime"); + let anime = url.searchParams.get("anime"); const episode = url.searchParams.get("episode") || ""; + // 如果启用了搜索关键字繁转简,则进行转换 + if (globals.animeTitleSimplified) { + const simplifiedTitle = simplized(anime); + log("info", `searchEpisodes converted traditional to simplified: ${anime} -> ${simplifiedTitle}`); + anime = simplifiedTitle; + } + log("info", `Search episodes with anime: ${anime}, episode: ${episode}`); if (!anime) { diff --git a/danmu_api/configs/envs.js b/danmu_api/configs/envs.js index 95cccc1..b45e1d7 100644 --- a/danmu_api/configs/envs.js +++ b/danmu_api/configs/envs.js @@ -323,6 +323,7 @@ export class Envs { 'ENABLE_EPISODE_FILTER': { category: 'match', type: 'boolean', description: '集标题过滤开关' }, 'STRICT_TITLE_MATCH': { category: 'match', type: 'boolean', description: '严格标题匹配模式' }, 'TITLE_TO_CHINESE': { category: 'match', type: 'boolean', description: '外语标题转换中文开关' }, + 'ANIME_TITLE_SIMPLIFIED': { category: 'match', type: 'boolean', description: '搜索的剧名标题自动繁转简' }, 'TITLE_MAPPING_TABLE': { category: 'match', type: 'map', description: '剧名映射表,用于自动匹配时替换标题进行搜索,格式:原始标题->映射标题;原始标题->映射标题;... ,例如:"唐朝诡事录->唐朝诡事录之西行;国色芳华->锦绣芳华"' }, // 弹幕配置 @@ -388,6 +389,7 @@ export class Envs { danmuOutputFormat: this.get('DANMU_OUTPUT_FORMAT', 'json', 'string'), // 弹幕输出格式配置(默认 json,可选值:json, xml) strictTitleMatch: this.get('STRICT_TITLE_MATCH', false, 'boolean'), // 严格标题匹配模式配置(默认 false,宽松模糊匹配) titleToChinese: this.get('TITLE_TO_CHINESE', false, 'boolean'), // 外语标题转换中文开关 + animeTitleSimplified: this.get('ANIME_TITLE_SIMPLIFIED', false, 'boolean'), // 搜索的剧名标题自动繁转简 titleMappingTable: this.resolveTitleMappingTable(), // 剧名映射表,用于自动匹配时替换标题进行搜索 rememberLastSelect: this.get('REMEMBER_LAST_SELECT', true, 'boolean'), // 是否记住手动选择结果,用于match自动匹配时优选上次的选择(默认 true,记住) MAX_LAST_SELECT_MAP: this.get('MAX_LAST_SELECT_MAP', 100, 'number'), // 记住上次选择映射缓存大小限制(默认 100) diff --git a/danmu_api/configs/globals.js b/danmu_api/configs/globals.js index 2e03deb..062d4aa 100644 --- a/danmu_api/configs/globals.js +++ b/danmu_api/configs/globals.js @@ -13,7 +13,7 @@ export const Globals = { accessedEnvVars: {}, // 静态常量 - VERSION: '1.14.0', + VERSION: '1.14.1', MAX_LOGS: 1000, // 日志存储,最多保存 1000 行 MAX_ANIMES: 100, MAX_RECORDS: 100, // 请求记录最大数量 diff --git a/danmu_api/ui/js/apitest.js b/danmu_api/ui/js/apitest.js index b5d940c..e79ec51 100644 --- a/danmu_api/ui/js/apitest.js +++ b/danmu_api/ui/js/apitest.js @@ -15,7 +15,8 @@ const apiConfigs = { method: 'GET', path: '/api/v2/search/episodes', params: [ - { name: 'anime', label: '动漫名称', type: 'text', required: true, placeholder: '示例: 生万物' } + { name: 'anime', label: '动漫名称', type: 'text', required: true, placeholder: '示例: 生万物' }, + { name: 'episode', label: '集', type: 'text', required: false, placeholder: '示例: 1, movie' } ] }, matchAnime: { diff --git a/forward/forward-widget.js b/forward/forward-widget.js index 8813cc8..371c842 100644 --- a/forward/forward-widget.js +++ b/forward/forward-widget.js @@ -224,6 +224,21 @@ WidgetMetadata = { }, ], }, + { + name: "animeTitleSimplified", + title: "搜索的剧名标题自动繁转简,默认false", + type: "input", + placeholders: [ + { + title: "false", + value: "false", + }, + { + title: "true", + value: "true", + }, + ], + }, // 弹幕配置 { @@ -437,7 +452,7 @@ if (typeof window !== 'undefined') { // 初始化全局配置 let globals; async function initGlobals(sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie, - platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, blockedWords, groupMinute, + platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, animeTitleSimplified, blockedWords, groupMinute, danmuLimit, danmuSimplifiedTraditional, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey) { // 将传入的参数设置到环境变量中,以便Globals可以访问它们 const env = {}; @@ -454,6 +469,7 @@ async function initGlobals(sourceOrder, otherServer, customSourceApiUrl, vodServ if (enableEpisodeFilter !== undefined) env.ENABLE_EPISODE_FILTER = enableEpisodeFilter; if (strictTitleMatch !== undefined) env.STRICT_TITLE_MATCH = strictTitleMatch; if (titleMappingTable !== undefined) env.TITLE_MAPPING_TABLE = titleMappingTable; + if (animeTitleSimplified !== undefined) env.ANIME_TITLE_SIMPLIFIED = animeTitleSimplified; if (blockedWords !== undefined) env.BLOCKED_WORDS = blockedWords; if (groupMinute !== undefined) env.GROUP_MINUTE = groupMinute; if (danmuLimit !== undefined) env.DANMU_LIMIT = danmuLimit; @@ -527,11 +543,11 @@ const PREFIX_URL = "http://localhost:9321" async function searchDanmu(params) { const { tmdbId, type, title, season, link, videoUrl, sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie, - platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, blockedWords, groupMinute, + platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, animeTitleSimplified, blockedWords, groupMinute, danmuLimit, danmuSimplifiedTraditional, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey } = params; await initGlobals(sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie, - platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, blockedWords, groupMinute, + platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, animeTitleSimplified, blockedWords, groupMinute, danmuLimit, danmuSimplifiedTraditional, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey); const response = await searchAnime(new URL(`${PREFIX_URL}/api/v2/search/anime?keyword=${title}`)); @@ -600,11 +616,11 @@ async function searchDanmu(params) { async function getDetailById(params) { const { animeId, sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie, - platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, blockedWords, groupMinute, + platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, animeTitleSimplified, blockedWords, groupMinute, danmuLimit, danmuSimplifiedTraditional, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey } = params; await initGlobals(sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie, - platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, blockedWords, groupMinute, + platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, animeTitleSimplified, blockedWords, groupMinute, danmuLimit, danmuSimplifiedTraditional, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey); const response = await getBangumi(`${PREFIX_URL}/api/v2/bangumi/${animeId}`); @@ -619,11 +635,11 @@ async function getDetailById(params) { async function getCommentsById(params) { const { commentId, link, videoUrl, season, episode, tmdbId, type, title, segmentTime, sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie, - platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, blockedWords, groupMinute, + platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, animeTitleSimplified, blockedWords, groupMinute, danmuLimit, danmuSimplifiedTraditional, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey } = params; await initGlobals(sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie, - platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, blockedWords, groupMinute, + platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, animeTitleSimplified, blockedWords, groupMinute, danmuLimit, danmuSimplifiedTraditional, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey); if (commentId) { @@ -666,11 +682,11 @@ async function getCommentsById(params) { async function getDanmuWithSegmentTime(params) { const { segmentTime, tmdbId, season, episode, sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie, - platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, blockedWords, groupMinute, + platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, animeTitleSimplified, blockedWords, groupMinute, danmuLimit, danmuSimplifiedTraditional, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey } = params; await initGlobals(sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie, - platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, blockedWords, groupMinute, + platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, animeTitleSimplified, blockedWords, groupMinute, danmuLimit, danmuSimplifiedTraditional, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey); const storeKey = season && episode ? `${tmdbId}.${season}.${episode}` : `${tmdbId}`; diff --git a/forward/forward-widget.test.js b/forward/forward-widget.test.js index 8843686..ddea6f7 100644 --- a/forward/forward-widget.test.js +++ b/forward/forward-widget.test.js @@ -227,6 +227,7 @@ async function testNewFlow() { episodeTitleFilter: '', enableEpisodeFilter: false, strictTitleMatch: false, + animeTitleSimplified: false, blockedWords: '', groupMinute: 1, danmuLimit: 0,