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: '',