* feat: 添加弹幕转换功能 - 支持将顶部/底部弹幕转换为浮动弹幕(CONVERT_TOP_BOTTOM_TO_SCROLL) - 支持将彩色弹幕转换为纯白弹幕(CONVERT_COLOR_TO_WHITE) - 解决部分播放器不支持顶部/底部弹幕和彩色弹幕的问题 - 优化 convertToDanmakuJson() 函数逻辑 - 转换在去重后执行,性能开销极小(< 0.5 微秒/条) - 已通过多平台真实测试(腾讯、爱奇艺、优酷、芒果) - 更新 .env 和 README.md 文档 * 修复 bahamut episodes 处理错误并添加 b23.tv 短链接解析功能 * fix: 移除 worker.js 中的 dotenv require 以修复 Cloudflare Workers 部署问题 * feat: 添加环境变量热更新功能 - 实现 .env 文件监听和自动重载 - 添加 200ms 防抖机制避免重复触发 - 支持文件删除事件处理 - 实现优雅关闭和资源清理 - 修复 dotenv 不覆盖已存在环境变量的问题(添加 override: true) - 添加 chokidar 依赖支持文件监听 - 更新文档说明热更新使用方法 影响范围: - Node.js 本地运行环境 - Docker 容器环境(需 Volume 挂载) - 不影响 Cloudflare/Vercel/Netlify 部署 * 调整日志级别默认值为 info * Update worker.js
This commit is contained in:
1 parent
cc9e303323
commit
571bc727a6
5 files changed
+314
-62
No files matched your search
+21
-2
@@ -1,5 +1,10 @@
|
||||
# 弹幕 API 环境变量配置示例
|
||||
# 复制此文件为 .env 并根据需要修改配置
|
||||
#
|
||||
# 热更新说明:
|
||||
# - Docker 环境:修改宿主机的 .env 文件后,容器会自动检测并重新加载配置(无需重启容器)
|
||||
# - Node.js 本地环境:修改 .env 文件后,应用会自动检测并重新加载配置(无需重启应用)
|
||||
# - Cloudflare/Vercel/Netlify:需要在平台的环境变量设置中修改,然后重新部署
|
||||
|
||||
# ==================== 基础配置 ====================
|
||||
|
||||
@@ -80,6 +85,20 @@ SOURCE_ORDER=360,vod,renren,hanjutv
|
||||
# 默认值:1
|
||||
GROUP_MINUTE=1
|
||||
|
||||
# ==================== 弹幕转换配置 ====================
|
||||
|
||||
# 是否将顶部和底部弹幕转换为浮动弹幕
|
||||
# 默认值:false(不转换)
|
||||
# 说明:启用后,顶部弹幕(ct=5)和底部弹幕(ct=4)会被转换为浮动弹幕(ct=1)
|
||||
# 可选值:true, false
|
||||
CONVERT_TOP_BOTTOM_TO_SCROLL=false
|
||||
|
||||
# 是否将彩色弹幕转换为纯白弹幕
|
||||
# 默认值:false(不转换)
|
||||
# 说明:启用后,所有非白色的弹幕颜色会被转换为纯白色(16777215)
|
||||
# 可选值:true, false
|
||||
CONVERT_COLOR_TO_WHITE=false
|
||||
|
||||
# ==================== 限流配置 ====================
|
||||
|
||||
# API 限流:1分钟内同一IP最大请求次数
|
||||
@@ -111,8 +130,8 @@ ENABLE_EPISODE_FILTER=false
|
||||
# - error: 仅显示错误日志
|
||||
# - warn: 显示错误和警告日志
|
||||
# - info: 显示所有日志(包括详细的调试信息)
|
||||
# 建议:生产环境使用 warn,调试时使用 info
|
||||
LOG_LEVEL=warn
|
||||
# 建议:生产环境使用 warn,开发/调试时使用 info(默认)
|
||||
LOG_LEVEL=info
|
||||
|
||||
# ==================== 缓存配置 ====================
|
||||
|
||||
|
||||
@@ -45,6 +45,10 @@ LogVar 弹幕 API 服务器
|
||||
- **部署支持**:支持本地运行、Docker 容器化、Vercel 一键部署、Netlify 一键部署、Cloudflare 一键部署和 Docker 一键启动。
|
||||
- **手动选择记忆**:支持记住之前搜索title时手动选择的anime,并在后续的match自动匹配时优选该anime【实验性】。
|
||||
- **手动搜索支持输入播放链接获取弹幕**:支持手动搜索的播放器输入爱优腾芒哔播放链接可获取弹幕,如`senplayer`。
|
||||
- **弹幕转换功能**:支持通过环境变量配置弹幕转换规则,包括:
|
||||
- 将顶部和底部弹幕转换为浮动弹幕(`CONVERT_TOP_BOTTOM_TO_SCROLL`)
|
||||
- 将彩色弹幕转换为纯白弹幕(`CONVERT_COLOR_TO_WHITE`)
|
||||
- 解决部分播放器不支持顶部/底部弹幕和彩色弹幕的问题
|
||||
|
||||
## 前置条件
|
||||
- Node.js(v18.0.0 或更高版本;理论兼容更低版本,请自行测试)
|
||||
@@ -68,6 +72,9 @@ LogVar 弹幕 API 服务器
|
||||
npm start
|
||||
```
|
||||
服务器将在 `http://{ip}:9321` 运行,默认token是`87654321`。
|
||||
|
||||
**热更新支持**:修改 `.env` 文件后,应用会自动检测并重新加载配置(无需重启应用)。
|
||||
|
||||
或者使用下面的命令
|
||||
```bash
|
||||
# 启动
|
||||
@@ -100,6 +107,13 @@ LogVar 弹幕 API 服务器
|
||||
- 使用`-e TOKEN=87654321`设置`TOKEN`环境变量,覆盖Dockerfile中的默认值。
|
||||
- 或使用 `--env-file .env` 加载 .env 文件中的所有环境变量:`docker run -d -p 9321:9321 --name danmu-api --env-file .env danmu-api`
|
||||
|
||||
**热更新支持**:如需支持环境变量热更新(修改 `.env` 文件后无需重启容器),请使用 Volume 挂载:
|
||||
```bash
|
||||
docker run -d -p 9321:9321 --name danmu-api -v $(pwd)/.env:/app/danmu_api/.env --env-file .env danmu-api
|
||||
```
|
||||
|
||||
> **推荐**:使用 docker compose 部署可以更方便地管理配置和支持热更新,详见下方"Docker 一键启动"部分。
|
||||
|
||||
3. **测试 API**:
|
||||
使用 `http://{ip}:9321/{TOKEN}` 访问上述 API 接口。
|
||||
|
||||
@@ -116,19 +130,22 @@ LogVar 弹幕 API 服务器
|
||||
- 使用`-e TOKEN=87654321`设置`TOKEN`环境变量。
|
||||
- 或使用 `--env-file .env` 加载 .env 文件中的所有环境变量:`docker run -d -p 9321:9321 --name danmu-api --env-file .env logvar/danmu-api:latest`
|
||||
|
||||
或使用 docker compose 部署:
|
||||
**热更新支持**:如需支持环境变量热更新(修改 `.env` 文件后无需重启容器),请使用 Volume 挂载:
|
||||
```bash
|
||||
docker run -d -p 9321:9321 --name danmu-api -v $(pwd)/.env:/app/danmu_api/.env --env-file .env logvar/danmu-api:latest
|
||||
```
|
||||
|
||||
或使用 docker compose 部署(**推荐,支持环境变量热更新**):
|
||||
```yaml
|
||||
services:
|
||||
danmu-api:
|
||||
image: logvar/danmu-api:latest
|
||||
container_name: danmu-api
|
||||
ports:
|
||||
- "9321:9321"
|
||||
environment:
|
||||
- TOKEN=87654321 # 请将 87654321 替换为你想自定义的 Token 值
|
||||
# 如需使用 .env 文件,取消下行注释并注释掉上面的 environment 部分
|
||||
# env_file: .env
|
||||
restart: unless-stopped # 可选配置,容器退出时自动重启(非必需,可根据需求删除)
|
||||
# 热更新支持:挂载 .env 文件,修改后容器会自动重新加载配置(无需重启容器)
|
||||
volumes:
|
||||
- ./.env:/app/danmu_api/.env
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
可以使用 watchtower 监控有新版本自动更新:
|
||||
@@ -265,6 +282,8 @@ LogVar 弹幕 API 服务器
|
||||
| ENABLE_EPISODE_FILTER | 【可选】是否在手动选择接口中启用集标题过滤,默认为`false`(禁用),启用后 GET /api/v2/bangumi/{id} 和 GET /api/v2/search/anime 接口会过滤掉预告、花絮等特殊集,以及名称包含特殊关键词的动漫。 |
|
||||
| BLOCKED_WORDS | 【可选】弹幕屏蔽词列表,默认为空,示例如下 |
|
||||
| GROUP_MINUTE | 【可选】合并去重分钟数,表示按n分钟分组后对弹幕合并去重,默认为1,最大值为30,0表示不去重 |
|
||||
| CONVERT_TOP_BOTTOM_TO_SCROLL | 【可选】是否将顶部和底部弹幕转换为浮动弹幕,默认为`false`(不转换),启用后顶部弹幕(ct=5)和底部弹幕(ct=4)会被转换为浮动弹幕(ct=1),可选值:`true`、`false` |
|
||||
| CONVERT_COLOR_TO_WHITE | 【可选】是否将彩色弹幕转换为纯白弹幕,默认为`false`(不转换),启用后所有非白色的弹幕颜色会被转换为纯白色(16777215),可选值:`true`、`false` |
|
||||
| PROXY_URL | 【可选】代理地址,示例: `http://127.0.0.1:7897` ,目前只对巴哈姆特生效(注意:如果巴哈姆特请求不通,会拖慢搜索返回速度,所以除vercel/netlify/cloudflare之外默认不开启bahamut源,开启请先在SOURCE_ORDER环境变量中添加`bahamut`)如果你使用docker部署并且访问不了bahamut源,请配置代理地址;vercel/netlify/cf中理应都自然能联通,不用填写 |
|
||||
| RATE_LIMIT_MAX_REQUESTS | 【可选】限流配置:1分钟内同一IP最大请求次数,默认为`3`,设置为`0`表示不限流 |
|
||||
| LOG_LEVEL | 【可选】日志级别,默认为`warn`,可选值:`error`(仅错误)、`warn`(错误和警告)、`info`(所有日志),生产环境建议使用`warn`,调试时使用`info` |
|
||||
@@ -346,6 +365,13 @@ danmu_api/
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 热更新相关
|
||||
- **本地运行**:修改 `.env` 文件后,应用会自动检测并重新加载配置(无需重启应用)。
|
||||
- **Docker 部署**:需要使用 Volume 挂载 `.env` 文件才能支持热更新。推荐使用 docker compose 部署(见"Docker 一键启动"部分),配置 Volume 后修改 `.env` 文件容器会自动重新加载配置。
|
||||
- **Vercel/Netlify/Cloudflare**:需要在平台的环境变量设置中修改,然后重新部署才能生效。
|
||||
|
||||
### 其他注意事项
|
||||
- 日志存储在内存中,服务器重启后会清空。
|
||||
- `/api/logs` 中的 JSON 日志会格式化显示,带缩进以提高可读性。
|
||||
- 搜索结果和弹幕数据存储在内存中,服务器重启后会清空,可通过配置 `UPSTASH_REDIS_REST_URL` 和 `UPSTASH_REDIS_REST_TOKEN` 启用 Redis 持久化存储。
|
||||
|
||||
+91
-5
@@ -1,13 +1,93 @@
|
||||
// server.js - 智能服务器启动器:根据 Node.js 环境自动选择最优启动模式
|
||||
|
||||
// 加载 .env 文件中的环境变量(本地开发时使用)
|
||||
try {
|
||||
require('dotenv').config();
|
||||
console.log('[server] .env file loaded successfully');
|
||||
} catch (e) {
|
||||
console.log('[server] dotenv not available or .env file not found, using system environment variables');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const dotenv = require('dotenv');
|
||||
|
||||
const envPath = path.join(__dirname, '.env');
|
||||
|
||||
function loadEnv() {
|
||||
try {
|
||||
dotenv.config({ path: envPath, override: true });
|
||||
console.log('[server] .env file loaded successfully');
|
||||
} catch (e) {
|
||||
console.log('[server] dotenv not available or .env file not found, using system environment variables');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始加载
|
||||
loadEnv();
|
||||
|
||||
// 监听 .env 文件变化(仅在文件存在时)
|
||||
let envWatcher = null;
|
||||
let reloadTimer = null;
|
||||
|
||||
function setupEnvWatcher() {
|
||||
if (!fs.existsSync(envPath)) {
|
||||
console.log('[server] .env file not found, skipping file watcher');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const chokidar = require('chokidar');
|
||||
envWatcher = chokidar.watch(envPath, {
|
||||
persistent: true,
|
||||
awaitWriteFinish: {
|
||||
stabilityThreshold: 100,
|
||||
pollInterval: 100
|
||||
}
|
||||
});
|
||||
|
||||
envWatcher.on('change', () => {
|
||||
// 防抖:避免短时间内多次触发
|
||||
if (reloadTimer) {
|
||||
clearTimeout(reloadTimer);
|
||||
}
|
||||
|
||||
reloadTimer = setTimeout(() => {
|
||||
console.log(`[server] .env file changed, reloading environment variables...`);
|
||||
|
||||
// 清除 dotenv 缓存并重新加载环境变量
|
||||
delete require.cache[require.resolve('dotenv')];
|
||||
loadEnv();
|
||||
|
||||
console.log('[server] Environment variables reloaded successfully');
|
||||
reloadTimer = null;
|
||||
}, 200); // 200ms 防抖
|
||||
});
|
||||
|
||||
envWatcher.on('unlink', () => {
|
||||
console.log('[server] .env file deleted, using default environment variables');
|
||||
});
|
||||
|
||||
envWatcher.on('error', (error) => {
|
||||
console.log('[server] File watcher error:', error.message);
|
||||
});
|
||||
|
||||
console.log('[server] .env file watcher started');
|
||||
} catch (e) {
|
||||
console.log('[server] chokidar not available, .env hot reload disabled');
|
||||
}
|
||||
}
|
||||
|
||||
// 优雅关闭:清理文件监听器
|
||||
function cleanupWatcher() {
|
||||
if (envWatcher) {
|
||||
console.log('[server] Closing file watcher...');
|
||||
envWatcher.close();
|
||||
envWatcher = null;
|
||||
}
|
||||
if (reloadTimer) {
|
||||
clearTimeout(reloadTimer);
|
||||
reloadTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 监听进程退出信号
|
||||
process.on('SIGTERM', cleanupWatcher);
|
||||
process.on('SIGINT', cleanupWatcher);
|
||||
|
||||
// 导入 ES module 兼容层(始终加载,但内部会根据需要启用)
|
||||
require('./esm-shim');
|
||||
|
||||
@@ -190,6 +270,9 @@ function createProxyServer() {
|
||||
function startServerSync() {
|
||||
console.log('[server] Starting server synchronously (optimal path)');
|
||||
|
||||
// 设置 .env 文件监听
|
||||
setupEnvWatcher();
|
||||
|
||||
// 启动主业务服务器 (9321)
|
||||
const server = createServer();
|
||||
server.listen(9321, '0.0.0.0', () => {
|
||||
@@ -209,6 +292,9 @@ async function startServerAsync() {
|
||||
try {
|
||||
console.log('[server] Starting server asynchronously (compatibility mode for Node.js <20.19.0 + node-fetch v3)');
|
||||
|
||||
// 设置 .env 文件监听
|
||||
setupEnvWatcher();
|
||||
|
||||
// 预加载 node-fetch v3(解决特定环境下 node-fetch v3 的加载问题)
|
||||
if (typeof global.loadNodeFetch === 'function') {
|
||||
console.log('[server] Pre-loading node-fetch v3...');
|
||||
|
||||
+168
-48
@@ -1,15 +1,6 @@
|
||||
// 加载 .env 文件(仅在 Node.js 环境中有效,Cloudflare Workers 会忽略)
|
||||
if (typeof process !== "undefined" && process.env && !process.env.CLOUDFLARE_ENVIRONMENT) {
|
||||
try {
|
||||
require('dotenv').config();
|
||||
} catch (e) {
|
||||
// dotenv 未安装或加载失败,继续执行
|
||||
}
|
||||
}
|
||||
|
||||
// 全局状态(Cloudflare 和 Vercel 都可能重用实例)
|
||||
// ⚠️ 不是持久化存储,每次冷启动会丢失
|
||||
const VERSION = "1.4.1";
|
||||
const VERSION = "1.4.2";
|
||||
let animes = [];
|
||||
let episodeIds = [];
|
||||
let episodeNum = 10001; // 全局变量,用于自增 ID
|
||||
@@ -339,8 +330,8 @@ function resolveEnableEpisodeFilter(env) {
|
||||
return DEFAULT_ENABLE_EPISODE_FILTER;
|
||||
}
|
||||
|
||||
// 日志级别配置(默认 warn,可选值:error, warn, info)
|
||||
const DEFAULT_LOG_LEVEL = "warn";
|
||||
// 日志级别配置(默认 info,可选值:error, warn, info)
|
||||
const DEFAULT_LOG_LEVEL = "info";
|
||||
let logLevel = DEFAULT_LOG_LEVEL;
|
||||
|
||||
function resolveLogLevel(env) {
|
||||
@@ -391,6 +382,34 @@ function resolveCommentCacheMinutes(env) {
|
||||
return DEFAULT_COMMENT_CACHE_MINUTES;
|
||||
}
|
||||
|
||||
// 顶部/底部弹幕转换为浮动弹幕配置(默认 false,禁用转换)
|
||||
const DEFAULT_CONVERT_TOP_BOTTOM_TO_SCROLL = false;
|
||||
let convertTopBottomToScroll = DEFAULT_CONVERT_TOP_BOTTOM_TO_SCROLL;
|
||||
|
||||
function resolveConvertTopBottomToScroll(env) {
|
||||
if (env && env.CONVERT_TOP_BOTTOM_TO_SCROLL !== undefined) {
|
||||
return env.CONVERT_TOP_BOTTOM_TO_SCROLL.toLowerCase() === 'true';
|
||||
}
|
||||
if (typeof process !== "undefined" && process.env?.CONVERT_TOP_BOTTOM_TO_SCROLL !== undefined) {
|
||||
return process.env.CONVERT_TOP_BOTTOM_TO_SCROLL.toLowerCase() === 'true';
|
||||
}
|
||||
return DEFAULT_CONVERT_TOP_BOTTOM_TO_SCROLL;
|
||||
}
|
||||
|
||||
// 彩色弹幕转换为纯白弹幕配置(默认 false,禁用转换)
|
||||
const DEFAULT_CONVERT_COLOR_TO_WHITE = false;
|
||||
let convertColorToWhite = DEFAULT_CONVERT_COLOR_TO_WHITE;
|
||||
|
||||
function resolveConvertColorToWhite(env) {
|
||||
if (env && env.CONVERT_COLOR_TO_WHITE !== undefined) {
|
||||
return env.CONVERT_COLOR_TO_WHITE.toLowerCase() === 'true';
|
||||
}
|
||||
if (typeof process !== "undefined" && process.env?.CONVERT_COLOR_TO_WHITE !== undefined) {
|
||||
return process.env.CONVERT_COLOR_TO_WHITE.toLowerCase() === 'true';
|
||||
}
|
||||
return DEFAULT_CONVERT_COLOR_TO_WHITE;
|
||||
}
|
||||
|
||||
// =====================
|
||||
// 数据结构处理函数
|
||||
// =====================
|
||||
@@ -1525,25 +1544,20 @@ function convertToDanmakuJson(contents, platform) {
|
||||
|
||||
for (const item of items) {
|
||||
let attributes, m;
|
||||
let time, mode, color;
|
||||
|
||||
// 新增:处理新格式的弹幕数据
|
||||
if ("progress" in item && "mode" in item && "content" in item) {
|
||||
// 处理新格式的弹幕对象
|
||||
attributes = [
|
||||
(item.progress / 1000).toFixed(2), // progress 转换为秒
|
||||
item.mode || 1,
|
||||
item.color || 16777215,
|
||||
`[${platform}]`
|
||||
].join(",");
|
||||
time = (item.progress / 1000).toFixed(2);
|
||||
mode = item.mode || 1;
|
||||
color = item.color || 16777215;
|
||||
m = item.content;
|
||||
} else if ("timepoint" in item) {
|
||||
// 处理对象数组输入
|
||||
attributes = [
|
||||
parseFloat(item.timepoint).toFixed(2),
|
||||
item.ct || 0,
|
||||
item.color || 16777215,
|
||||
`[${platform}]`
|
||||
].join(",");
|
||||
time = parseFloat(item.timepoint).toFixed(2);
|
||||
mode = item.ct || 0;
|
||||
color = item.color || 16777215;
|
||||
m = item.content;
|
||||
} else {
|
||||
if (!("p" in item)) {
|
||||
@@ -1551,24 +1565,23 @@ function convertToDanmakuJson(contents, platform) {
|
||||
}
|
||||
// 处理 XML 解析后的格式
|
||||
const pValues = item.p.split(",");
|
||||
time = parseFloat(pValues[0]).toFixed(2);
|
||||
mode = pValues[1] || 0;
|
||||
if (pValues.length === 4) {
|
||||
attributes = [
|
||||
parseFloat(pValues[0]).toFixed(2),
|
||||
pValues[1] || 0,
|
||||
pValues[2] || 16777215,
|
||||
`[${platform}]`
|
||||
].join(",");
|
||||
color = pValues[2] || 16777215;
|
||||
} else {
|
||||
attributes = [
|
||||
parseFloat(pValues[0]).toFixed(2),
|
||||
pValues[1] || 0,
|
||||
pValues[3] || 16777215,
|
||||
`[${platform}]`
|
||||
].join(",");
|
||||
color = pValues[3] || 16777215;
|
||||
}
|
||||
m = item.m;
|
||||
}
|
||||
|
||||
attributes = [
|
||||
time,
|
||||
mode,
|
||||
color,
|
||||
`[${platform}]`
|
||||
].join(",");
|
||||
|
||||
danmus.push({ p: attributes, m, cid: cidCounter++ });
|
||||
}
|
||||
|
||||
@@ -1601,12 +1614,56 @@ function convertToDanmakuJson(contents, platform) {
|
||||
log("info", `去重分钟数: ${groupMinute}`);
|
||||
const groupedDanmus = groupDanmusByMinute(filteredDanmus, groupMinute);
|
||||
|
||||
// 应用弹幕转换规则(在去重之后)
|
||||
let convertedDanmus = groupedDanmus;
|
||||
if (convertTopBottomToScroll || convertColorToWhite) {
|
||||
let topBottomCount = 0;
|
||||
let colorCount = 0;
|
||||
|
||||
convertedDanmus = groupedDanmus.map(danmu => {
|
||||
const pValues = danmu.p.split(',');
|
||||
if (pValues.length < 3) return danmu;
|
||||
|
||||
let mode = parseInt(pValues[1], 10);
|
||||
let color = parseInt(pValues[2], 10);
|
||||
let modified = false;
|
||||
|
||||
// 1. 将顶部/底部弹幕转换为浮动弹幕
|
||||
if (convertTopBottomToScroll && (mode === 4 || mode === 5)) {
|
||||
topBottomCount++;
|
||||
mode = 1;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// 2. 将彩色弹幕转换为纯白弹幕
|
||||
if (convertColorToWhite && color !== 16777215) {
|
||||
colorCount++;
|
||||
color = 16777215;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (modified) {
|
||||
const newP = [pValues[0], mode, color, ...pValues.slice(3)].join(',');
|
||||
return { ...danmu, p: newP };
|
||||
}
|
||||
return danmu;
|
||||
});
|
||||
|
||||
// 统计输出转换结果
|
||||
if (topBottomCount > 0) {
|
||||
log("info", `[danmu convert] 转换了 ${topBottomCount} 条顶部/底部弹幕为浮动弹幕`);
|
||||
}
|
||||
if (colorCount > 0) {
|
||||
log("info", `[danmu convert] 转换了 ${colorCount} 条彩色弹幕为纯白弹幕`);
|
||||
}
|
||||
}
|
||||
|
||||
log("info", `danmus_original: ${danmus.length}`);
|
||||
log("info", `danmus_filter: ${filteredDanmus.length}`);
|
||||
log("info", `danmus_group: ${groupedDanmus.length}`);
|
||||
// 输出前五条弹幕
|
||||
log("info", "Top 5 danmus:", JSON.stringify(groupedDanmus.slice(0, 5), null, 2));
|
||||
return groupedDanmus;
|
||||
log("info", "Top 5 danmus:", JSON.stringify(convertedDanmus.slice(0, 5), null, 2));
|
||||
return convertedDanmus;
|
||||
}
|
||||
|
||||
function buildQueryString(params) {
|
||||
@@ -2818,6 +2875,47 @@ async function fetchMangoTV(inputUrl) {
|
||||
return convertToDanmakuJson(contents, "mango");
|
||||
}
|
||||
|
||||
// =====================
|
||||
// 解析 b23.tv 短链接
|
||||
// =====================
|
||||
|
||||
async function resolveB23Link(shortUrl) {
|
||||
try {
|
||||
log("info", `正在解析 b23.tv 短链接: ${shortUrl}`);
|
||||
|
||||
// 设置超时时间(默认5秒)
|
||||
const timeout = parseInt(process.env.VOD_REQUEST_TIMEOUT || '5000');
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
// 使用原生 fetch 获取重定向后的 URL
|
||||
// fetch 默认会自动跟踪重定向,response.url 会是最终的 URL
|
||||
const response = await fetch(shortUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||
},
|
||||
signal: controller.signal,
|
||||
redirect: 'follow'
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// 获取最终的 URL(重定向后的 URL)
|
||||
const finalUrl = response.url;
|
||||
if (finalUrl && finalUrl !== shortUrl) {
|
||||
log("info", `b23.tv 短链接已解析为: ${finalUrl}`);
|
||||
return finalUrl;
|
||||
}
|
||||
|
||||
log("error", "无法解析 b23.tv 短链接");
|
||||
return shortUrl; // 如果解析失败,返回原 URL
|
||||
} catch (error) {
|
||||
log("error", "解析 b23.tv 短链接失败:", error);
|
||||
return shortUrl; // 如果出错,返回原 URL
|
||||
}
|
||||
}
|
||||
|
||||
// =====================
|
||||
// 获取bilibili弹幕
|
||||
// =====================
|
||||
@@ -4764,15 +4862,25 @@ async function handleBahamutAnimes(animesBahamut, queryTitle, curAnimes) {
|
||||
.map(async (anime) => {
|
||||
const epData = await getBahamutEpisodes(anime.video_sn);
|
||||
const detail = epData.video;
|
||||
const eps = epData.anime.episodes["0"]
|
||||
|
||||
// 处理 episodes 对象中的多个键("0", "1", "2" 等)
|
||||
// 某些内容(如电影)可能在不同的键中
|
||||
let eps = null;
|
||||
if (epData.anime.episodes) {
|
||||
// 优先使用 "0" 键,如果不存在则使用第一个可用的键
|
||||
eps = epData.anime.episodes["0"] || Object.values(epData.anime.episodes)[0];
|
||||
}
|
||||
|
||||
let links = [];
|
||||
for (const ep of eps) {
|
||||
const epTitle = `第${ep.episode}集`;
|
||||
links.push({
|
||||
"name": ep.episode,
|
||||
"url": ep.videoSn.toString(),
|
||||
"title": `【bahamut】 ${epTitle}`
|
||||
});
|
||||
if (eps && Array.isArray(eps)) {
|
||||
for (const ep of eps) {
|
||||
const epTitle = `第${ep.episode}集`;
|
||||
links.push({
|
||||
"name": ep.episode,
|
||||
"url": ep.videoSn.toString(),
|
||||
"title": `【bahamut】 ${epTitle}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (links.length > 0) {
|
||||
@@ -5485,7 +5593,11 @@ async function getComment(path) {
|
||||
danmus = await fetchIqiyi(url);
|
||||
} else if (url.includes('.mgtv.com')) {
|
||||
danmus = await fetchMangoTV(url);
|
||||
} else if (url.includes('.bilibili.com')) {
|
||||
} else if (url.includes('.bilibili.com') || url.includes('b23.tv')) {
|
||||
// 如果是 b23.tv 短链接,先解析为完整 URL
|
||||
if (url.includes('b23.tv')) {
|
||||
url = await resolveB23Link(url);
|
||||
}
|
||||
danmus = await fetchBilibili(url);
|
||||
} else if (url.includes('.youku.com')) {
|
||||
danmus = await fetchYouku(url);
|
||||
@@ -5579,6 +5691,10 @@ async function getCommentByUrl(req) {
|
||||
} else if (url.includes('.mgtv.com')) {
|
||||
danmus = await fetchMangoTV(url);
|
||||
} else if (url.includes('.bilibili.com') || url.includes('b23.tv')) {
|
||||
// 如果是 b23.tv 短链接,先解析为完整 URL
|
||||
if (url.includes('b23.tv')) {
|
||||
url = await resolveB23Link(url);
|
||||
}
|
||||
danmus = await fetchBilibili(url);
|
||||
} else if (url.includes('.youku.com')) {
|
||||
danmus = await fetchYouku(url);
|
||||
@@ -5649,6 +5765,10 @@ async function handleRequest(req, env, deployPlatform, clientIp) {
|
||||
envs["searchCacheMinutes"] = searchCacheMinutes;
|
||||
commentCacheMinutes = resolveCommentCacheMinutes(env);
|
||||
envs["commentCacheMinutes"] = commentCacheMinutes;
|
||||
convertTopBottomToScroll = resolveConvertTopBottomToScroll(env);
|
||||
envs["convertTopBottomToScroll"] = convertTopBottomToScroll;
|
||||
convertColorToWhite = resolveConvertColorToWhite(env);
|
||||
envs["convertColorToWhite"] = convertColorToWhite;
|
||||
redisUrl = resolveRedisUrl(env);
|
||||
envs["redisUrl"] = encryptStr(redisUrl);
|
||||
redisToken = resolveRedisToken(env);
|
||||
@@ -5934,4 +6054,4 @@ export { handleRequest, searchAnime, searchEpisodes, matchAnime, getBangumi, get
|
||||
fetchMangoTV, fetchBilibili, fetchYouku, fetchOtherServer, httpGet, httpPost, hanjutvSearch, getHanjutvEpisodes,
|
||||
getHanjutvComments, getHanjutvDetail, bahamutSearch, getBahamutEpisodes, getBahamutComments, tencentSearch, getTencentEpisodes,
|
||||
pingRedis, getRedisKey, setRedisKey, setRedisKeyWithExpiry, getSearchCache, setSearchCache, isSearchCacheValid,
|
||||
getCommentCache, setCommentCache, isCommentCacheValid};
|
||||
getCommentCache, setCommentCache, isCommentCacheValid, resolveB23Link};
|
||||
@@ -9,6 +9,7 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.3",
|
||||
"dotenv": "^16.4.7",
|
||||
"esbuild": "^0.25.10",
|
||||
"https-proxy-agent": "^7.0.6",
|
||||
|
||||
Reference in new issue
Block a user