本地和Docker部署支持实时保存缓存到文件
This commit is contained in:
1 parent
40cfd503b7
commit
4dbe4aff34
5 files changed
+132
-4
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user