diff --git a/README.md b/README.md index 83e9b68..9ca5227 100644 --- a/README.md +++ b/README.md @@ -418,6 +418,7 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数 | STRICT_TITLE_MATCH | 【可选】是否启用严格标题匹配模式,默认为`false`(宽松模糊匹配),启用后只匹配标题开头或完全匹配的结果。例如:搜索"遮天"时,`false`会匹配"古惑仔3之只手遮天",`true`只匹配"遮天"、"遮天 第一季"等。可选值:`true`、`false` | | TITLE_TO_CHINESE | 【可选】是否在match自动匹配时将外语标题转换成中文标题,适用于网盘没有刮削的资源,默认值:false(不转换),说明:需配合TMDB_API_KEY使用 | | TITLE_MAPPING_TABLE | 【可选】剧名映射表,用于自动匹配时替换标题进行搜索,格式:原始标题->映射标题;原始标题->映射标题;... ,例如:"唐朝诡事录->唐朝诡事录之西行;国色芳华->锦绣芳华" | +| TITLE_NOISE_FILTER | 【可选】剧名杂音清理规则,按正则表达式清理搜索与匹配阶段的剧名杂音词(如`百花杀(真彩)`→`百花杀`),默认值如下,设为空值可禁用 | | ANIME_TITLE_SIMPLIFIED | 【可选】是否在搜索时将繁体剧名标题自动转换为简体,适用于繁体标题搜索,默认值:false(不转换),可选值:`true`、`false` | | BLOCKED_WORDS | 【可选】弹幕屏蔽词列表,默认为空,示例如下 | | GROUP_MINUTE | 【可选】合并去重分钟数,表示按n分钟分组后对弹幕合并去重,默认为1,最大值为30,0表示不去重 | @@ -455,12 +456,17 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数 ```regex # EPISODE_TITLE_FILTER 默认值 -(特别|惊喜|纳凉)?企划(?!(书|案|部))|合伙人手记|超前(营业|vlog)?|速览|vlog|(?°◆◇■□●○★☆▼▲♥♦♠♣①②③④⑤⑥⑦⑧⑨⑩]/,/[一二三四五六七八九十百\d]+刷/,/第[一二三四五六七八九十百\d]+/,/(全体成员|报到|报道|来啦|签到|刷|打卡|我在|来了|考古|爱了|挖坟|留念|你好|回来|哦哦|重温|复习|重刷|再看|在看|前排|沙发|有人看|板凳|末排|我老婆|我老公|撅了|后排|周目|重看|包养|DVD|同上|同样|我也是|俺也|算我|爱豆|我家爱豆|我家哥哥|加我|三连|币|新人|入坑|补剧|冲了|硬了|看完|舔屏|万人|牛逼|煞笔|傻逼|卧槽|tm|啊这|哇哦)/ diff --git a/config/.env.example b/config/.env.example index 723aa68..314206e 100644 --- a/config/.env.example +++ b/config/.env.example @@ -122,7 +122,7 @@ SOURCE_ORDER=douban,360,renren,hanjutv # - 可以直接修改下方规则,按需增删关键词 # 示例:只过滤预告和花絮 # EPISODE_TITLE_FILTER=预告|花絮 -EPISODE_TITLE_FILTER=(特别|惊喜|纳凉)?企划(?!(书|案|部))|合伙人手记|超前(营业|vlog)?|速览|vlog|(?} JSON 字符串 @@ -613,6 +637,7 @@ export class Envs { 'TITLE_TO_CHINESE': { category: 'match', type: 'boolean', description: '外语标题转换中文开关' }, 'ANIME_TITLE_SIMPLIFIED': { category: 'match', type: 'boolean', description: '搜索的剧名标题自动繁转简' }, 'TITLE_MAPPING_TABLE': { category: 'match', type: 'map', description: '剧名映射表,用于自动匹配时替换标题进行搜索,格式:原始标题->映射标题;原始标题->映射标题;... ,例如:"唐朝诡事录->唐朝诡事录之西行;国色芳华->锦绣芳华"' }, + 'TITLE_NOISE_FILTER': { category: 'match', type: 'text', description: '剧名杂音清理规则,按正则表达式清理搜索与匹配阶段的剧名杂音词(如`百花杀(真彩)`→`百花杀`)。默认值:[((](?:臻彩|真彩|高清|标清|超清|国配|中配|日配|粤语|原声|台配|无修|未删减|完整版)[))],中英文括号均匹配。设为空值可禁用' }, 'AI_BASE_URL': { category: 'match', type: 'text', description: 'AI服务基础URL,不填默认为https://api.openai.com/v1' }, 'AI_MODEL': { category: 'match', type: 'text', description: 'AI模型名称,不填默认为gpt-4o' }, 'AI_API_KEY': { category: 'match', type: 'text', description: 'AI服务API密钥,默认为空,需手动填写' }, @@ -673,6 +698,7 @@ export class Envs { platformOrderArr: this.resolvePlatformOrder(), // 自动匹配优选平台 animeTitleFilter: this.resolveAnimeTitleFilter(), // 剧名正则过滤 episodeTitleFilter: this.resolveEpisodeTitleFilter(), // 剧集标题正则过滤 + titleNoiseFilter: this.resolveTitleNoiseFilter(), // 剧名杂音清理规则 blockedWords: this.get('BLOCKED_WORDS', '', 'string'), // 屏蔽词列表 groupMinute: Math.min(this.get('GROUP_MINUTE', 1, 'number'), 30), // 分钟内合并去重(默认 1,最大值30,0表示不去重) danmuLimit: this.get('DANMU_LIMIT', 0, 'number'), // 等间隔采样限制弹幕总数,单位为k,即千:默认 0,表示不限制弹幕数,若改为5,弹幕总数在超过5000的情况下会将弹幕数控制在5000 diff --git a/danmu_api/utils/common-util.js b/danmu_api/utils/common-util.js index 84fa328..430491e 100644 --- a/danmu_api/utils/common-util.js +++ b/danmu_api/utils/common-util.js @@ -267,8 +267,12 @@ export function titleMatches(title, query, parsedSeason = null) { // 策略1:严格模式仅允许头部或完全匹配 if (globals.strictTitleMatch) return strictTitleMatch(title, query); + // 剧名杂音清理:移除画质/配音/版本等杂音词,避免阻塞匹配 + const tagFilter = globals.titleNoiseFilter || null; + const cleanTitle = tagFilter ? title.replace(tagFilter, '').trim() : title; + // 预处理:移除干扰字符并转小写,消除格式与大小写差异 - const t = normalizeSpaces(title).toLowerCase(); + const t = normalizeSpaces(cleanTitle).toLowerCase(); const q = normalizeSpaces(query).toLowerCase(); // 预处理:构建搜索词变种池 (原词、简体、繁体),利用 Set 去重 @@ -290,6 +294,15 @@ export function titleMatches(title, query, parsedSeason = null) { } } + // 查询词含杂音词时,将清理后的干净查询词加入候选池 + // 如"百花杀(真彩)"→ 追加"百花杀",避免"真彩"二字导致包含匹配失败 + if (tagFilter) { + const tagStripped = query.replace(tagFilter, '').trim(); + if (tagStripped && tagStripped !== query) { + qList = [...new Set([...qList, normalizeSpaces(tagStripped).toLowerCase()])]; + } + } + // 季度特征校验:先于包含匹配执行,确保错误季度的标题不会因干净查询词误通过 if (querySeason !== null) { const titleSeason = getExplicitSeasonNumber(title); @@ -307,7 +320,7 @@ export function titleMatches(title, query, parsedSeason = null) { if (qList.some(kw => t.includes(kw))) return true; // 策略3:相似度匹配 (阈值0.8) - return qList.some(kw => { + const simMatch = qList.some(kw => { // 长度差异过大,或纯英文/数字时,禁止使用相似度计算策略 if (Math.abs(t.length - kw.length) > Math.max(t.length, kw.length) * 0.7 || /^[a-zA-Z0-9]+$/.test(kw)) { return false; @@ -327,6 +340,22 @@ export function titleMatches(title, query, parsedSeason = null) { return (matchCount / kw.length) > 0.8; }); + + // 年份噪音兜底:前序策略均未命中时,尝试去年份后再次包含匹配 + // 如查询词含杂音年份"(2026)"但源标题不含,去年后可正常命中 + if (!simMatch) { + const yearStripped = query.replace(/[\(\(]\s*(?:19|20)\d{2}\s*[\)\)]|\b(?:19|20)\d{2}\b/g, '').trim(); + if (yearStripped && yearStripped !== query) { + // 去年份后可能残留杂音词,继续用配置的正则去除 + const cleanYear = tagFilter ? yearStripped.replace(tagFilter, '').trim() : yearStripped; + const yearQ = normalizeSpaces(cleanYear).toLowerCase(); + // 仅当标题不含年份时才接受兜底,保留用户刻意用年份过滤的语义 + const titleHasYear = /[\(\(]\s*(?:19|20)\d{2}\s*[\)\)]|\b(?:19|20)\d{2}\b/.test(title); + if (yearQ && t.includes(yearQ) && !titleHasYear) return true; + } + } + + return false; } /**