Skip to content

Commit f006c0a

Browse files
committed
2 parents 2359aec + 1bdb2a8 commit f006c0a

28 files changed

Lines changed: 1375 additions & 212 deletions

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ LogVar 弹幕 API 服务器
3636
- [使用 Docker 运行](#使用-docker-运行)
3737
- [Docker 一键启动 【推荐】](#docker-一键启动-推荐)
3838
- [部署到 Vercel 【推荐】](#部署到-vercel-推荐)
39-
- [部署到 Netlify 【推荐】](#部署到-netlify-推荐)
39+
- [部署到 Netlify](#部署到-netlify)
4040
- [部署到 腾讯云 edgeone pages](#部署到-腾讯云-edgeone-pages)
4141
- [部署到 Cloudflare](#部署到-cloudflare)
4242
- [部署到 Hugging Face Spaces](#部署到-hugging-face-spaces)
@@ -145,7 +145,7 @@ LogVar 弹幕 API 服务器
145145
```bash
146146
npm start
147147
```
148-
服务器将在 `http://{ip}:9321` 运行,默认token是`87654321`
148+
服务器将在 `http://{ip}:9321` 运行,默认token是`87654321`Node 服务默认通过 `::` 同时监听 IPv6 和 IPv4;不支持 IPv6 绑定时会自动回退到 `0.0.0.0`。IPv6 地址访问格式为 `http://[IPv6地址]:9321`。若操作系统强制启用了 `IPV6_V6ONLY`,需调整系统网络策略后才能通过同一监听端口接受 IPv4 连接。
149149
如需修改端口,可设置环境变量 `DANMU_API_PORT`(例如 `DANMU_API_PORT=8080 npm start`)。
150150
HTTPS 反向代理应传递 `X-Forwarded-Proto`;无法传递时可设置 `DANMU_API_PUBLIC_PROTO=https`,用于生成正确的对外弹幕链接。
151151

@@ -215,6 +215,8 @@ GET http://127.0.0.1:9321/87654321/api/logs
215215
- 使用`-e TOKEN=87654321`设置`TOKEN`环境变量,覆盖Dockerfile中的默认值。
216216
- 或使用 `--env-file .env` 加载 .env 文件中的所有环境变量:`docker run -d -p 9321:9321 --name danmu-api --env-file .env danmu-api`
217217

218+
> 容器内服务默认启用 IPv4/IPv6 双栈监听。通过 IPv6 从宿主机访问时,Docker 守护进程及容器网络也需要启用 IPv6;否则仍可正常使用 IPv4。
219+
218220
**热更新支持**:如需支持环境变量热更新(修改 `.env` 文件后无需重启容器),请使用 Volume 挂载:
219221
```bash
220222
docker run -d -p 9321:9321 --name danmu-api -v $(pwd)/.env:/app/.env --env-file .env danmu-api
@@ -306,7 +308,7 @@ GET http://127.0.0.1:9321/87654321/api/logs
306308
> hk有可能访问不了360或其他源,可以尝试切其他region
307309
- vercel在国内被墙,请配合代理或绑定自定义域名使用
308310

309-
## 部署到 Netlify 【推荐】
311+
## 部署到 Netlify
310312

311313
> ⚠️ **风险提示:Netlify 存在封号风险!**
312314
>
@@ -504,6 +506,7 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数
504506
| AI_API_KEY | 【可选】AI服务的API密钥,用于身份验证,默认为空,需手动填写 |
505507
| AI_MATCH_PROMPT | 【可选】AI匹配提示词,用于自定义AI匹配行为,不填提供默认提示词,提示词如下 |
506508
| USE_BANGUMI_DATA | 【可选】[Bangumi Data](https://github.com/bangumi-data/bangumi-data) 加速匹配开关,默认值:`false`(关闭),开启后将动画元数据缓存至本地或内存中给源调用,提升动画源的检索与匹配速度并解锁隐藏/区域番剧(本地和Docker部署使用时请先挂载.cache目录获得最佳体验,云部署使用时会将数据缓存至临时内存中如果体验不佳请关闭) |
509+
| NIPAPLAY_REPLACE_DANDAN | 【可选】 [NipaPlay](https://github.com/AimesSoft/NipaPlay-Reload) 弹弹302关联弹幕替代开关(用于 dandan 源),默认为`false`(关闭,使用弹弹原生弹幕),可选值:`true`、`false`。开启后 dandan 源以 nipaplay 弹弹302关联弹幕替代弹弹原生弹幕,因使用的是项目链路获取弹幕所以`1.会丢失弹弹平台弹幕` `2.无法获取下架视频` `3.如果关联中有巴哈姆特平台需要确保能够连通巴哈` |
507510

508511
```regex
509512
# EPISODE_TITLE_FILTER 默认值
@@ -718,8 +721,10 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数
718721
│ ├── log-util.js # 日志工具
719722
│ ├── merge-util.js # 源合并处理工具
720723
│ ├── migu-util.js # 咪咕工具
724+
│ ├── nipaplay-util.js # NipaPlay 弹弹302关联链接工具
721725
│ ├── offset-util.js # 弹幕偏移工具
722726
│ ├── redis-util.js # redis工具
727+
│ ├── server-listen-util.js # IPv4/IPv6 双栈监听与 IPv4 回退工具
723728
│ ├── time-util.js # 时间日期工具
724729
│ ├── tmdb-util.js # TMDB API请求处理工具
725730
│ └── zh-util.js # 中文繁简转换工具
@@ -788,6 +793,8 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数
788793
### 特别感谢
789794
- 开源项目 [danmaku-anywhere](https://github.com/Mr-Quin/danmaku-anywhere) 提供的[弹弹play开放平台](https://doc.dandanplay.com/open/)接口
790795
796+
- 开源项目 [NipaPlay-Reload](https://github.com/AimesSoft/NipaPlay-Reload) 提供的[弹弹play开放平台](https://doc.dandanplay.com/open/)302关联链接请求授权
797+
791798
- 开源项目 [animeko](https://github.com/open-ani/animeko) 提供的弹幕API
792799
793800
- 开源项目 [bangumi-data](https://github.com/bangumi-data/bangumi-data) 提供的平台动画元数据

build-forward-widget.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ const forwardRuntimeCompatPlugin = {
6161
namespace: 'forward-node-builtins'
6262
}));
6363

64+
build.onResolve({ filter: /^node:(?:http|https)$/ }, (args) => ({
65+
path: args.path.slice('node:'.length),
66+
namespace: 'forward-node-builtins'
67+
}));
68+
6469
// brotli ships a compressed dictionary specifically for browser bundles.
6570
build.onResolve({ filter: /^\.\/dictionary-data$/ }, (args) => {
6671
if (/[\\/]node_modules[\\/]brotli[\\/]dec[\\/]dictionary\.js$/.test(args.importer)) {
@@ -93,6 +98,11 @@ const forwardRuntimeCompatPlugin = {
9398
`
9499
}));
95100

101+
build.onLoad({ filter: /^(?:http|https)$/, namespace: 'forward-node-builtins' }, () => ({
102+
loader: 'js',
103+
contents: `export default { Agent: class Agent {} };`
104+
}));
105+
96106
build.onLoad({ filter: /^dan-any$/, namespace: 'forward-optional-modules' }, () => ({
97107
loader: 'js',
98108
contents: `
@@ -176,6 +186,7 @@ const forwardRuntimeCompatPlugin = {
176186
],
177187
define: {
178188
'widgetVersion': `"${Globals.VERSION}"`,
189+
'globalThis.__FORWARD_WIDGET__': 'true',
179190
'globalThis.__FORWARD_WIDGET_DEBUG__': debugBuild ? 'true' : 'false'
180191
},
181192
banner: {

config/.env.example

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,18 @@ VOD_REQUEST_TIMEOUT=10000
8585
# 默认值:8
8686
YOUKU_CONCURRENCY=8
8787

88+
# ==================== NipaPlay 弹弹302关联弹幕配置 ====================
89+
90+
# NipaPlay 弹弹302关联弹幕替代开关(用于 dandan 源)
91+
# 默认值:false(关闭,使用弹弹原生弹幕),可选值:true、false
92+
# 说明:
93+
# - 开启后 dandan 源以 nipaplay 弹弹302关联弹幕替代弹弹原生弹幕
94+
# - 因使用的是项目链路获取弹幕所以:
95+
# 1.会丢失弹弹平台弹幕
96+
# 2.无法获取下架视频
97+
# 3.如果关联中有巴哈姆特平台需要确保能够连通巴哈
98+
# NIPAPLAY_REPLACE_DANDAN=false
99+
88100
# ==================== 源排序配置 ====================
89101

90102
# 数据源查询顺序(可选值:360, vod, tmdb, douban, tencent, youku, iqiyi, imgo, bilibili, migu, renren, hanjutv, sohu, leshi, xigua, maiduidui, aiyifan, hongguo, dandan, bahamut, animeko, custom)

danmu_api/apis/dandan-api.js

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1077,10 +1077,12 @@ function findEpisodeByNumber(filteredEpisodes, episode, targetEpisode, platform
10771077
}
10781078

10791079
// 策略2:使用数组索引
1080-
if (platformEpisodes.length >= targetEpisode) {
1080+
if (targetEpisode > 0 && platformEpisodes.length >= targetEpisode) {
10811081
const fallbackEp = platformEpisodes[targetEpisode - 1];
1082-
log("info", `Using fallback array index for episode ${targetEpisode}: ${fallbackEp.episodeTitle}`);
1083-
return fallbackEp;
1082+
if (fallbackEp) {
1083+
log("info", `Using fallback array index for episode ${targetEpisode}: ${fallbackEp.episodeTitle}`);
1084+
return fallbackEp;
1085+
}
10841086
}
10851087

10861088
// 策略3:使用episodeNumber字段匹配
@@ -1346,19 +1348,21 @@ function findCrossSeasonEpisodeMap(searchData, title, year, season, episode, pla
13461348
break;
13471349
}
13481350

1349-
if (currentTargetEpisode <= allEps.length) {
1351+
if (currentTargetEpisode > 0 && currentTargetEpisode <= allEps.length) {
13501352
const targetEp = allEps[currentTargetEpisode - 1];
1351-
if (platform && getPlatformMatchScore(extractEpisodeTitle(targetEp.episodeTitle), platform) === 0) {
1352-
currentSeason++;
1353-
continue;
1353+
if (targetEp) {
1354+
if (platform && getPlatformMatchScore(extractEpisodeTitle(targetEp.episodeTitle), platform) === 0) {
1355+
currentSeason++;
1356+
continue;
1357+
}
1358+
log("info", `[system] [spillover] 跨季溢出查找命中 (按相对排位计算) -> 所在季:S${currentSeason} 集标题:${targetEp.episodeTitle}`);
1359+
bestRes = {
1360+
anime: seasonData.anime,
1361+
episode: targetEp,
1362+
score: platform ? getPlatformMatchScore(seasonData.actualPlatform, platform) : 1
1363+
};
1364+
break;
13541365
}
1355-
log("info", `[system] [spillover] 跨季溢出查找命中 (按相对排位计算) -> 所在季:S${currentSeason} 集标题:${targetEp.episodeTitle}`);
1356-
bestRes = {
1357-
anime: seasonData.anime,
1358-
episode: targetEp,
1359-
score: platform ? getPlatformMatchScore(seasonData.actualPlatform, platform) : 1
1360-
};
1361-
break;
13621366
}
13631367

13641368
// 目标集号超出实际集数但仍落在该季 TMDB 标准区间内: 真实集数偏短时返回该季最后一集, 避免无谓顺延至后续季
@@ -2077,10 +2081,10 @@ export async function matchAnime(url, req, clientIp) {
20772081
// 示例返回
20782082
return jsonResponse(resData);
20792083
} catch (error) {
2080-
// 处理 JSON 解析错误或其他异常
2081-
log("error", `[system] [match] Failed to parse request body: ${error.message}`);
2084+
// 处理匹配请求中的异常
2085+
log("error", `[system] [match] Error processing match request: ${error.stack || error.message}`);
20822086
return jsonResponse(
2083-
{ errorCode: 400, success: false, errorMessage: "Invalid JSON body" },
2087+
{ errorCode: 400, success: false, errorMessage: error.message || "Invalid JSON body" },
20842088
400
20852089
);
20862090
}

danmu_api/apis/env-api.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { jsonResponse } from '../utils/http-util.js';
22
import { log } from '../utils/log-util.js';
33
import { HandlerFactory } from '../configs/handlers/handler-factory.js';
44
import { globals } from '../configs/globals.js';
5+
import { syncBangumiDataLifecycleOnConfigChange } from '../utils/bangumi-data-util.js';
56
import AIClient from '../utils/ai-util.js';
67

78
/**
@@ -24,6 +25,10 @@ export async function handleSetEnv(request) {
2425
// 调用handler的setEnv方法
2526
const result = await handler.setEnv(key, value);
2627

28+
if (result && key === 'USE_BANGUMI_DATA') {
29+
syncBangumiDataLifecycleOnConfigChange(deployPlatform);
30+
}
31+
2732
if (result) {
2833
return jsonResponse({ success: true, message: `环境变量 ${key} 设置成功` });
2934
} else {
@@ -55,6 +60,10 @@ export async function handleAddEnv(request) {
5560
// 调用handler的addEnv方法
5661
const result = await handler.addEnv(key, value);
5762

63+
if (result && key === 'USE_BANGUMI_DATA') {
64+
syncBangumiDataLifecycleOnConfigChange(deployPlatform);
65+
}
66+
5867
if (result) {
5968
return jsonResponse({ success: true, message: `环境变量 ${key} 添加成功` });
6069
} else {
@@ -86,6 +95,10 @@ export async function handleDelEnv(request) {
8695
// 调用handler的delEnv方法
8796
const result = await handler.delEnv(key);
8897

98+
if (result && key === 'USE_BANGUMI_DATA') {
99+
syncBangumiDataLifecycleOnConfigChange(deployPlatform);
100+
}
101+
89102
if (result) {
90103
return jsonResponse({ success: true, message: `环境变量 ${key} 删除成功` });
91104
} else {

danmu_api/apis/system-api.js

Lines changed: 66 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -169,39 +169,62 @@ export function handleClearLogs() {
169169

170170
/**
171171
* 处理清理缓存的请求
172-
* @returns {Response} 表示操作成功的响应
172+
* @param {Request} [req] 可选请求体 { items: string[] },用于指定待清理项;未提供时清理全部已知项
173+
* @returns {Response} 表示操作结果的响应
173174
*/
174-
export async function handleClearCache() {
175-
try {
176-
// 清理 globals 中的缓存数据
177-
globals.animes = [];
178-
globals.episodeIds = [];
179-
globals.episodeNum = 10001; // 重置为初始值
180-
globals.lastSelectMap = new Map(); // 重新创建 Map 对象
181-
globals.reqRecords = []; // 清空请求记录
182-
globals.todayReqNum = 0; // 重置今日请求次数
183-
175+
export async function handleClearCache(req) {
176+
// 各清理项对应的重置逻辑;请求记录与今日计数归入「请求历史记录」项
177+
const clearActions = {
178+
animes: () => { globals.animes = []; },
179+
episodeIds: () => { globals.episodeIds = []; },
180+
episodeNum: () => { globals.episodeNum = 10001; }, // 重置为初始值
181+
lastSelectMap: () => { globals.lastSelectMap = new Map(); }, // 重新创建 Map 对象
184182
// 清理搜索和弹幕缓存
185-
globals.searchCache = new Map();
186-
globals.commentCache = new Map();
187-
globals.requestHistory = new Map();
183+
searchCache: () => { globals.searchCache = new Map(); },
184+
commentCache: () => { globals.commentCache = new Map(); },
185+
requestHistory: () => {
186+
globals.requestHistory = new Map();
187+
globals.reqRecords = []; // 清空请求记录
188+
globals.todayReqNum = 0; // 重置今日请求次数
189+
},
190+
bangumiData: () => {
191+
try {
192+
clearBangumiDataCache(true); // 清理 Bangumi-Data 内存与磁盘缓存
193+
if (globals.useBangumiData) {
194+
// 触发异步数据重载
195+
initBangumiData(globals.deployPlatform, false).catch(e => {
196+
log("warn", `[system] [server] Bangumi-Data background reload failed: ${e.message}`);
197+
});
198+
}
199+
} catch (e) {
200+
log("error", `[system] [server] Failed to clear Bangumi-Data cache: ${e.message}`);
201+
}
202+
}
203+
};
204+
const allItems = Object.keys(clearActions);
188205

206+
let effectiveItems;
207+
if (req) {
208+
let parsed = null;
189209
try {
190-
// 清理 Bangumi-Data 内存与磁盘缓存
191-
clearBangumiDataCache(true);
192-
193-
// 触发异步数据重载
194-
if (globals.useBangumiData) {
195-
initBangumiData(globals.deployPlatform, false).catch(e => {
196-
log("warn", `[system] [server] Bangumi-Data background reload failed: ${e.message}`);
197-
});
198-
}
210+
parsed = await req.json();
199211
} catch (e) {
200-
log("error", `[system] [server] Failed to clear Bangumi-Data cache: ${e.message}`);
212+
parsed = null;
201213
}
202-
214+
const items = parsed && Array.isArray(parsed.items) ? parsed.items : null;
215+
// 仅保留自身属性键,排除 __proto__ 等原型链键,避免误放行导致整次清理失败
216+
effectiveItems = items ? items.filter(key => Object.prototype.hasOwnProperty.call(clearActions, key)) : allItems;
217+
} else {
218+
effectiveItems = allItems;
219+
}
220+
221+
try {
222+
for (const key of effectiveItems) {
223+
clearActions[key]();
224+
}
225+
203226
log("info", `[system] [server] Memory cache cleared successfully`);
204-
227+
205228
// 同步清理本地缓存和Redis缓存
206229
try {
207230
// 如果本地缓存有效,更新本地缓存
@@ -213,7 +236,7 @@ export async function handleClearCache() {
213236
} catch (localError) {
214237
log("warn", `[system] [server] Local cache may not be available: ${localError.message}`);
215238
}
216-
239+
217240
try {
218241
// 如果Redis有效,更新Redis缓存
219242
if (globals.redisValid) {
@@ -235,19 +258,22 @@ export async function handleClearCache() {
235258
} catch (redisError) {
236259
log("warn", `[system] [server] LocalRedis may not be available: ${redisError.message}`);
237260
}
238-
239-
log("info", `[system] [server] All caches cleared successfully`);
240-
return jsonResponse({ success: true, message: "Cache cleared successfully", clearedItems: {
241-
animes: 0,
242-
episodeIds: 0,
243-
episodeNum: 10001,
244-
lastSelectMap: 0,
245-
searchCache: 0,
246-
commentCache: 0,
247-
requestHistory: 0,
248-
reqRecords: 0,
249-
todayReqNum: 0
250-
}}, 200);
261+
262+
const clearedItems = {};
263+
for (const key of effectiveItems) {
264+
if (key === "requestHistory") {
265+
clearedItems.requestHistory = 0;
266+
clearedItems.reqRecords = 0;
267+
clearedItems.todayReqNum = 0;
268+
} else if (key === "episodeNum") {
269+
clearedItems.episodeNum = 10001;
270+
} else {
271+
clearedItems[key] = 0;
272+
}
273+
}
274+
275+
log("info", `[system] [server] Selected caches cleared successfully`);
276+
return jsonResponse({ success: true, message: "Cache cleared successfully", clearedItems }, 200);
251277
} catch (error) {
252278
log("error", `[system] [server] Cache clear failed: ${error.message}`);
253279
return jsonResponse({ success: false, message: `Cache clear failed: ${error.message}` }, 500);

0 commit comments

Comments
 (0)