From 4fd7ccf412a132983eedd167ff561961683e7ba2 Mon Sep 17 00:00:00 2001 From: huangxd- <649924757@qq.com> Date: Tue, 27 Jan 2026 23:39:31 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=AE=80=E4=BD=93=E8=BD=AC?= =?UTF-8?q?=E7=B9=81=E4=BD=93=E5=BC=80=E5=85=B3=20(#169)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 增加简体转繁体开关 * 修改readme和小版本号 * 修改readme * 修改缩进问题 --- README.md | 7 +++++-- config/.env.example | 8 ++++++- danmu_api/configs/envs.js | 10 +++++---- danmu_api/configs/globals.js | 2 +- danmu_api/utils/danmu-util.js | 10 +++++++++ forward/forward-widget.js | 38 ++++++++++++++++++++++++---------- forward/forward-widget.test.js | 1 + 7 files changed, 57 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 388d4f0..82545fa 100644 --- a/README.md +++ b/README.md @@ -304,7 +304,7 @@ LogVar 弹幕 API 服务器 > cf部署可能不稳定,推荐用vercel/netlify部署。 ## API食用指南 -支持 forward/senplayer/hills/小幻/yamby/eplayerx/afusekt/uz影视/dscloud/lenna/danmaku-anywhere/omnibox/ChaiChaiEmbyTV/moontv/capyplayer/kerkerker 等支持弹幕API的播放器。 +支持 forward/senplayer/hills/小幻/yamby/eplayerx/afusekt/uz影视/dscloud/lenna/danmaku-anywhere/omnibox/ChaiChaiEmbyTV/moontv/capyplayer/kerkerker/LinPlayer 等支持弹幕API的播放器。 配合 dd-danmaku 扩展新增对 Emby Web 端弹幕的支持,具体使用方法参考 [PR #98](https://github.com/huangxd-/danmu_api/pull/98) 。 @@ -385,7 +385,8 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数 | CONVERT_TOP_BOTTOM_TO_SCROLL | 【可选】是否将顶部和底部弹幕转换为浮动弹幕,默认为`false`(不转换),启用后顶部弹幕(ct=5)和底部弹幕(ct=4)会被转换为浮动弹幕(ct=1),可选值:`true`、`false` | | CONVERT_COLOR | 【可选】弹幕转换颜色配置,默认为`default`(不转换),`white` 将所有非白色的弹幕颜色转换为纯白色,`color` 将所有白色弹幕转换为随机颜色(包含白色),可选值:`default`、`white`、`color` | | DANMU_OUTPUT_FORMAT | 【可选】弹幕输出格式,默认为`json`,可选值:`json`(JSON格式)、`xml`(XML格式),支持通过查询参数`?format=xml`或`?format=json`覆盖此设置,优先级:查询参数 > 环境变量 > 默认值 | -| DANMU_SIMPLIFIED | 【可选】是否将繁体弹幕转换为简体,目前只对巴哈姆特生效,默认为`true`(转换),可选值:`false`(不转换) | +| DANMU_SIMPLIFIED | 【可选】是否将繁体弹幕转换为简体,目前只对巴哈姆特生效,默认为`true`(转换),可选值:`false`(不转换),优先级:DANMU_SIMPLIFIED < DANMU_TRADITIONAL | +| DANMU_TRADITIONAL | 【可选】是否将简体弹幕转换为繁体,对所有源生效,默认为`false`(不转换),可选值:`true`(转换),优先级:DANMU_TRADITIONAL > DANMU_SIMPLIFIED | | PROXY_URL | 【可选】代理/反代地址,目前只对巴哈姆特、TMDB API、bilibili生效,支持格式:
正常代理:`http://127.0.0.1:7890`
万能反代:`@http://127.0.0.1`
特定反代:`源字段@http://127.0.0.1`,目前支持的字段有:`bahamut,tmdb,bilibili`(bilibili字段会启用阿b的港澳台番剧的搜索与获取)
混合配置/示例:`http://你的代理地址:28233,bahamut@你的巴哈反代地址,tmdb@你的tmdb反代地址,@你的万能反代地址`
优先级:特定反代 > 万能反代 > 正常代理,高优先级覆盖低优先级使用。
(注意:如果巴哈姆特请求不通,会拖慢搜索返回速度,如需使用bahamut源请在SOURCE_ORDER环境变量中手动添加`bahamut`)如果你使用docker部署并且访问不了bahamut源,请配置代理地址或者反代([Netlify反代教程](https://github.com/wan0ge/bahamut-api-proxy));vercel/netlify/cf中理应都自然能联通,不用填写 | | TMDB_API_KEY | 【可选】TMDB API Key地址,目前只对巴哈姆特生效,配置后并行从TMDB获取日语原名搜索巴哈(如果TMDB条目类型不是动画或制作地区不是jp则不会进行巴哈搜索)可以解决巴哈译名不同导致的搜索无结果问题,例如大陆常用译名`间谍过家家`在巴哈译名为`間諜家家酒`,正常搜索无法搜索到,配置后可以解决这一问题但会稍微影响请求速度,[TMDBAPI](https://www.themoviedb.org/settings/api)获取方法参考:[TMDB API Key申请 - 绿联NAS私有云](https://www.ugnas.com/tutorial-detail/id-226.html) | | RATE_LIMIT_MAX_REQUESTS | 【可选】限流配置:1分钟内同一IP最大请求次数,默认为`3`,设置为`0`表示不限流 | @@ -583,6 +584,8 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数 - 弹幕分片下载请求已加入重试机制,重试次数为1次 - 如果同时配置了本地缓存和redis缓存,则以redis缓存优先 - 弹弹play源用的是第三方接口,感谢开源项目 [danmaku-anywhere](https://github.com/Mr-Quin/danmaku-anywhere) +- 西瓜视频源因为部分调用了douyin接口,所以在国外环境部署的情况下会获取不到弹幕 +- 有任何问题,如部署/环境变量配置等,可通过deepwiki对本项目进行提问,链接入口:https://deepwiki.com/huangxd-/danmu_api,其中项目内容一般每周刷新一次 ### 关联项目 [喂饭教程1:danmu_api vercel 自动同步部署方案 - 永远保持最新版本!实时同步原作者更新](https://github.com/xiaoyao20084321/log-var-danmu-deployment-guide) diff --git a/config/.env.example b/config/.env.example index c6e1a30..048b168 100644 --- a/config/.env.example +++ b/config/.env.example @@ -138,10 +138,16 @@ CONVERT_COLOR=default # 是否将繁体弹幕转换为简体,目前只对巴哈姆特源生效 # 默认值:true(转换) -# 说明:禁用后,不进行巴哈弹幕繁转简 +# 说明:禁用后,不进行巴哈弹幕繁转简,优先级:DANMU_SIMPLIFIED < DANMU_TRADITIONAL # 可选值:true, false DANMU_SIMPLIFIED=true +# 是否将简体弹幕全部转换为繁体,对所有源生效 +# 默认值:false(不转换) +# 说明:优先级:DANMU_TRADITIONAL > DANMU_SIMPLIFIED +# 可选值:true, false +DANMU_TRADITIONAL=false + # ==================== 弹幕推送配置 ==================== # 弹幕推送地址 diff --git a/danmu_api/configs/envs.js b/danmu_api/configs/envs.js index bb9fda9..58511c1 100644 --- a/danmu_api/configs/envs.js +++ b/danmu_api/configs/envs.js @@ -280,7 +280,7 @@ export class Envs { 'VOD_REQUEST_TIMEOUT': { category: 'source', type: 'number', description: 'VOD请求超时时间,默认10000', min: 5000, max: 30000 }, 'BILIBILI_COOKIE': { category: 'source', type: 'text', description: 'B站Cookie' }, 'YOUKU_CONCURRENCY': { category: 'source', type: 'number', description: '优酷并发配置,默认8', min: 1, max: 16 }, - 'MERGE_SOURCE_PAIRS': { category: 'source', type: 'text', description: '源合并配置,配置后将对应源合并同时一起获取弹幕返回,支持多源链式合并,第一个为主源。\n格式:源1&源2&源3,多组用逗号分隔。\n示例:dandan&animeko&bahamut, bilibili&animeko\n目前允许的源:tencent,youku,iqiyi,imgo,bilibili,sohu,leshi,xigua,renren,hanjutv,bahamut,dandan,animeko' }, + 'MERGE_SOURCE_PAIRS': { category: 'source', type: 'text', description: '源合并配置,配置后将对应源合并同时一起获取弹幕返回,支持多源链式合并,第一个为主源。\n格式:源1&源2&源3,多组用逗号分隔。\n示例:dandan&animeko&bahamut, bilibili&animeko\n目前允许的源:tencent,youku,iqiyi,imgo,bilibili,sohu,leshi,xigua,renren,hanjutv,bahamut,dandan,animeko' }, // 匹配配置 'PLATFORM_ORDER': { category: 'match', type: 'multi-select', options: this.ALLOWED_PLATFORMS, description: '平台排序配置' }, @@ -294,7 +294,8 @@ export class Envs { 'BLOCKED_WORDS': { category: 'danmu', type: 'text', description: '屏蔽词列表' }, 'GROUP_MINUTE': { category: 'danmu', type: 'number', description: '分钟内合并去重(0表示不去重),默认1', min: 0, max: 30 }, 'DANMU_LIMIT': { category: 'danmu', type: 'number', description: '弹幕数量限制,单位为k,即千:默认 0,表示不限制弹幕数', min: 0, max: 100 }, - 'DANMU_SIMPLIFIED': { category: 'danmu', type: 'boolean', description: '弹幕繁体转简体开关' }, + 'DANMU_SIMPLIFIED': { category: 'danmu', type: 'boolean', description: '弹幕繁体转简体开关,优先级:DANMU_SIMPLIFIED < DANMU_TRADITIONAL,默认值为true' }, + 'DANMU_TRADITIONAL': { category: 'danmu', type: 'boolean', description: '弹幕简体转繁体开关,优先级:DANMU_TRADITIONAL > DANMU_SIMPLIFIED,默认值为false' }, 'CONVERT_TOP_BOTTOM_TO_SCROLL': { category: 'danmu', type: 'boolean', description: '顶部/底部弹幕转换为浮动弹幕' }, 'CONVERT_COLOR': { category: 'danmu', type: 'select', options: ['default', 'white', 'color'], description: '弹幕转换颜色配置' }, 'DANMU_OUTPUT_FORMAT': { category: 'danmu', type: 'select', options: ['json', 'xml'], description: '弹幕输出格式,默认json' }, @@ -331,14 +332,15 @@ export class Envs { vodRequestTimeout: this.get('VOD_REQUEST_TIMEOUT', '10000', 'string'), // vod超时时间(默认10秒) bilibliCookie: this.get('BILIBILI_COOKIE', '', 'string', true), // b站cookie youkuConcurrency: Math.min(this.get('YOUKU_CONCURRENCY', 8, 'number'), 16), // 优酷并发配置 - mergeSourcePairs: this.resolveMergeSourcePairs(), // 源合并配置,用于将源合并获取 + mergeSourcePairs: this.resolveMergeSourcePairs(), // 源合并配置,用于将源合并获取 platformOrderArr: this.resolvePlatformOrder(), // 自动匹配优选平台 episodeTitleFilter: this.resolveEpisodeTitleFilter(), // 剧集标题正则过滤 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 proxyUrl: this.get('PROXY_URL', '', 'string', true), // 代理/反代地址 - danmuSimplified: this.get('DANMU_SIMPLIFIED', true, 'boolean'), // 弹幕繁体转简体开关 + danmuSimplified: this.get('DANMU_SIMPLIFIED', true, 'boolean'), // 弹幕繁体转简体开关,优先级:DANMU_SIMPLIFIED < DANMU_TRADITIONAL,默认值为true + danmuTraditional: this.get('DANMU_TRADITIONAL', false, 'boolean'), // 弹幕简体转繁体开关,优先级:DANMU_TRADITIONAL > DANMU_SIMPLIFIED,默认值为false danmuPushUrl: this.get('DANMU_PUSH_URL', '', 'string'), // 代理/反代地址 tmdbApiKey: this.get('TMDB_API_KEY', '', 'string', true), // TMDB API KEY redisUrl: this.get('UPSTASH_REDIS_REST_URL', '', 'string', true), // upstash redis url diff --git a/danmu_api/configs/globals.js b/danmu_api/configs/globals.js index 08f24ec..71c04fb 100644 --- a/danmu_api/configs/globals.js +++ b/danmu_api/configs/globals.js @@ -13,7 +13,7 @@ export const Globals = { accessedEnvVars: {}, // 静态常量 - VERSION: '1.13.0', + VERSION: '1.13.1', MAX_LOGS: 500, // 日志存储,最多保存 500 行 MAX_ANIMES: 100, diff --git a/danmu_api/utils/danmu-util.js b/danmu_api/utils/danmu-util.js index b2fea57..a534f51 100644 --- a/danmu_api/utils/danmu-util.js +++ b/danmu_api/utils/danmu-util.js @@ -1,6 +1,7 @@ import { globals } from '../configs/globals.js'; import { log } from './log-util.js' import { jsonResponse, xmlResponse } from "./http-util.js"; +import { traditionalized } from './zh-util.js'; // ===================== // danmu处理相关函数 @@ -300,6 +301,15 @@ export function convertToDanmakuJson(contents, platform) { } } + // 在最后一步转换弹幕文本为繁体字 + if (globals.danmuTraditional) { + convertedDanmus = convertedDanmus.map(danmu => ({ + ...danmu, + m: traditionalized(danmu.m) + })); + log("info", `[danmu convert] 转换了 ${convertedDanmus.length} 条弹幕为繁体字`); + } + log("info", `danmus_original: ${danmus.length}`); log("info", `danmus_filter: ${filteredDanmus.length}`); log("info", `danmus_group: ${groupedDanmus.length}`); diff --git a/forward/forward-widget.js b/forward/forward-widget.js index 9cbed3d..0e40808 100644 --- a/forward/forward-widget.js +++ b/forward/forward-widget.js @@ -301,7 +301,7 @@ WidgetMetadata = { }, { name: "danmuSimplified", - title: "弹幕繁体转简体开关,目前只对巴哈姆特生效,默认true", + title: "弹幕繁体转简体开关,目前只对巴哈姆特生效,默认true,优先级:DANMU_SIMPLIFIED < DANMU_TRADITIONAL", type: "input", placeholders: [ { @@ -314,6 +314,21 @@ WidgetMetadata = { }, ], }, + { + name: "danmuTraditional", + title: "弹幕简体转繁体开关,对所有源生效,默认false,优先级:DANMU_TRADITIONAL > DANMU_SIMPLIFIED", + type: "input", + placeholders: [ + { + title: "false", + value: "false", + }, + { + title: "true", + value: "true", + }, + ], + }, { name: "convertTopBottomToScroll", title: "顶部/底部弹幕转换为浮动弹幕,默认false", @@ -434,7 +449,7 @@ if (typeof window !== 'undefined') { let globals; async function initGlobals(sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie, platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, blockedWords, groupMinute, - danmuLimit, danmuSimplified, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey) { + danmuLimit, danmuSimplified, danmuTraditional, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey) { // 将传入的参数设置到环境变量中,以便Globals可以访问它们 const env = {}; @@ -454,6 +469,7 @@ async function initGlobals(sourceOrder, otherServer, customSourceApiUrl, vodServ if (groupMinute !== undefined) env.GROUP_MINUTE = groupMinute; if (danmuLimit !== undefined) env.DANMU_LIMIT = danmuLimit; if (danmuSimplified !== undefined) env.DANMU_SIMPLIFIED = danmuSimplified; + if (danmuTraditional !== undefined) env.DANMU_TRADITIONAL = danmuTraditional; if (convertTopBottomToScroll !== undefined) env.CONVERT_TOP_BOTTOM_TO_SCROLL = convertTopBottomToScroll; if (convertColor !== undefined) env.CONVERT_COLOR = convertColor; if (proxyUrl !== undefined) env.PROXY_URL = proxyUrl; @@ -524,11 +540,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, - danmuLimit, danmuSimplified, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey } = params; + danmuLimit, danmuSimplified, danmuTraditional, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey } = params; await initGlobals(sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie, platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, blockedWords, groupMinute, - danmuLimit, danmuSimplified, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey); + danmuLimit, danmuSimplified, danmuTraditional, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey); const response = await searchAnime(new URL(`${PREFIX_URL}/api/v2/search/anime?keyword=${title}`)); const resJson = await response.json(); @@ -597,11 +613,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, - danmuLimit, danmuSimplified, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey } = params; + danmuLimit, danmuSimplified, danmuTraditional, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey } = params; await initGlobals(sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie, platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, blockedWords, groupMinute, - danmuLimit, danmuSimplified, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey); + danmuLimit, danmuSimplified, danmuTraditional, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey); const response = await getBangumi(`${PREFIX_URL}/api/v2/bangumi/${animeId}`); const resJson = await response.json(); @@ -616,11 +632,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, - danmuLimit, danmuSimplified, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey } = params; + danmuLimit, danmuSimplified, danmuTraditional, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey } = params; await initGlobals(sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie, platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, blockedWords, groupMinute, - danmuLimit, danmuSimplified, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey); + danmuLimit, danmuSimplified, danmuTraditional, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey); if (commentId) { const storeKey = season && episode ? `${tmdbId}.${season}.${episode}` : `${tmdbId}`; @@ -637,7 +653,7 @@ async function getCommentsById(params) { if (lastCommentId === commentId && segmentList) { return await getDanmuWithSegmentTime({ segmentTime, tmdbId, season, episode, otherServer, customSourceApiUrl, vodServers, bilibiliCookie, sourceOrder, blockedWords, groupMinute, vodReturnMode, vodRequestTimeout, platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, - danmuLimit, danmuSimplified, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey }) + danmuLimit, danmuSimplified, danmuTraditional, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey }) } else { Widget.storage.remove(storeKey); Widget.storage.remove(commentIdKey); @@ -663,11 +679,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, - danmuLimit, danmuSimplified, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey } = params; + danmuLimit, danmuSimplified, danmuTraditional, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey } = params; await initGlobals(sourceOrder, otherServer, customSourceApiUrl, vodServers, vodReturnMode, vodRequestTimeout, bilibiliCookie, platformOrder, episodeTitleFilter, enableEpisodeFilter, strictTitleMatch, titleMappingTable, blockedWords, groupMinute, - danmuLimit, danmuSimplified, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey); + danmuLimit, danmuSimplified, danmuTraditional, convertTopBottomToScroll, convertColor, proxyUrl, tmdbApiKey); const storeKey = season && episode ? `${tmdbId}.${season}.${episode}` : `${tmdbId}`; const segmentList = Widget.storage.get(storeKey); diff --git a/forward/forward-widget.test.js b/forward/forward-widget.test.js index e46caf2..7d94256 100644 --- a/forward/forward-widget.test.js +++ b/forward/forward-widget.test.js @@ -231,6 +231,7 @@ async function testNewFlow() { groupMinute: 1, danmuLimit: 0, danmuSimplified: false, + danmuTraditional: false, convertTopBottomToScroll: false, convertColor: 'default', proxyUrl: '',