feat: 添加弹幕转换功能 - 支持顶部/底部和彩色弹幕转换 (#62) (#65)

* 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:
woleigedouer authored and GitHub committed 2025-10-19 23:35:25 +08:00
1 parent cc9e303323
commit 571bc727a6
5 files changed
+314 -62

No files matched your search

+21 -2
View File
@@ -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
# ==================== 缓存配置 ====================
+33 -7
View File
@@ -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
View File
@@ -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
View File
@@ -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};
+1
View File
@@ -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",