diff --git a/README.md b/README.md
index 75da5f2..de4300e 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ LogVar 弹幕 API 服务器
一个人人都能部署的基于 js 的弹幕 API 服务器,支持爱优腾芒哔咪人韩巴狐乐西埋弹幕直接获取,兼容弹弹play的搜索、详情查询和弹幕获取接口规范,并提供日志记录,支持vercel/netlify/edgeone/cloudflare/docker/claw等部署方式,不用提前下载弹幕,没有nas或小鸡也能一键部署。
-本项目仅为个人爱好开发,代码开源。如有任何侵权行为,请联系本人删除。
+本项目仅为个人学习爱好开发,代码开源。如有任何侵权行为,请联系本人删除。
有问题提issue或 [私信机器人](https://t.me/ddjdd_bot) 都ok。
@@ -67,7 +67,7 @@ LogVar 弹幕 API 服务器
- 搜索结果缓存(可通过 `SEARCH_CACHE_MINUTES` 配置,默认1分钟)
- 弹幕缓存(可通过 `COMMENT_CACHE_MINUTES` 配置,默认5分钟)
- 用户偏好记录(可通过 `MAX_LAST_SELECT_MAP` 配置,默认100条)
- - Redis 分布式缓存支持(可选)
+ - Redis 分布式缓存支持,包括本地redis和upstash redis(可选)
- 本地和Docker部署支持实时保存缓存到文件(挂载.cache目录即可)
- **部署支持**:支持本地运行、Docker 容器化、Vercel 一键部署、Netlify 一键部署、Edgeone 一键部署、Cloudflare 一键部署、Claw部署和 Docker 一键启动。
- **手动选择记忆**:支持记住之前搜索title时手动选择的anime,并在后续的match自动匹配时优选该anime【实验性】。
@@ -389,6 +389,7 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数
| DANMU_LIMIT | 【可选】等间隔采样限制弹幕总数,单位为k,即千:默认 0,表示不限制弹幕数,若改为5,弹幕总数在超过5000的情况下会将弹幕数控制在5000 |
| CONVERT_TOP_BOTTOM_TO_SCROLL | 【可选】是否将顶部和底部弹幕转换为浮动弹幕,默认为`false`(不转换),启用后顶部弹幕(ct=5)和底部弹幕(ct=4)会被转换为浮动弹幕(ct=1),可选值:`true`、`false` |
| CONVERT_COLOR | 【可选】弹幕转换颜色配置,默认为`default`(不转换),`white` 将所有非白色的弹幕颜色转换为纯白色,`color` 将所有白色弹幕转换为随机颜色(包含白色),可选值:`default`、`white`、`color` |
+| LIKE_SWITCH | 【可选】弹幕点赞数显示开关,默认为`true`(开启),开启后会在弹幕内容后显示点赞数标记,≥5 才显示,避免低赞干扰 |
| DANMU_OUTPUT_FORMAT | 【可选】弹幕输出格式,默认为`json`,可选值:`json`(JSON格式)、`xml`(XML格式),支持通过查询参数`?format=xml`或`?format=json`覆盖此设置,优先级:查询参数 > 环境变量 > 默认值 |
| DANMU_SIMPLIFIED_TRADITIONAL | 【可选】弹幕简繁体转换设置:default(默认不转换)、simplified(繁转简)、traditional(简转繁) |
| 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中理应都自然能联通,不用填写 |
@@ -401,6 +402,7 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数
| MAX_LAST_SELECT_MAP | 【可选】最后选择映射缓存大小限制,默认为`100`,lastSelectMap最多保存的条目数,超过限制时删除最早的条目(FIFO),用于存储查询关键字上次选择的animeId |
| UPSTASH_REDIS_REST_URL | 【可选】Upstash redis url,需配合UPSTASH_REDIS_REST_TOKEN使用,用于持久化存储,不会因为冷启动而丢失过去的查询信息(在cf/eo/claw上配置后应该能更稳定点,也能解决小幻掉匹配的问题,但会稍微影响请求速度),获取方法请参考:`https://cloud.tencent.cn/developer/article/2424508` |
| UPSTASH_REDIS_REST_TOKEN | 【可选】Upstash redis token,需配合UPSTASH_REDIS_REST_URL使用,用于持久化存储,不会因为冷启动而丢失过去的查询信息(在cf/eo/claw上配置后应该能更稳定点,也能解决小幻掉匹配的问题,但会稍微影响请求速度),获取方法请参考:`https://cloud.tencent.cn/developer/article/2424508` |
+| LOCAL_REDIS_URL | 【可选】本地Redis连接URL,用于本地缓存存储,适用于docker和本地部署环境,格式:`redis://:password@127.0.0.1:6379/0`,默认为空(不使用本地Redis) |
| DEPLOY_PLATFROM_ACCOUNT | 【可选】部署账号ID,调用部署服务API需要,配置后可使用UI界面配置服务,不同部署平台获取方式可查看 [部署平台环境变量配置指南](https://github.com/huangxd-/danmu_api/tree/main/danmu_api/ui/README.md#部署平台环境变量配置指南) ,docker部署和本地node部署并不需要配置 |
| DEPLOY_PLATFROM_PROJECT | 【可选】部署项目名称,调用部署服务API需要,配置后可使用UI界面配置服务,不同部署平台获取方式可查看 [部署平台环境变量配置指南](https://github.com/huangxd-/danmu_api/tree/main/danmu_api/ui/README.md#部署平台环境变量配置指南) ,docker部署和本地node部署并不需要配置 |
| DEPLOY_PLATFROM_TOKEN | 【可选】部署平台token,调用部署服务API需要,配置后可使用UI界面配置服务,不同部署平台获取方式可查看 [部署平台环境变量配置指南](https://github.com/huangxd-/danmu_api/tree/main/danmu_api/ui/README.md#部署平台环境变量配置指南) ,docker部署和本地node部署并不需要配置 |
@@ -591,6 +593,7 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数
│ ├── hanjutv-util.js # 韩剧tv加解密工具
│ ├── http-util.js # 请求工具
│ ├── imdb-util.js # IMDB API请求工具
+│ ├── local-redis-util.js # 本地redis工具
│ ├── log-util.js # 日志工具
│ ├── merge-util.js # 源合并处理工具
│ ├── migu-util.js # 咪咕工具
@@ -622,6 +625,7 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数
- 日志存储在内存中,服务器重启后会清空。
- `/api/logs` 中的 JSON 日志会格式化显示,带缩进以提高可读性。
- 搜索结果和弹幕数据存储在内存中,服务器重启后会清空,可通过配置 `UPSTASH_REDIS_REST_URL` 和 `UPSTASH_REDIS_REST_TOKEN` 启用 Redis 持久化存储。
+- 已支持本地redis,可通过配置 `LOCAL_REDIS_URL` 启用,只支持docker和本地部署环境。
- 搜索结果缓存默认时间为 1 分钟,可通过环境变量 `SEARCH_CACHE_MINUTES` 调整(设置为 0 表示不缓存)。
- 确保 `package.json` 中包含 `node-fetch` 依赖。
- 一键部署需要将项目推送到公开的 Git 仓库(如 GitHub),并更新按钮中的仓库地址。
@@ -638,7 +642,7 @@ API 支持返回 Bilibili 标准 XML 格式的弹幕数据,通过查询参数
- TMDB源请求逻辑:search tmdb -> tmdbId -> imdbId -> doubanId -> playUrl;优点:emby通过tmdb刮削,标题通过tmdb搜索,返回的信息可能更加匹配;缺点:链条过长,请求时长5-10s左右,中间一环数据有缺失,就没有返回结果。
- TMDB源在SOURCE_ORDER添加tmdb的同时,需要添加TMDB_API_KEY环境变量
- 弹幕分片下载请求已加入重试机制,重试次数为1次
-- 如果同时配置了本地缓存和redis缓存,则以redis缓存优先
+- 如果同时配置了本地缓存和upstash redis缓存和本地redis缓存,优先级为本地redis > upstash redis缓存 > 本地缓存
- 弹弹play源用的是第三方接口,感谢开源项目 [danmaku-anywhere](https://github.com/Mr-Quin/danmaku-anywhere)
- 有任何问题,如部署/环境变量配置等,可通过deepwiki对本项目进行提问,链接入口:https://deepwiki.com/huangxd-/danmu_api,其中项目内容一般每周刷新一次
diff --git a/config/.env.example b/config/.env.example
index 87c92b9..f4add15 100644
--- a/config/.env.example
+++ b/config/.env.example
@@ -145,6 +145,12 @@ CONVERT_COLOR=default
# 可选值:default, simplified, traditional
DANMU_SIMPLIFIED_TRADITIONAL=default
+# 弹幕点赞数显示开关
+# 默认值:true(开启)
+# 说明:开启后会在弹幕内容后显示点赞数标记,≥5 才显示,避免低赞干扰
+# 可选值:true, false
+LIKE_SWITCH=true
+
# ==================== 弹幕推送配置 ====================
# 弹幕推送地址
@@ -271,6 +277,13 @@ MAX_LAST_SELECT_MAP=100
# 默认值:空
# UPSTASH_REDIS_REST_TOKEN=
+# ==================== 本地 Redis 配置 ====================
+
+# 本地 Redis 连接 URL(用于本地缓存存储)
+# 格式:redis://:password@127.0.0.1:6379/0
+# 默认值:空(不使用本地 Redis)
+# LOCAL_REDIS_URL=
+
# ==================== 部署平台API接口信息 配置 ====================
# 部署账号ID,调用部署服务API需要,配置后可使用UI界面配置服务
diff --git a/danmu_api/apis/dandan-api.js b/danmu_api/apis/dandan-api.js
index a301cd5..ae9ef48 100644
--- a/danmu_api/apis/dandan-api.js
+++ b/danmu_api/apis/dandan-api.js
@@ -3,6 +3,7 @@ import { getPageTitle, jsonResponse, httpGet } from '../utils/http-util.js';
import { log } from '../utils/log-util.js'
import { simplized } from '../utils/zh-util.js';
import { setRedisKey, updateRedisCaches } from "../utils/redis-util.js";
+import { setLocalRedisKey, updateLocalRedisCaches } from "../utils/local-redis-util.js";
import {
setCommentCache, addAnime, findAnimeIdByCommentId, findTitleById, findUrlById, getCommentCache, getPreferAnimeId,
getSearchCache, removeEarliestAnime, setPreferByAnimeId, setSearchCache, storeAnimeIdsToMap, writeCacheToFile,
@@ -201,6 +202,9 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null
if (globals.redisValid && curAnimes.length !== 0) {
await updateRedisCaches();
}
+ if (globals.localRedisValid && curAnimes.length !== 0) {
+ await updateLocalRedisCaches();
+ }
return jsonResponse({
errorCode: 0,
@@ -382,6 +386,9 @@ export async function searchAnime(url, preferAnimeId = null, preferSource = null
if (globals.redisValid && curAnimes.length !== 0) {
await updateRedisCaches();
}
+ if (globals.localRedisValid && curAnimes.length !== 0) {
+ await updateLocalRedisCaches();
+ }
// 缓存搜索结果
if (curAnimes.length > 0) {
@@ -1389,6 +1396,9 @@ export async function getComment(path, queryFormat, segmentFlag) {
if (globals.redisValid && animeId) {
setRedisKey('lastSelectMap', globals.lastSelectMap).catch(e => log("error", "Redis set error", e));
}
+ if (globals.localRedisValid && animeId) {
+ setLocalRedisKey('lastSelectMap', globals.lastSelectMap);
+ }
}
// 缓存弹幕结果
diff --git a/danmu_api/apis/system-api.js b/danmu_api/apis/system-api.js
index 5801d37..545c8c1 100644
--- a/danmu_api/apis/system-api.js
+++ b/danmu_api/apis/system-api.js
@@ -32,6 +32,7 @@ export function handleConfig(hasPermission = false) {
...globals.accessedEnvVars,
localCacheValid: globals.localCacheValid,
redisValid: globals.redisValid,
+ localRedisValid: globals.localRedisValid,
aiValid: globals.aiValid,
deployPlatform: globals.deployPlatform
};
@@ -76,7 +77,7 @@ export function handleConfig(hasPermission = false) {
hasAdminToken: hasAdminToken, // 添加admin token配置状态
repository: "https://github.com/huangxd-/danmu_api.git",
description: "一个人人都能部署的基于 js 的弹幕 API 服务器,支持爱优腾芒哔咪人韩巴狐乐西埋弹幕直接获取,兼容弹弹play的搜索、详情查询和弹幕获取接口规范,并提供日志记录,支持vercel/netlify/edgeone/cloudflare/docker/claw等部署方式,不用提前下载弹幕,没有nas或小鸡也能一键部署。",
- notice: "本项目仅为个人爱好开发,代码开源。如有任何侵权行为,请联系本人删除。有问题提issue或私信机器人都ok,TG MSG ROBOT: [https://t.me/ddjdd_bot]; 推荐加互助群咨询,TG GROUP: [https://t.me/logvar_danmu_group]; 关注频道获取最新更新内容,TG CHANNEL: [https://t.me/logvar_danmu_channel]。"
+ notice: "本项目仅为个人学习爱好开发,代码开源。如有任何侵权行为,请联系本人删除。有问题提issue或私信机器人都ok,TG MSG ROBOT: [https://t.me/ddjdd_bot]; 推荐加互助群咨询,TG GROUP: [https://t.me/logvar_danmu_group]; 关注频道获取最新更新内容,TG CHANNEL: [https://t.me/logvar_danmu_channel]。"
});
}
@@ -195,6 +196,17 @@ export async function handleClearCache() {
} catch (redisError) {
log("warn", `[server] Redis may not be available: ${redisError.message}`);
}
+
+ try {
+ // 如果本地Redis有效,更新本地Redis缓存
+ if (globals.localRedisValid) {
+ const { updateLocalRedisCaches } = await import("../utils/local-redis-util.js");
+ await updateLocalRedisCaches();
+ log("info", `[server] LocalRedis cache cleared successfully`);
+ }
+ } catch (redisError) {
+ log("warn", `[server] LocalRedis may not be available: ${redisError.message}`);
+ }
log("info", `[server] All caches cleared successfully`);
return jsonResponse({ success: true, message: "Cache cleared successfully", clearedItems: {
diff --git a/danmu_api/configs/envs.js b/danmu_api/configs/envs.js
index 1ab87d8..f78738e 100644
--- a/danmu_api/configs/envs.js
+++ b/danmu_api/configs/envs.js
@@ -401,6 +401,7 @@ export class Envs {
'CONVERT_COLOR': { category: 'danmu', type: 'select', options: ['default', 'white', 'color'], description: '弹幕转换颜色配置' },
'DANMU_OUTPUT_FORMAT': { category: 'danmu', type: 'select', options: ['json', 'xml'], description: '弹幕输出格式,默认json' },
'DANMU_PUSH_URL': { category: 'danmu', type: 'text', description: '弹幕推送地址,示例 http://127.0.0.1:9978/action?do=refresh&type=danmaku&path= ' },
+ 'LIKE_SWITCH': { category: 'danmu', type: 'boolean', description: '弹幕点赞数显示开关,默认开启' },
// 缓存配置
'SEARCH_CACHE_MINUTES': { category: 'cache', type: 'number', description: '搜索结果缓存时间(分钟),默认1', min: 1, max: 120 },
@@ -409,6 +410,7 @@ export class Envs {
'MAX_LAST_SELECT_MAP': { category: 'cache', type: 'number', description: '记住上次选择映射缓存大小限制', min: 10, max: 1000 },
'UPSTASH_REDIS_REST_URL': { category: 'cache', type: 'text', description: 'Upstash Redis请求链接' },
'UPSTASH_REDIS_REST_TOKEN': { category: 'cache', type: 'text', description: 'Upstash Redis访问令牌' },
+ 'LOCAL_REDIS_URL': { category: 'cache', type: 'text', description: '本地 Redis 连接URL,示例:redis://:password@127.0.0.1:6379/0,只支持本地部署和docker部署' },
// 系统配置
'PROXY_URL': { category: 'system', type: 'text', description: '代理/反代地址' },
@@ -443,9 +445,11 @@ export class Envs {
proxyUrl: this.get('PROXY_URL', '', 'string', true), // 代理/反代地址
danmuSimplifiedTraditional: this.get('DANMU_SIMPLIFIED_TRADITIONAL', 'default', 'string'), // 弹幕简繁体转换设置:default(默认不转换)、simplified(繁转简)、traditional(简转繁)
danmuPushUrl: this.get('DANMU_PUSH_URL', '', 'string'), // 代理/反代地址
+ likeSwitch: this.get('LIKE_SWITCH', true, 'boolean'), // 弹幕点赞数显示开关,默认开启
tmdbApiKey: this.get('TMDB_API_KEY', '', 'string', true), // TMDB API KEY
redisUrl: this.get('UPSTASH_REDIS_REST_URL', '', 'string', true), // upstash redis url
redisToken: this.get('UPSTASH_REDIS_REST_TOKEN', '', 'string', true), // upstash redis url
+ localRedisUrl: this.get('LOCAL_REDIS_URL', '', 'string', true), // 本地 Redis 连接URL,示例:redis://:password@127.0.0.1:6379/0,只支持本地部署和docker部署
rateLimitMaxRequests: this.get('RATE_LIMIT_MAX_REQUESTS', 3, 'number'), // 限流配置:时间窗口内最大请求次数(默认 3,0表示不限流)
enableAnimeEpisodeFilter: this.get('ENABLE_ANIME_EPISODE_FILTER', false, 'boolean'), // 控制手动搜索的时候是否根据ANIME_TITLE_FILTER进行剧名过滤以及根据EPISODE_TITLE_FILTER进行集标题过滤(默认 false,禁用过滤)
logLevel: this.get('LOG_LEVEL', 'info', 'string'), // 日志级别配置(默认 info,可选值:error, warn, info)
diff --git a/danmu_api/configs/globals.js b/danmu_api/configs/globals.js
index 1ffa7ea..b0ddd84 100644
--- a/danmu_api/configs/globals.js
+++ b/danmu_api/configs/globals.js
@@ -13,7 +13,7 @@ export const Globals = {
accessedEnvVars: {},
// 静态常量
- VERSION: '1.15.2',
+ VERSION: '1.15.3',
MAX_LOGS: 1000, // 日志存储,最多保存 1000 行
MAX_ANIMES: 100,
MAX_RECORDS: 100, // 请求记录最大数量
@@ -27,6 +27,7 @@ export const Globals = {
localCacheValid: false, // 本地缓存是否生效
localCacheInitialized: false, // 本地缓存是否已初始化
redisValid: false, // redis是否生效
+ localRedisValid: false, // 本地redis是否生效
aiValid: false, // AI配置是否生效
redisCacheInitialized: false, // redis 缓存是否已初始化
lastSelectMap: new Map(), // 存储查询关键字上次选择的animeId,用于下次match自动匹配时优先选择该anime
diff --git a/danmu_api/sources/hanjutv.js b/danmu_api/sources/hanjutv.js
index c9ea088..b4889ff 100644
--- a/danmu_api/sources/hanjutv.js
+++ b/danmu_api/sources/hanjutv.js
@@ -145,6 +145,8 @@ export default class HanjutvSource extends BaseSource {
sidSet.add(sid);
resultList.push(item);
}
+
+ const pluckNames = (list) => list?.map(item => item.name) || [];
return {
resultList,
@@ -153,6 +155,10 @@ export default class HanjutvSource extends BaseSource {
s5Matched: s5Matched.length,
webTotal: webCandidates.length,
webMatched: webMatched.length,
+ s5MatchedList: pluckNames(s5Matched),
+ s5UnmatchedList: pluckNames(s5Unmatched),
+ webMatchedList: pluckNames(webMatched),
+ webUnmatchedList: pluckNames(webUnmatched)
}
};
}
@@ -240,6 +246,7 @@ export default class HanjutvSource extends BaseSource {
return [];
}
+ log("info", `[Hanjutv] 搜索候选统计 s5MatchedList=${JSON.stringify(stats.s5MatchedList)}, s5UnmatchedList=${JSON.stringify(stats.s5UnmatchedList)}, webMatchedList=${JSON.stringify(stats.webMatchedList)}, webMatchedList=${JSON.stringify(stats.webUnmatchedList)}`);
log("info", `[Hanjutv] 搜索候选统计 s5=${stats.s5Total}(命中${stats.s5Matched}), web=${stats.webTotal}(命中${stats.webMatched})`);
log("info", `[Hanjutv] 搜索找到 ${resultList.length} 个有效结果`);
diff --git a/danmu_api/ui/js/systemsettings.js b/danmu_api/ui/js/systemsettings.js
index 0d99dae..8aa5e7d 100644
--- a/danmu_api/ui/js/systemsettings.js
+++ b/danmu_api/ui/js/systemsettings.js
@@ -392,7 +392,14 @@ function renderValueInput(item) {
if (type === 'boolean') {
// 布尔开关
- const checked = value === 'true' || value === true;
+ // 对于LIKE_SWITCH变量,默认值设为true(开启状态)
+ let checked;
+ if (currentKey === 'LIKE_SWITCH' || currentKey === 'REMEMBER_LAST_SELECT') {
+ // 如果值为空或未定义,LIKE_SWITCH和REMEMBER_LAST_SELECT默认为true(开启)
+ checked = value === 'true' || value === true || (value === '' || value === undefined || value === null);
+ } else {
+ checked = value === 'true' || value === true;
+ }
container.innerHTML = \`