From 10b2ca89ae2e9666408a453c6b5fcff8abf81354 Mon Sep 17 00:00:00 2001 From: huangxd- <649924757@qq.com> Date: Sun, 1 Mar 2026 13:46:28 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=9C=AC=E5=9C=B0redis?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E9=85=8D=E7=BD=AE=20(#213)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 弹幕点赞数改成镂空♡显示,数量使用下标数字,并增加环境变量LIKE_SWITCH开关该功能,默认开启 * 增加hanjutv日志打印 * 修改说明 * 支持本地redis缓存配置 * 增加render环境的判断 * Revert "增加render环境的判断" This reverts commit 952257895a2705debf079485c8558969b46f986a. * 修改注释 * 修复cloudflare不支持redis包安装的问题 * 修复cloudflare不支持redis包安装的问题 --- README.md | 10 +- config/.env.example | 13 ++ danmu_api/apis/dandan-api.js | 10 + danmu_api/apis/system-api.js | 14 +- danmu_api/configs/envs.js | 4 + danmu_api/configs/globals.js | 3 +- danmu_api/sources/hanjutv.js | 7 + danmu_api/ui/js/systemsettings.js | 9 +- danmu_api/ui/template.js | 2 +- danmu_api/utils/danmu-util.js | 53 +++-- danmu_api/utils/local-redis-util.js | 313 ++++++++++++++++++++++++++++ danmu_api/worker.js | 6 + danmu_api/worker.test.js | 48 +++++ forward/forward-widget.js | 36 +++- forward/forward-widget.test.js | 11 +- package.json | 3 +- wrangler.toml | 3 +- 17 files changed, 508 insertions(+), 37 deletions(-) create mode 100644 danmu_api/utils/local-redis-util.js 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 = \`
diff --git a/danmu_api/ui/template.js b/danmu_api/ui/template.js index eb5a5ef..d387554 100644 --- a/danmu_api/ui/template.js +++ b/danmu_api/ui/template.js @@ -305,7 +305,7 @@ export const HTML_TEMPLATE = /* html */ ` - +