From 571bc727a6a8836e3d6da2ca5f514bab509dd165 Mon Sep 17 00:00:00 2001 From: woleigedouer <38418090+woleigedouer@users.noreply.github.com> Date: Sun, 19 Oct 2025 23:35:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=BC=B9=E5=B9=95?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2=E5=8A=9F=E8=83=BD=20-=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E9=A1=B6=E9=83=A8/=E5=BA=95=E9=83=A8=E5=92=8C=E5=BD=A9?= =?UTF-8?q?=E8=89=B2=E5=BC=B9=E5=B9=95=E8=BD=AC=E6=8D=A2=20(#62)=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .env.example | 23 ++++- README.md | 40 ++++++-- danmu_api/server.js | 96 +++++++++++++++++++- danmu_api/worker.js | 216 ++++++++++++++++++++++++++++++++++---------- package.json | 1 + 5 files changed, 314 insertions(+), 62 deletions(-) diff --git a/.env.example b/.env.example index 2b71e66..efc7d16 100644 --- a/.env.example +++ b/.env.example @@ -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 # ==================== 缓存配置 ==================== diff --git a/README.md b/README.md index 58f7c9e..44b832a 100644 --- a/README.md +++ b/README.md @@ -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 持久化存储。 diff --git a/danmu_api/server.js b/danmu_api/server.js index 68c64df..73e4cac 100644 --- a/danmu_api/server.js +++ b/danmu_api/server.js @@ -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...'); diff --git a/danmu_api/worker.js b/danmu_api/worker.js index 79edca3..5581d69 100644 --- a/danmu_api/worker.js +++ b/danmu_api/worker.js @@ -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}; diff --git a/package.json b/package.json index 08db7d4..d210bb5 100644 --- a/package.json +++ b/package.json @@ -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",