本地和Docker部署支持实时保存缓存到文件

This commit is contained in:
huangxd- committed 2025-11-15 09:15:29 +08:00
1 parent 40cfd503b7
commit 4dbe4aff34
5 files changed
+132 -4

No files matched your search

+3
View File
@@ -48,6 +48,7 @@ LogVar 弹幕 API 服务器
- 弹幕缓存(可通过 `COMMENT_CACHE_MINUTES` 配置,默认5分钟)
- 用户偏好记录(可通过 `MAX_LAST_SELECT_MAP` 配置,默认100条)
- Redis 分布式缓存支持(可选)
- 本地和Docker部署支持实时保存缓存到文件(挂载.cache目录即可)
- **部署支持**:支持本地运行、Docker 容器化、Vercel 一键部署、Netlify 一键部署、Edgeone 一键部署、Cloudflare 一键部署、Claw部署和 Docker 一键启动。
- **手动选择记忆**:支持记住之前搜索title时手动选择的anime,并在后续的match自动匹配时优选该anime【实验性】。
- **手动搜索支持输入播放链接获取弹幕**:支持手动搜索的播放器输入爱优腾芒哔播放链接可获取弹幕,如`senplayer`。
@@ -164,6 +165,7 @@ LogVar 弹幕 API 服务器
volumes:
- ./.env:/app/.env
- ./config.yaml:/app/config.yaml
- ./.chche:/app/.cache # 配置.chche目录,会将缓存实时保存在本地文件
restart: unless-stopped
```
@@ -491,6 +493,7 @@ danmu_api/
- TMDB源请求逻辑:search tmdb -> tmdbId -> imdbId -> doubanId -> playUrl;优点:emby通过tmdb刮削,标题通过tmdb搜索,返回的信息可能更加匹配;缺点:链条过长,请求时长5-10s左右,中间一环数据有缺失,就没有返回结果。
- TMDB源在SOURCE_ORDER添加tmdb的同时,需要添加TMDB_API_KEY环境变量
- 弹幕分片下载请求已加入重试机制,重试次数为1次
- 如果同时配置了本地缓存和redis缓存,则以redis缓存优先
### 关联项目
[喂饭教程1:danmu_api vercel 自动同步部署方案 - 永远保持最新版本!实时同步原作者更新](https://github.com/xiaoyao20084321/log-var-danmu-deployment-guide)
+14 -2
View File
@@ -4,7 +4,8 @@ import { log } from '../utils/log-util.js'
import { setRedisKey, updateRedisCaches } from "../utils/redis-util.js";
import {
setCommentCache, addAnime, findAnimeIdByCommentId, findTitleById, findUrlById, getCommentCache, getPreferAnimeId,
getSearchCache, removeEarliestAnime, setPreferByAnimeId, setSearchCache, storeAnimeIdsToMap
getSearchCache, removeEarliestAnime, setPreferByAnimeId, setSearchCache, storeAnimeIdsToMap, writeCacheToFile,
updateLocalCaches
} from "../utils/cache-util.js";
import { formatDanmuResponse } from "../utils/danmu-util.js";
import { extractEpisodeTitle, convertChineseNumber, parseFileName, createDynamicPlatformOrder, normalizeSpaces } from "../utils/common-util.js";
@@ -138,6 +139,10 @@ export async function searchAnime(url) {
addAnime(Anime.fromJson({...tmpAnime, links: links}));
if (globals.animes.length > globals.MAX_ANIMES) removeEarliestAnime();
// 如果有新的anime获取到,则更新本地缓存
if (globals.localCacheValid && curAnimes.length !== 0) {
await updateLocalCaches();
}
// 如果有新的anime获取到,则更新redis
if (globals.redisValid && curAnimes.length !== 0) {
await updateRedisCaches();
@@ -276,9 +281,13 @@ export async function searchAnime(url) {
curAnimes.push(...validAnimes);
}
// 如果有新的anime获取到,则更新本地缓存
if (globals.localCacheValid && curAnimes.length !== 0) {
await updateLocalCaches();
}
// 如果有新的anime获取到,则更新redis
if (globals.redisValid && curAnimes.length !== 0) {
await updateRedisCaches();
await updateRedisCaches();
}
// 缓存搜索结果
@@ -788,6 +797,9 @@ export async function getComment(path, queryFormat) {
const animeId = findAnimeIdByCommentId(commentId);
setPreferByAnimeId(animeId);
if (globals.localCacheValid && animeId) {
writeCacheToFile('lastSelectMap', JSON.stringify(Object.fromEntries(globals.lastSelectMap)));
}
if (globals.redisValid && animeId) {
await setRedisKey('lastSelectMap', globals.lastSelectMap);
}
+3 -1
View File
@@ -11,7 +11,7 @@ export const Globals = {
accessedEnvVars: {},
// 静态常量
VERSION: '1.7.5',
VERSION: '1.7.6',
MAX_LOGS: 500, // 日志存储,最多保存 500 行
MAX_ANIMES: 100,
@@ -21,6 +21,8 @@ export const Globals = {
episodeNum: 10001, // 全局变量,用于自增 ID
logBuffer: [],
requestHistory: new Map(), // 记录每个 IP 地址的请求历史
localCacheValid: false, // 本地缓存是否生效
localCacheInitialized: false, // 本地缓存是否已初始化
redisValid: false, // redis是否生效
redisCacheInitialized: false, // redis 缓存是否已初始化
lastSelectMap: new Map(), // 存储查询关键字上次选择的animeId,用于下次match自动匹配时优先选择该anime
+106
View File
@@ -1,6 +1,10 @@
import { globals } from '../configs/globals.js';
import { log } from './log-util.js'
import { Anime } from "../models/dandan-model.js";
import { simpleHash } from "./codec-util.js";
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
// =====================
// cache数据结构处理函数
@@ -303,4 +307,106 @@ export function cleanupExpiredIPs(currentTime) {
if (cleanedCount > 0) {
log("info", `[Rate Limit] Cleanup completed: removed ${cleanedCount} expired IP records`);
}
}
// 从本地缓存目录读取缓存数据
export function readCacheFromFile(key) {
const cacheFilePath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '.cache', `${key}`);
if (fs.existsSync(cacheFilePath)) {
const fileContent = fs.readFileSync(cacheFilePath, 'utf8');
return JSON.parse(fileContent);
}
return null;
}
// 将缓存数据写入本地缓存文件
export function writeCacheToFile(key, value) {
const cacheFilePath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '.cache', `${key}`);
fs.writeFileSync(cacheFilePath, JSON.stringify(value), 'utf8');
}
// 从本地获取缓存
export async function getLocalCaches() {
if (!globals.localCacheInitialized) {
try {
log("info", 'getLocalCaches start.');
// 从本地缓存文件读取数据并恢复到 globals 中
globals.animes = JSON.parse(readCacheFromFile('animes')) || globals.animes;
globals.episodeIds = JSON.parse(readCacheFromFile('episodeIds')) || globals.episodeIds;
globals.episodeNum = JSON.parse(readCacheFromFile('episodeNum')) || globals.episodeNum;
// 恢复 lastSelectMap 并转换为 Map 对象
const lastSelectMapData = readCacheFromFile('lastSelectMap');
if (lastSelectMapData) {
globals.lastSelectMap = new Map(Object.entries(JSON.parse(lastSelectMapData)));
log("info", `Restored lastSelectMap from local cache with ${globals.lastSelectMap.size} entries`);
}
// 更新哈希值
globals.lastHashes.animes = simpleHash(JSON.stringify(globals.animes));
globals.lastHashes.episodeIds = simpleHash(JSON.stringify(globals.episodeIds));
globals.lastHashes.episodeNum = simpleHash(JSON.stringify(globals.episodeNum));
globals.lastHashes.lastSelectMap = simpleHash(JSON.stringify(Object.fromEntries(globals.lastSelectMap)));
globals.localCacheInitialized = true;
log("info", 'getLocalCaches completed successfully.');
} catch (error) {
log("error", `getLocalCaches failed: ${error.message}`, error.stack);
globals.localCacheInitialized = true; // 标记为已初始化,避免重复尝试
}
}
}
// 更新本地缓存
export async function updateLocalCaches() {
try {
log("info", 'updateLocalCaches start.');
const updates = [];
// 检查每个变量的哈希值
const variables = [
{ key: 'animes', value: globals.animes },
{ key: 'episodeIds', value: globals.episodeIds },
{ key: 'episodeNum', value: globals.episodeNum },
{ key: 'lastSelectMap', value: globals.lastSelectMap }
];
for (const { key, value } of variables) {
// 对于 lastSelectMap(Map 对象),需要转换为普通对象后再序列化
const serializedValue = key === 'lastSelectMap' ? JSON.stringify(Object.fromEntries(value)) : JSON.stringify(value);
const currentHash = simpleHash(serializedValue);
if (currentHash !== globals.lastHashes[key]) {
writeCacheToFile(key, serializedValue);
updates.push({ key, hash: currentHash });
}
}
// 输出更新日志
if (updates.length > 0) {
log("info", `Updated local caches for keys: ${updates.map(u => u.key).join(', ')}`);
updates.forEach(({ key, hash }) => {
globals.lastHashes[key] = hash; // 更新本地哈希
});
} else {
log("info", 'No changes detected, skipping local cache update.');
}
} catch (error) {
log("error", `updateLocalCaches failed: ${error.message}`, error.stack);
log("error", `Error details - Name: ${error.name}, Cause: ${error.cause ? error.cause.message : 'N/A'}`);
}
}
// 判断是否有效的本地缓存目录
export function judgeLocalCacheValid(urlPath) {
if (!globals.localCacheValid && urlPath !== "/favicon.ico" && urlPath !== "/robots.txt") {
const cacheDirPath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '.cache');
if (fs.existsSync(cacheDirPath)) {
globals.localCacheValid = true;
} else {
globals.localCacheValid = false;
}
}
}
+6 -1
View File
@@ -2,7 +2,7 @@ import { Globals } from './configs/globals.js';
import { jsonResponse } from './utils/http-util.js';
import { log, formatLogMessage } from './utils/log-util.js'
import { getRedisCaches, judgeRedisValid } from "./utils/redis-util.js";
import { cleanupExpiredIPs, findUrlById, getCommentCache } from "./utils/cache-util.js";
import { cleanupExpiredIPs, findUrlById, getCommentCache, getLocalCaches, judgeLocalCacheValid } from "./utils/cache-util.js";
import { formatDanmuResponse } from "./utils/danmu-util.js";
import { getBangumi, getComment, getCommentByUrl, matchAnime, searchAnime, searchEpisodes } from "./apis/dandan-api.js";
@@ -16,12 +16,16 @@ async function handleRequest(req, env, deployPlatform, clientIp) {
let path = url.pathname;
const method = req.method;
judgeLocalCacheValid(path),
await judgeRedisValid(path);
log("info", `request url: ${JSON.stringify(url)}`);
log("info", `request path: ${path}`);
log("info", `client ip: ${clientIp}`);
if (globals.localCacheValid && path !== "/favicon.ico" && path !== "/robots.txt") {
await getLocalCaches();
}
if (globals.redisValid && path !== "/favicon.ico" && path !== "/robots.txt") {
await getRedisCaches();
}
@@ -33,6 +37,7 @@ async function handleRequest(req, env, deployPlatform, clientIp) {
version: globals.VERSION,
envs: {
...globals.accessedEnvVars,
localCacheValid: globals.localCacheValid,
redisValid: globals.redisValid
},
repository: "https://github.com/huangxd-/danmu_api.git",