diff --git a/.env.example b/.env.example index f4b96c6..2b71e66 100644 --- a/.env.example +++ b/.env.example @@ -103,7 +103,18 @@ ENABLE_EPISODE_FILTER=false # 格式:http://proxy-server:port 或 https://proxy-server:port # PROXY_URL= -# ==================== 搜索结果缓存配置 ==================== +# ==================== 日志配置 ==================== + +# 日志级别(可选值:error, warn, info) +# 默认值:warn +# 说明: +# - error: 仅显示错误日志 +# - warn: 显示错误和警告日志 +# - info: 显示所有日志(包括详细的调试信息) +# 建议:生产环境使用 warn,调试时使用 info +LOG_LEVEL=warn + +# ==================== 缓存配置 ==================== # 搜索结果缓存时间(分钟) # 默认值:1 @@ -112,6 +123,12 @@ ENABLE_EPISODE_FILTER=false # 设置为 0 表示不缓存 SEARCH_CACHE_MINUTES=1 +# 弹幕缓存时间(分钟) +# 默认值:1 +# 说明:弹幕数据的缓存时间,独立于搜索结果缓存 +# 可根据需要调整 +COMMENT_CACHE_MINUTES=1 + # ==================== Redis 配置(Upstash)==================== # Upstash Redis REST URL(用于缓存) diff --git a/README.md b/README.md index 52d5026..58f7c9e 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,11 @@ LogVar 弹幕 API 服务器 - `POST /api/v2/comment/by-url`:通过视频URL直接获取弹幕。 - `GET /api/logs`:获取最近的日志(最多 500 行,格式为 `[时间戳] 级别: 消息`)。 - **日志记录**:捕获 `console.log`(info 级别)和 `console.error`(error 级别),JSON 内容格式化输出。 -- **搜索结果缓存**:支持内存缓存搜索结果和弹幕数据,避免短期内重复的不必要API请求,可通过 `SEARCH_CACHE_MINUTES` 环境变量配置缓存时间(默认1分钟)。 +- **智能缓存管理**:支持内存缓存搜索结果和弹幕数据,避免短期内重复的不必要API请求。包括: + - 搜索结果缓存(可通过 `SEARCH_CACHE_MINUTES` 配置,默认1分钟) + - 弹幕缓存(可通过 `COMMENT_CACHE_MINUTES` 配置,默认5分钟) + - 用户偏好记录(可通过 `MAX_LAST_SELECT_MAP` 配置,默认100条) + - Redis 分布式缓存支持(可选) - **部署支持**:支持本地运行、Docker 容器化、Vercel 一键部署、Netlify 一键部署、Cloudflare 一键部署和 Docker 一键启动。 - **手动选择记忆**:支持记住之前搜索title时手动选择的anime,并在后续的match自动匹配时优选该anime【实验性】。 - **手动搜索支持输入播放链接获取弹幕**:支持手动搜索的播放器输入爱优腾芒哔播放链接可获取弹幕,如`senplayer`。 @@ -263,9 +267,12 @@ LogVar 弹幕 API 服务器 | GROUP_MINUTE | 【可选】合并去重分钟数,表示按n分钟分组后对弹幕合并去重,默认为1,最大值为30,0表示不去重 | | 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` | | SEARCH_CACHE_MINUTES | 【可选】搜索结果缓存时间(分钟),默认为`1`,避免短期内重复的不必要API请求,同时保证获取最新的结果列表,可根据需要调整:Vercel/Cloudflare建议`1-5`分钟,Docker可设置`5-30`分钟,设置为`0`表示不缓存 | -| UPSTASH_REDIS_REST_URL | 【可选】update redis url,需配合UPSTASH_REDIS_REST_TOKEN使用,用于持久化存储,不会因为冷启动而丢失过去的查询信息(在cf/eo/claw上配置后应该能更稳定点,也能解决小幻掉匹配的问题,但会稍微影响请求速度),获取方法请参考:`https://cloud.tencent.cn/developer/article/2424508` | -| UPSTASH_REDIS_REST_TOKEN | 【可选】update redis token,需配合UPSTASH_REDIS_REST_URL使用,用于持久化存储,不会因为冷启动而丢失过去的查询信息(在cf/eo/claw上配置后应该能更稳定点,也能解决小幻掉匹配的问题,但会稍微影响请求速度),获取方法请参考:`https://cloud.tencent.cn/developer/article/2424508` | +| COMMENT_CACHE_MINUTES | 【可选】弹幕缓存时间(分钟),默认为`1`,弹幕数据的缓存时间,独立于搜索结果缓存 | +| 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` | ```regex # EPISODE_TITLE_FILTER 默认值 diff --git a/danmu_api/worker.js b/danmu_api/worker.js index ba6f8b0..cfccc76 100644 --- a/danmu_api/worker.js +++ b/danmu_api/worker.js @@ -26,6 +26,8 @@ let envs = {}; const requestHistory = new Map(); // redis是否生效 let redisValid = false; +// Redis 缓存是否已初始化 +let redisCacheInitialized = false; // 存储查询关键字上次选择的animeId,用于下次match自动匹配时优先选择该anime let lastSelectMap = new Map(); // 存储上一次各变量哈希值 @@ -337,6 +339,26 @@ function resolveEnableEpisodeFilter(env) { return DEFAULT_ENABLE_EPISODE_FILTER; } +// 日志级别配置(默认 warn,可选值:error, warn, info) +const DEFAULT_LOG_LEVEL = "warn"; +let logLevel = DEFAULT_LOG_LEVEL; + +function resolveLogLevel(env) { + let level = DEFAULT_LOG_LEVEL; + if (env && env.LOG_LEVEL) { + level = env.LOG_LEVEL.toLowerCase(); + } else if (typeof process !== "undefined" && process.env?.LOG_LEVEL) { + level = process.env.LOG_LEVEL.toLowerCase(); + } + + // 验证日志级别 + const validLevels = ["error", "warn", "info"]; + if (!validLevels.includes(level)) { + return DEFAULT_LOG_LEVEL; + } + return level; +} + // 搜索结果缓存时间配置(分钟,默认 1) const DEFAULT_SEARCH_CACHE_MINUTES = 1; let searchCacheMinutes = DEFAULT_SEARCH_CACHE_MINUTES; @@ -353,6 +375,22 @@ function resolveSearchCacheMinutes(env) { return DEFAULT_SEARCH_CACHE_MINUTES; } +// 弹幕缓存时间配置(分钟,默认 1) +const DEFAULT_COMMENT_CACHE_MINUTES = 1; +let commentCacheMinutes = DEFAULT_COMMENT_CACHE_MINUTES; + +function resolveCommentCacheMinutes(env) { + if (env && env.COMMENT_CACHE_MINUTES) { + const n = parseInt(env.COMMENT_CACHE_MINUTES, 10); + if (!Number.isNaN(n) && n > 0) return n; + } + if (typeof process !== "undefined" && process.env?.COMMENT_CACHE_MINUTES) { + const n = parseInt(process.env.COMMENT_CACHE_MINUTES, 10); + if (!Number.isNaN(n) && n > 0) return n; + } + return DEFAULT_COMMENT_CACHE_MINUTES; +} + // ===================== // 数据结构处理函数 // ===================== @@ -396,6 +434,7 @@ function setSearchCache(keyword, results) { results: results, timestamp: Date.now() }); + log("info", `Cached search results for "${keyword}" (${results.length} animes)`); } @@ -409,7 +448,7 @@ function isCommentCacheValid(videoUrl) { const now = Date.now(); const cacheAgeMinutes = (now - cached.timestamp) / (1000 * 60); - if (cacheAgeMinutes > searchCacheMinutes) { + if (cacheAgeMinutes > commentCacheMinutes) { // 缓存已过期,删除它 commentCache.delete(videoUrl); log("info", `Comment cache for "${videoUrl}" expired after ${cacheAgeMinutes.toFixed(2)} minutes`); @@ -434,6 +473,7 @@ function setCommentCache(videoUrl, comments) { comments: comments, timestamp: Date.now() }); + log("info", `Cached comments for "${videoUrl}" (${comments.length} comments)`); } @@ -452,6 +492,7 @@ function addEpisode(url, title) { // 添加新对象 episodeIds.push(newEpisode); + log("info", `Added to episodeIds: ${JSON.stringify(newEpisode)}`); return newEpisode; // 返回新添加的对象 } @@ -493,49 +534,57 @@ function findTitleById(id) { // 添加 anime 对象到 animes,并将其 links 添加到 episodeIds function addAnime(anime) { - // 确保 anime 有 links 属性且是数组 - if (!anime.links || !Array.isArray(anime.links)) { - log("error", `Invalid or missing links in anime: ${JSON.stringify(anime)}`); + try { + // 确保 anime 有 links 属性且是数组 + if (!anime.links || !Array.isArray(anime.links)) { + log("error", `Invalid or missing links in anime: ${JSON.stringify(anime)}`); + return false; + } + + // 遍历 links,调用 addEpisode,并收集返回的对象 + const newLinks = []; + anime.links.forEach(link => { + if (link.url) { + const episode = addEpisode(link.url, link.title); + if (episode) { + newLinks.push(episode); // 仅添加成功添加的 episode + } + } else { + log("error", `Invalid link in anime, missing url: ${JSON.stringify(link)}`); + } + }); + + // 创建新的 anime 副本 + const animeCopy = { ...anime, links: newLinks }; + + // 检查是否已存在相同 animeId 的 anime + const existingAnimeIndex = animes.findIndex(a => a.animeId === anime.animeId); + + if (existingAnimeIndex !== -1) { + // 如果存在,先删除旧的 + animes.splice(existingAnimeIndex, 1); + log("info", `Removed old anime at index: ${existingAnimeIndex}`); + } + + // 将新的添加到数组末尾(最新位置) + animes.push(animeCopy); + log("info", `Added anime to latest position: ${anime.animeId}`); + + // 检查是否超过 MAX_ANIMES,超过则删除最早的 + if (animes.length > MAX_ANIMES) { + const removeSuccess = removeEarliestAnime(); + if (!removeSuccess) { + log("error", "Failed to remove earliest anime, but continuing"); + } + } + + log("info", `animes: ${JSON.stringify(animes)}`); + + return true; + } catch (error) { + log("error", `addAnime failed: ${error.message}`); return false; } - - // 遍历 links,调用 addEpisode,并收集返回的对象 - const newLinks = []; - anime.links.forEach(link => { - if (link.url) { - const episode = addEpisode(link.url, link.title); - if (episode) { - newLinks.push(episode); // 仅添加成功添加的 episode - } - } else { - log("error", `Invalid link in anime, missing url: ${JSON.stringify(link)}`); - } - }); - - // 创建新的 anime 副本 - const animeCopy = { ...anime, links: newLinks }; - - // 检查是否已存在相同 animeId 的 anime - const existingAnimeIndex = animes.findIndex(a => a.animeId === anime.animeId); - - if (existingAnimeIndex !== -1) { - // 如果存在,先删除旧的 - animes.splice(existingAnimeIndex, 1); - log("info", `Removed old anime at index: ${existingAnimeIndex}`); - } - - // 将新的添加到数组末尾(最新位置) - animes.push(animeCopy); - log("info", `Added anime to latest position: ${anime.animeId}`); - - // 检查是否超过 MAX_ANIMES,超过则删除最早的 - if (animes.length > MAX_ANIMES) { - removeEarliestAnime(); - } - - log("info", "animes: ", animes); - - return true; } // 删除最早添加的 anime,并从 episodeIds 删除其 links 中的 url @@ -569,24 +618,25 @@ function storeAnimeIdsToMap(curAnimes, key) { } // 保存旧的prefer值 - const oldPrefer = lastSelectMap[key]?.prefer; + const oldValue = lastSelectMap.get(key); + const oldPrefer = oldValue?.prefer; - // 如果key已存在,先删除它(为了更新顺序) - if (lastSelectMap[key]) { - delete lastSelectMap[key]; + // 如果key已存在,先删除它(为了更新顺序,保证 FIFO) + if (lastSelectMap.has(key)) { + lastSelectMap.delete(key); } // 添加新记录,保留prefer字段 - lastSelectMap[key] = { + lastSelectMap.set(key, { animeIds: [...uniqueAnimeIds], ...(oldPrefer !== undefined && { prefer: oldPrefer }) - }; + }); - // 检查大小限制 - const keys = Object.keys(lastSelectMap); - if (keys.length > MAX_LAST_SELECT_MAP) { - // 删除最早的记录(第一个key) - delete lastSelectMap[keys[0]]; + // 检查是否超过 MAX_LAST_SELECT_MAP,超过则删除最早的 + if (lastSelectMap.size > MAX_LAST_SELECT_MAP) { + const firstKey = lastSelectMap.keys().next().value; + lastSelectMap.delete(firstKey); + log("info", `Removed earliest entry from lastSelectMap: ${firstKey}`); } } @@ -604,9 +654,10 @@ function findAnimeIdByCommentId(commentId) { // 通过 animeId 查找 lastSelectMap 中 animeIds 包含该 animeId 的 key,并设置其 prefer 为 animeId function setPreferByAnimeId(animeId) { - for (const key in lastSelectMap) { - if (lastSelectMap[key].animeIds.includes(animeId)) { - lastSelectMap[key].prefer = animeId; + for (const [key, value] of lastSelectMap.entries()) { + if (value.animeIds && value.animeIds.includes(animeId)) { + value.prefer = animeId; + lastSelectMap.set(key, value); // 确保更新被保存 return key; // 返回被修改的 key } } @@ -615,10 +666,11 @@ function setPreferByAnimeId(animeId) { // 通过title查询优选animeId function getPreferAnimeId(title) { - if (!lastSelectMap[title] || !lastSelectMap[title]["prefer"]) { + const value = lastSelectMap.get(title); + if (!value || !value.prefer) { return null; } - return lastSelectMap[title]["prefer"]; + return value.prefer; } // ===================== @@ -839,6 +891,31 @@ async function getPageTitle(url) { } } +// ===================== +// 限流 IP 清理函数 +// ===================== + +// 清理所有过期的 IP 记录(超过 1 分钟没有请求的 IP) +function cleanupExpiredIPs(currentTime) { + const oneMinute = 60 * 1000; + let cleanedCount = 0; + + for (const [ip, timestamps] of requestHistory.entries()) { + const validTimestamps = timestamps.filter(ts => currentTime - ts <= oneMinute); + if (validTimestamps.length === 0) { + requestHistory.delete(ip); + cleanedCount++; + log("info", `[Rate Limit] Cleaned up expired IP record: ${ip}`); + } else if (validTimestamps.length < timestamps.length) { + requestHistory.set(ip, validTimestamps); + } + } + + if (cleanedCount > 0) { + log("info", `[Rate Limit] Cleanup completed: removed ${cleanedCount} expired IP records`); + } +} + // ===================== // upstash redis 读写请求 (先简单实现,不加锁) // ===================== @@ -887,9 +964,18 @@ async function getRedisKey(key) { } } +// 辅助函数:序列化值,处理 Map 对象 +function serializeValue(key, value) { + // 对于 lastSelectMap(Map 对象),需要转换为普通对象后再序列化 + if (key === 'lastSelectMap' && value instanceof Map) { + return JSON.stringify(Object.fromEntries(value)); + } + return JSON.stringify(value); +} + // 使用 POST 发送 SET 命令,仅在值变化时更新 async function setRedisKey(key, value) { - const serializedValue = JSON.stringify(value); + const serializedValue = serializeValue(key, value); const currentHash = simpleHash(serializedValue); // 检查值是否变化 @@ -925,7 +1011,7 @@ async function setRedisKey(key, value) { // 使用 POST 发送 SETEX 命令,仅在值变化时更新 async function setRedisKeyWithExpiry(key, value, expirySeconds) { - const serializedValue = JSON.stringify(value); + const serializedValue = serializeValue(key, value); const currentHash = simpleHash(serializedValue); // 检查值是否变化 @@ -986,58 +1072,100 @@ async function runPipeline(commands) { // 优化后的 getRedisCaches,单次请求获取所有键 async function getRedisCaches() { - if (animes.length === 0) { - log("info", 'getCaches start.'); - const keys = ['animes', 'episodeIds', 'episodeNum', 'lastSelectMap']; - const commands = keys.map(key => ['GET', key]); // 构造 pipeline 命令 - const results = await runPipeline(commands); + if (!redisCacheInitialized) { + try { + log("info", 'getRedisCaches start.'); + const keys = ['animes', 'episodeIds', 'episodeNum', 'lastSelectMap']; + const commands = keys.map(key => ['GET', key]); // 构造 pipeline 命令 + const results = await runPipeline(commands); - // 解析结果,按顺序赋值 - animes = results[0].result ? JSON.parse(results[0].result) : animes; - episodeIds = results[1].result ? JSON.parse(results[1].result) : episodeIds; - episodeNum = results[2].result ? JSON.parse(results[2].result) : episodeNum; - lastSelectMap = results[3].result ? JSON.parse(results[3].result) : lastSelectMap; + // 解析结果,按顺序赋值 + animes = results[0].result ? JSON.parse(results[0].result) : animes; + episodeIds = results[1].result ? JSON.parse(results[1].result) : episodeIds; + episodeNum = results[2].result ? JSON.parse(results[2].result) : episodeNum; - // 更新哈希值 - lastHashes.animes = simpleHash(JSON.stringify(animes)); - lastHashes.episodeIds = simpleHash(JSON.stringify(episodeIds)); - lastHashes.episodeNum = simpleHash(JSON.stringify(episodeNum)); - lastHashes.lastSelectMap = simpleHash(JSON.stringify(lastSelectMap)); + // 恢复 lastSelectMap 并转换为 Map 对象 + const lastSelectMapData = results[3].result ? JSON.parse(results[3].result) : null; + if (lastSelectMapData && typeof lastSelectMapData === 'object') { + lastSelectMap = new Map(Object.entries(lastSelectMapData)); + log("info", `Restored lastSelectMap from Redis with ${lastSelectMap.size} entries`); + } + + // 更新哈希值 + lastHashes.animes = simpleHash(JSON.stringify(animes)); + lastHashes.episodeIds = simpleHash(JSON.stringify(episodeIds)); + lastHashes.episodeNum = simpleHash(JSON.stringify(episodeNum)); + lastHashes.lastSelectMap = simpleHash(JSON.stringify(Object.fromEntries(lastSelectMap))); + + redisCacheInitialized = true; + log("info", 'getRedisCaches completed successfully.'); + } catch (error) { + log("error", `getRedisCaches failed: ${error.message}`, error.stack); + redisCacheInitialized = true; // 标记为已初始化,避免重复尝试 + } } } // 优化后的 updateRedisCaches,仅更新有变化的变量 async function updateRedisCaches() { - log("info", 'updateCaches start.'); - const commands = []; - const updates = []; + try { + log("info", 'updateCaches start.'); + const commands = []; + const updates = []; - // 检查每个变量的哈希值 - const variables = [ - { key: 'animes', value: animes }, - { key: 'episodeIds', value: episodeIds }, - { key: 'episodeNum', value: episodeNum }, - { key: 'lastSelectMap', value: lastSelectMap } - ]; + // 检查每个变量的哈希值 + const variables = [ + { key: 'animes', value: animes }, + { key: 'episodeIds', value: episodeIds }, + { key: 'episodeNum', value: episodeNum }, + { key: 'lastSelectMap', value: lastSelectMap } + ]; - for (const { key, value } of variables) { - const currentHash = simpleHash(JSON.stringify(value)); - if (currentHash !== lastHashes[key]) { - commands.push(['SET', key, JSON.stringify(value)]); - updates.push({ key, hash: currentHash }); + 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 !== lastHashes[key]) { + commands.push(['SET', key, serializedValue]); + updates.push({ key, hash: currentHash }); + } } - } - // 如果有需要更新的键,执行 pipeline - if (commands.length > 0) { - log("info", `Updating ${commands.length} changed keys: ${updates.map(u => u.key).join(', ')}`); - await runPipeline(commands); - // 更新哈希值 - updates.forEach(({ key, hash }) => { - lastHashes[key] = hash; - }); - } else { - log("info", 'No changes detected, skipping Redis update.'); + // 如果有需要更新的键,执行 pipeline + if (commands.length > 0) { + log("info", `Updating ${commands.length} changed keys: ${updates.map(u => u.key).join(', ')}`); + const results = await runPipeline(commands); + + // 检查每个操作的结果 + let successCount = 0; + let failureCount = 0; + + if (Array.isArray(results)) { + results.forEach((result, index) => { + if (result && result.result === 'OK') { + successCount++; + } else { + failureCount++; + log("warn", `Failed to update Redis key: ${updates[index]?.key}, result: ${JSON.stringify(result)}`); + } + }); + } + + // 只有在所有操作都成功时才更新哈希值 + if (failureCount === 0) { + updates.forEach(({ key, hash }) => { + lastHashes[key] = hash; + }); + log("info", `Redis update completed successfully: ${successCount} keys updated`); + } else { + log("warn", `Redis update partially failed: ${successCount} succeeded, ${failureCount} failed`); + } + } else { + log("info", 'No changes detected, skipping Redis update.'); + } + } catch (error) { + log("error", `updateRedisCaches failed: ${error.message}`, error.stack); + log("error", `Error details - Name: ${error.name}, Cause: ${error.cause ? error.cause.message : 'N/A'}`); } } @@ -1092,7 +1220,7 @@ async function get360Animes(title) { ); const data = response.data; - log("info", "360kan response:", data); + log("info", `360kan response: ${JSON.stringify(data)}`); let animes = []; if ('rows' in data.data.longData) { @@ -1128,7 +1256,7 @@ async function get360Zongyi(title, entId, site, year) { ); const data = await response.data; - log("info", "360kan zongyi response:", data); + log("info", `360kan zongyi response: ${JSON.stringify(data)}`); const episodeList = data.data.list; if (!episodeList) { @@ -1273,6 +1401,17 @@ async function getVodAnimesFromFastestServer(title, servers) { // 工具方法 // ===================== +// 生成有效的 ISO 8601 日期格式 +function generateValidStartDate(year) { + // 验证年份是否有效(1900-2100) + const yearNum = parseInt(year); + if (isNaN(yearNum) || yearNum < 1900 || yearNum > 2100) { + // 如果年份无效,使用当前年份 + return `${new Date().getFullYear()}-01-01T00:00:00Z`; + } + return `${yearNum}-01-01T00:00:00Z`; +} + function printFirst200Chars(data) { let dataToPrint; @@ -1449,9 +1588,9 @@ function convertToDanmakuJson(contents, platform) { return null; // 如果不是有效的正则格式则返回 null }).filter(regex => regex !== null); // 过滤掉无效的项 - log("info", "原始屏蔽词字符串:", blockedWords); + log("info", `原始屏蔽词字符串: ${blockedWords}`); const regexArrayToString = array => Array.isArray(array) ? array.map(regex => regex.toString()).join('\n') : String(array); - log("info", "屏蔽词列表:", regexArrayToString(regexArray)); + log("info", `屏蔽词列表: ${regexArrayToString(regexArray)}`); // 过滤列表 const filteredDanmus = danmus.filter(item => { @@ -1459,12 +1598,12 @@ function convertToDanmakuJson(contents, platform) { }); // 按n分钟内去重 - log("info", "去重分钟数:", groupMinute); + log("info", `去重分钟数: ${groupMinute}`); const groupedDanmus = groupDanmusByMinute(filteredDanmus, groupMinute); - log("info", "danmus_original:", danmus.length); - log("info", "danmus_filter:", filteredDanmus.length); - log("info", "danmus_group:", groupedDanmus.length); + 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; @@ -2266,7 +2405,7 @@ async function fetchTencentVideo(inputUrl) { vid = lastPart.split('.')[0]; // 去除文件扩展名 } - log("info", "vid:", vid); + log("info", `vid: ${vid}`); // 获取页面标题 let res; @@ -2285,7 +2424,7 @@ async function fetchTencentVideo(inputUrl) { // 使用正则表达式提取