修复芒果tv弹幕没有彩色弹幕的问题并优化upstash redis请求 (#60)
* 修复芒果tv弹幕没有彩色弹幕的问题 * 优化upstash redis请求,增加hash缓存,减少调用量
This commit is contained in:
1 parent
cc3ffe6b0b
commit
5df03312d0
1 file changed
+216
-61
+216
-61
@@ -28,6 +28,13 @@ const requestHistory = new Map();
|
||||
let redisValid = false;
|
||||
// 存储查询关键字上次选择的animeId,用于下次match自动匹配时优先选择该anime
|
||||
let lastSelectMap = new Map();
|
||||
// 存储上一次各变量哈希值
|
||||
let lastHashes = {
|
||||
animes: null,
|
||||
episodeIds: null,
|
||||
episodeNum: null,
|
||||
lastSelectMap: null
|
||||
};
|
||||
// 搜索结果缓存,存储格式:{ keyword: { results, timestamp } }
|
||||
const searchCache = new Map();
|
||||
// 弹幕缓存,存储格式:{ videoUrl: { comments, timestamp } }
|
||||
@@ -526,7 +533,7 @@ function addAnime(anime) {
|
||||
removeEarliestAnime();
|
||||
}
|
||||
|
||||
console.log("animes: ", animes);
|
||||
log("info", "animes: ", animes);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -839,6 +846,7 @@ async function getPageTitle(url) {
|
||||
// 使用 GET 发送简单命令(如 PING 检查连接)
|
||||
async function pingRedis() {
|
||||
const url = `${redisUrl}/ping`;
|
||||
log("info", `[redis] 开始发送 PING 请求:`, url);
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
@@ -860,6 +868,7 @@ async function pingRedis() {
|
||||
// 使用 GET 发送 GET 命令(读取键值)
|
||||
async function getRedisKey(key) {
|
||||
const url = `${redisUrl}/get/${key}`;
|
||||
log("info", `[redis] 开始发送 GET 请求:`, url);
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
@@ -878,9 +887,19 @@ async function getRedisKey(key) {
|
||||
}
|
||||
}
|
||||
|
||||
// 使用 POST 发送 SET 命令(写入键值,值在请求体中)
|
||||
// 使用 POST 发送 SET 命令,仅在值变化时更新
|
||||
async function setRedisKey(key, value) {
|
||||
const serializedValue = JSON.stringify(value);
|
||||
const currentHash = simpleHash(serializedValue);
|
||||
|
||||
// 检查值是否变化
|
||||
if (lastHashes[key] === currentHash) {
|
||||
log("info", `[redis] 键 ${key} 无变化,跳过 SET 请求`);
|
||||
return { result: "OK" }; // 模拟成功响应
|
||||
}
|
||||
|
||||
const url = `${redisUrl}/set/${key}`;
|
||||
log("info", `[redis] 开始发送 SET 请求:`, url);
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
@@ -888,22 +907,35 @@ async function setRedisKey(key, value) {
|
||||
'Authorization': `Bearer ${redisToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(value) // 值作为 body 发送,支持 JSON 或字符串
|
||||
body: serializedValue
|
||||
});
|
||||
return await response.json(); // 预期: ["OK"]
|
||||
const result = await response.json();
|
||||
lastHashes[key] = currentHash; // 更新哈希值
|
||||
log("info", `[redis] 键 ${key} 更新成功`);
|
||||
return result; // 预期: ["OK"]
|
||||
} catch (error) {
|
||||
log("error", `[redis] 请求失败:`, error.message);
|
||||
log("error", `[redis] SET 请求失败:`, error.message);
|
||||
log("error", '- 错误类型:', error.name);
|
||||
if (error.cause) {
|
||||
log("error", '- 码:', error.cause.code); // e.g., 'ECONNREFUSED', 'ETIMEDOUT', 'ECONNRESET'
|
||||
log("error", '- 码:', error.cause.code);
|
||||
log("error", '- 原因:', error.cause.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 复杂命令,如 SETEX(设置键值并过期,使用查询参数)
|
||||
// 使用 POST 发送 SETEX 命令,仅在值变化时更新
|
||||
async function setRedisKeyWithExpiry(key, value, expirySeconds) {
|
||||
const serializedValue = JSON.stringify(value);
|
||||
const currentHash = simpleHash(serializedValue);
|
||||
|
||||
// 检查值是否变化
|
||||
if (lastHashes[key] === currentHash) {
|
||||
log("info", `[redis] 键 ${key} 无变化,跳过 SETEX 请求`);
|
||||
return { result: "OK" }; // 模拟成功响应
|
||||
}
|
||||
|
||||
const url = `${redisUrl}/set/${key}?EX=${expirySeconds}`;
|
||||
log("info", `[redis] 开始发送 SETEX 请求:`, url);
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
@@ -911,49 +943,102 @@ async function setRedisKeyWithExpiry(key, value, expirySeconds) {
|
||||
'Authorization': `Bearer ${redisToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(value)
|
||||
body: serializedValue
|
||||
});
|
||||
return await response.json();
|
||||
const result = await response.json();
|
||||
lastHashes[key] = currentHash; // 更新哈希值
|
||||
log("info", `[redis] 键 ${key} 更新成功(带过期时间 ${expirySeconds}s)`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
log("error", `[redis] 请求失败:`, error.message);
|
||||
log("error", `[redis] SETEX 请求失败:`, error.message);
|
||||
log("error", '- 错误类型:', error.name);
|
||||
if (error.cause) {
|
||||
log("error", '- 码:', error.cause.code); // e.g., 'ECONNREFUSED', 'ETIMEDOUT', 'ECONNRESET'
|
||||
log("error", '- 码:', error.cause.code);
|
||||
log("error", '- 原因:', error.cause.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从redis获取变量数据
|
||||
async function getCaches() {
|
||||
if (animes.length === 0) {
|
||||
log("info", 'getCaches start.');
|
||||
const [kv_animes, kv_episodeIds, kv_episodeNum, kv_logBuffer, kv_lastSelectMap] = await Promise.all([
|
||||
getRedisKey('animes'),
|
||||
getRedisKey('episodeIds'),
|
||||
getRedisKey('episodeNum'),
|
||||
getRedisKey('logBuffer'),
|
||||
getRedisKey('lastSelectMap'),
|
||||
]);
|
||||
|
||||
animes = kv_animes.result ? JSON.parse(kv_animes.result) : animes;
|
||||
episodeIds = kv_episodeIds.result ? JSON.parse(kv_episodeIds.result) : episodeIds;
|
||||
episodeNum = kv_episodeNum.result ? JSON.parse(kv_episodeNum.result) : episodeNum;
|
||||
logBuffer = kv_logBuffer.result ? JSON.parse(kv_logBuffer.result) : logBuffer;
|
||||
lastSelectMap = kv_lastSelectMap.result ? JSON.parse(kv_lastSelectMap.result) : lastSelectMap;
|
||||
// 通用的 pipeline 请求函数
|
||||
async function runPipeline(commands) {
|
||||
const url = `${redisUrl}/pipeline`;
|
||||
log("info", `[redis] 开始发送 PIPELINE 请求:`, url);
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${redisToken}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(commands) // commands 是一个数组,包含多个 Redis 命令
|
||||
});
|
||||
const result = await response.json();
|
||||
return result; // 返回结果数组,按命令顺序
|
||||
} catch (error) {
|
||||
log("error", `[redis] Pipeline 请求失败:`, error.message);
|
||||
log("error", '- 错误类型:', error.name);
|
||||
if (error.cause) {
|
||||
log("error", '- 码:', error.cause.code);
|
||||
log("error", '- 原因:', error.cause.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 存储更新后的变量到redis
|
||||
async function updateCaches() {
|
||||
log("info", 'updateCaches start.');
|
||||
await Promise.all([
|
||||
setRedisKey('animes', animes),
|
||||
setRedisKey('episodeIds', episodeIds),
|
||||
setRedisKey('episodeNum', episodeNum),
|
||||
setRedisKey('logBuffer', logBuffer),
|
||||
setRedisKey('lastSelectMap', lastSelectMap)
|
||||
]);
|
||||
// 优化后的 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);
|
||||
|
||||
// 解析结果,按顺序赋值
|
||||
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;
|
||||
|
||||
// 更新哈希值
|
||||
lastHashes.animes = simpleHash(JSON.stringify(animes));
|
||||
lastHashes.episodeIds = simpleHash(JSON.stringify(episodeIds));
|
||||
lastHashes.episodeNum = simpleHash(JSON.stringify(episodeNum));
|
||||
lastHashes.lastSelectMap = simpleHash(JSON.stringify(lastSelectMap));
|
||||
}
|
||||
}
|
||||
|
||||
// 优化后的 updateRedisCaches,仅更新有变化的变量
|
||||
async function updateRedisCaches() {
|
||||
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 }
|
||||
];
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有需要更新的键,执行 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.');
|
||||
}
|
||||
}
|
||||
|
||||
// =====================
|
||||
@@ -1793,6 +1878,30 @@ function encryptStr(str) {
|
||||
return '*'.repeat(str.length);
|
||||
}
|
||||
|
||||
// RGB 转整数的函数
|
||||
function rgbToInt(color) {
|
||||
// 检查 RGB 值是否有效
|
||||
if (
|
||||
typeof color.r !== 'number' || color.r < 0 || color.r > 255 ||
|
||||
typeof color.g !== 'number' || color.g < 0 || color.g > 255 ||
|
||||
typeof color.b !== 'number' || color.b < 0 || color.b > 255
|
||||
) {
|
||||
return -1;
|
||||
}
|
||||
return color.r * 256 * 256 + color.g * 256 + color.b;
|
||||
}
|
||||
|
||||
// 简单的字符串哈希函数
|
||||
function simpleHash(str) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash >>> 0; // 确保为无符号 32 位整数
|
||||
}
|
||||
return hash.toString(16); // 转换为十六进制
|
||||
}
|
||||
|
||||
// =====================
|
||||
// 腾讯视频搜索和分集
|
||||
// =====================
|
||||
@@ -2423,7 +2532,7 @@ async function fetchMangoTV(inputUrl) {
|
||||
|
||||
// 弹幕和视频信息 API 基础地址
|
||||
const api_video_info = "https://pcweb.api.mgtv.com/video/info";
|
||||
const api_danmaku = "https://galaxy.bz.mgtv.com/rdbarrage";
|
||||
const api_ctl_barrage = "https://galaxy.bz.mgtv.com/getctlbarrage";
|
||||
|
||||
// 解析 URL 获取 cid 和 vid
|
||||
// 手动解析 URL(没有 URL 对象的情况下)
|
||||
@@ -2454,7 +2563,7 @@ async function fetchMangoTV(inputUrl) {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
log("info", "请求视频信息失败:", error);
|
||||
log("error", "请求视频信息失败:", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -2464,23 +2573,64 @@ async function fetchMangoTV(inputUrl) {
|
||||
log("info", "标题:", title);
|
||||
|
||||
// 计算弹幕分段请求
|
||||
const step = 60 * 1000; // 每60秒一个分段
|
||||
const end_time = time_to_second(time) * 1000; // 将视频时长转换为毫秒
|
||||
const promises = [];
|
||||
for (let i = 0; i < end_time; i += step) {
|
||||
const danmakuUrl = `${api_danmaku}?vid=${vid}&cid=${cid}&time=${i}`;
|
||||
promises.push(
|
||||
httpGet(danmakuUrl, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||
},
|
||||
})
|
||||
);
|
||||
try {
|
||||
const ctlBarrageUrl = `${api_ctl_barrage}?version=8.1.39&abroad=0&uuid=&os=10.15.7&platform=0&mac=&vid=${vid}&pid=&cid=${cid}&ticket=`;
|
||||
const res = await httpGet(ctlBarrageUrl, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||
},
|
||||
});
|
||||
const ctlBarrage = typeof res.data === "string" ? JSON.parse(res.data) : res.data;
|
||||
|
||||
// 每1分钟一个分段
|
||||
for (let i = 0; i < Math.ceil(time_to_second(time) / 60); i += 1) {
|
||||
const danmakuUrl = `https://${ctlBarrage.data?.cdn_list.split(',')[0]}/${ctlBarrage.data?.cdn_version}/${i}.json`;
|
||||
promises.push(
|
||||
httpGet(danmakuUrl, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
log("error", "请求弹幕分片失败:", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
log("info", "弹幕分段数量:", promises.length);
|
||||
|
||||
// 默认颜色值
|
||||
const DEFAULT_COLOR_INT = -1;
|
||||
|
||||
// 处理 v2_color 对象的转换逻辑
|
||||
function transformV2Color(v2_color) {
|
||||
// 如果 v2_color 不存在,返回默认值
|
||||
if (!v2_color) {
|
||||
return DEFAULT_COLOR_INT;
|
||||
}
|
||||
// 计算左右颜色的整数值
|
||||
const leftColor = rgbToInt(v2_color.color_left);
|
||||
const rightColor = rgbToInt(v2_color.color_right);
|
||||
// 如果左右颜色均为 -1,返回默认值
|
||||
if (leftColor === -1 && rightColor === -1) {
|
||||
return DEFAULT_COLOR_INT;
|
||||
}
|
||||
// 如果左颜色无效,返回右颜色
|
||||
if (leftColor === -1) {
|
||||
return rightColor;
|
||||
}
|
||||
// 如果右颜色无效,返回左颜色
|
||||
if (rightColor === -1) {
|
||||
return leftColor;
|
||||
}
|
||||
// 返回左右颜色的平均值
|
||||
return Math.floor((leftColor + rightColor) / 2);
|
||||
}
|
||||
|
||||
// 解析弹幕数据
|
||||
let contents = [];
|
||||
try {
|
||||
@@ -2502,10 +2652,15 @@ async function fetchMangoTV(inputUrl) {
|
||||
uid: 0, //发送人的 id
|
||||
content: "",
|
||||
};
|
||||
if (item.type === 1) {
|
||||
content.ct = 5;
|
||||
} else if (item.type === 2) {
|
||||
content.ct = 4;
|
||||
if (item?.v2_color) {
|
||||
content.color = transformV2Color(item?.v2_color);
|
||||
}
|
||||
if (item?.v2_position) {
|
||||
if (item?.v2_position === 1) {
|
||||
content.ct = 5;
|
||||
} else if (item?.v2_position === 2) {
|
||||
content.ct = 4;
|
||||
}
|
||||
}
|
||||
content.timepoint = item.time / 1000;
|
||||
content.content = item.content;
|
||||
@@ -4556,7 +4711,7 @@ async function searchAnime(url) {
|
||||
|
||||
// 如果有新的anime获取到,则更新redis
|
||||
if (redisValid && curAnimes.length !== 0) {
|
||||
await updateCaches();
|
||||
await updateRedisCaches();
|
||||
}
|
||||
|
||||
return jsonResponse({
|
||||
@@ -4667,7 +4822,7 @@ async function searchAnime(url) {
|
||||
|
||||
// 如果有新的anime获取到,则更新redis
|
||||
if (redisValid && curAnimes.length !== 0) {
|
||||
await updateCaches();
|
||||
await updateRedisCaches();
|
||||
}
|
||||
|
||||
// 缓存搜索结果
|
||||
@@ -5262,8 +5417,8 @@ async function handleRequest(req, env, deployPlatform, clientIp) {
|
||||
otherServer = resolveOtherServer(env);
|
||||
envs["otherServer"] = otherServer;
|
||||
vodServers = resolveVodServers(env);
|
||||
vodReturnMode = resolveVodReturnMode(env);
|
||||
envs["vodServers"] = vodServers.map(s => `${s.name}@${s.url}`).join(',');
|
||||
vodReturnMode = resolveVodReturnMode(env);
|
||||
envs["vodReturnMode"] = vodReturnMode;
|
||||
bilibliCookie = resolveBilibiliCookie(env);
|
||||
envs["bilibliCookie"] = encryptStr(bilibliCookie);
|
||||
@@ -5310,7 +5465,7 @@ async function handleRequest(req, env, deployPlatform, clientIp) {
|
||||
log("info", `client ip: ${clientIp}`);
|
||||
|
||||
if (redisValid && path !== "/favicon.ico" && path !== "/robots.txt") {
|
||||
await getCaches();
|
||||
await getRedisCaches();
|
||||
}
|
||||
|
||||
function handleHomepage() {
|
||||
@@ -5321,7 +5476,7 @@ async function handleRequest(req, env, deployPlatform, clientIp) {
|
||||
envs: envs,
|
||||
repository: "https://github.com/huangxd-/danmu_api.git",
|
||||
description: "一个人人都能部署的基于 js 的弹幕 API 服务器,支持爱优腾芒哔人韩巴弹幕直接获取,兼容弹弹play的搜索、详情查询和弹幕获取接口,并提供日志记录,支持vercel/cloudflare/docker/claw等部署方式,不用提前下载弹幕,没有nas或小鸡也能一键部署。",
|
||||
notice: "本项目仅为个人爱好开发,代码开源。如有任何侵权行为,请联系本人删除。有问题提issue或私信机器人都ok。https://t.me/ddjdd_bot"
|
||||
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]。"
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user