增加环境变量ANIME_TITLE_SIMPLIFIED,在搜索时将繁体剧名标题自动转换为简体,适用于繁体标题搜索 (#187)
* 修复搜索到的标题中带有括号导致自动匹配季不正确的问题 * 增加环境变量ANIME_TITLE_SIMPLIFIED,在搜索时将繁体剧名标题自动转换为简体,适用于繁体标题搜索,默认值:false(不转换) * 接口调试ui界面上的/api/v2/search/episodes接口增加episode,用于指定集
This commit is contained in:
1 parent
16d6c5ab4d
commit
a6a0babd22
8 files changed
+63
-14
No files matched your search
@@ -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 |
|
||||
|
||||
@@ -296,3 +296,8 @@ MAX_LAST_SELECT_MAP=100
|
||||
# 示例:TITLE_MAPPING_TABLE: "唐朝诡事录->唐朝诡事录之西行;国色芳华->锦绣芳华"
|
||||
# 默认值:空(不启用标题映射)
|
||||
TITLE_MAPPING_TABLE=
|
||||
|
||||
# 是否在搜索时将繁体剧名标题自动转换为简体,适用于繁体标题搜索
|
||||
# 默认值:false(不转换)
|
||||
# 可选值:true, false
|
||||
ANIME_TITLE_SIMPLIFIED=false
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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, // 请求记录最大数量
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -227,6 +227,7 @@ async function testNewFlow() {
|
||||
episodeTitleFilter: '',
|
||||
enableEpisodeFilter: false,
|
||||
strictTitleMatch: false,
|
||||
animeTitleSimplified: false,
|
||||
blockedWords: '',
|
||||
groupMinute: 1,
|
||||
danmuLimit: 0,
|
||||
|
||||
Reference in new issue
Block a user