karin-plugin-kkk
Version:
Karin 的「抖音」「B 站」视频解析/动态推送插件
1,405 lines (1,404 loc) • 267 kB
JavaScript
import { Root } from "../root.js";
import fs from "node:fs";
import { C as Client, b as bilibiliErrorCodeMap, g as getBilibiliData, a as bilibiliApiUrls, w as wbi_sign, c as getDouyinData, h as heicConvertExports, d as browserExports } from "./vendor-BfPxWvnG.js";
import karin, { logger, config, segment, createNotFoundResponse, copyConfigSync, filesByExt, watch, requireFileSync, ffmpeg, ffprobe, render, common, defineConfig, components, karinPathRoot, mkdirSync } from "node-karin";
import axios, { AxiosError } from "node-karin/axios";
import { pipeline } from "stream/promises";
import path, { join } from "node:path";
import { g as getDouyinDB, a as getBilibiliDB, c as cleanOldDynamicCache, b as bilibiliDBInstance, d as douyinDBInstance } from "./db-39zy-DOZ.js";
import template from "node-karin/template";
import _ from "node-karin/lodash";
import "node:child_process";
import "playwright";
import { markdown } from "@karinjs/md-html";
import { karinPathTemp, karinPathBase } from "node-karin/root";
import YAML from "node-karin/yaml";
class Base {
e;
headers;
amagi;
constructor(e) {
this.e = e;
this.headers = baseHeaders;
const client = Client({
cookies: {
douyin: Config.cookies.douyin,
bilibili: Config.cookies.bilibili,
kuaishou: Config.cookies.kuaishou
},
request: {
timeout: Config.request.timeout,
headers: { "User-Agent": Config.request["User-Agent"] },
proxy: Config.request.proxy?.switch ? Config.request.proxy : false
}
});
this.amagi = new Proxy(client, {
get(target, prop) {
const method = target[prop];
if (typeof method === "function") {
return async (...args) => {
const result = await Function.prototype.apply.call(method, target, args);
if (!result) {
logger.warn(`Amagi API调用 (${String(prop)}) 返回了空值`);
return result;
}
if (prop === "getDouyinData" && result.code !== 200) {
const err = result;
const img2 = await Render("apiError/index", err.error);
if (Object.keys(e).length === 0) {
const botId = statBotId(Config.pushlist);
const list = config.master();
let master = list[0];
if (master === "console") {
master = list[1];
}
await karin.sendMaster(botId.douyin.botId, master, [segment.text("推送任务出错!请即时解决以消除警告"), img2[0]]);
throw new Error(err.data.amagiMessage);
}
await e.reply(img2);
throw new Error(err.data.amagiMessage);
}
if (prop === "getBilibiliData" && result.code in bilibiliErrorCodeMap) {
const err = result;
const img2 = await Render("apiError/index", err.error);
if (Object.keys(e).length === 0) {
const botId = statBotId(Config.pushlist);
const list = config.master();
let master = list[0];
if (master === "console") {
master = list[1];
}
await karin.sendMaster(botId.bilibili.botId, master, [segment.text("推送任务出错!请即时解决以消除警告"), img2[0]]);
throw new Error(err.data.amagiMessage);
}
await e.reply(img2);
throw new Error(err.data.amagiMessage);
}
return result;
};
}
return method;
}
});
}
}
const statBotId = (pushList) => {
const platformBotCount = {
douyin: /* @__PURE__ */ new Map(),
bilibili: /* @__PURE__ */ new Map()
};
pushList.douyin.forEach((item) => {
item.group_id.forEach((gid) => {
const botId = gid.split(":")[1];
platformBotCount.douyin.set(botId, (platformBotCount.douyin.get(botId) ?? 0) + 1);
});
});
pushList.bilibili.forEach((item) => {
item.group_id.forEach((gid) => {
const botId = gid.split(":")[1];
platformBotCount.bilibili.set(botId, (platformBotCount.bilibili.get(botId) ?? 0) + 1);
});
});
let douyinMaxCount = 0;
let douyinMostFrequentBot = "";
platformBotCount.douyin.forEach((count, botId) => {
if (count > douyinMaxCount) {
douyinMaxCount = count;
douyinMostFrequentBot = botId;
}
});
let biliMaxCount = 0;
let biliMostFrequentBot = "";
platformBotCount.bilibili.forEach((count, botId) => {
if (count > biliMaxCount) {
biliMaxCount = count;
biliMostFrequentBot = botId;
}
});
return {
douyin: {
botId: douyinMostFrequentBot,
count: douyinMaxCount
},
bilibili: {
botId: biliMostFrequentBot,
count: biliMaxCount
}
};
};
const Count = (count) => {
if (count > 1e4) {
return (count / 1e4).toFixed(1) + "万";
} else {
return count?.toString() ?? "无法获取";
}
};
const uploadFile = async (event, file, videoUrl, options) => {
let sendStatus = true;
let File;
let newFileSize = file.totalBytes;
let selfId;
let contact;
if (options?.active) {
selfId = options?.activeOption?.uin;
contact = karin.contactGroup(options?.activeOption?.group_id);
} else {
selfId = event.selfId;
contact = event.contact;
}
if (Config.upload.compress && file.totalBytes > Config.upload.compresstrigger) {
const Duration = await mergeFile("获取指定视频文件时长", { path: file.filepath });
logger.warn(logger.yellow(`视频大小 (${file.totalBytes} MB) 触发压缩条件(设定值:${Config.upload.compresstrigger} MB),正在进行压缩至${Config.upload.compressvalue} MB...`));
const message = [
segment.text(`视频大小 (${file.totalBytes} MB) 触发压缩条件(设定值:${Config.upload.compresstrigger} MB),正在进行压缩至${Config.upload.compressvalue} MB...`),
options?.message_id ? segment.reply(options.message_id) : segment.text("")
];
const msg1 = await karin.sendMsg(selfId, contact, message);
const targetBitrate = Common.calculateBitrate(Config.upload.compresstrigger, Duration) * 0.75;
const startTime = Date.now();
file.filepath = await mergeFile("压缩视频", { path: file.filepath, targetBitrate, resultPath: `${Common.tempDri.video}tmp_${Date.now()}.mp4` });
const endTime = Date.now();
newFileSize = await Common.getVideoFileSize(file.filepath);
logger.debug(`原始视频大小为: ${file.totalBytes.toFixed(1)} MB, ${logger.green(`经 FFmpeg 压缩后最终视频大小为: ${newFileSize.toFixed(1)} MB,原视频文件已删除`)}`);
const message2 = [
segment.text(`压缩后最终视频大小为: ${newFileSize.toFixed(1)} MB,压缩耗时:${((endTime - startTime) / 1e3).toFixed(1)} 秒`),
segment.reply(msg1.messageId)
];
await karin.sendMsg(selfId, contact, message2);
}
if (options) {
options.useGroupFile = Config.upload.usegroupfile && newFileSize > Config.upload.groupfilevalue;
}
if (Config.upload.sendbase64 && !options?.useGroupFile) {
const videoBuffer = await fs.promises.readFile(file.filepath);
File = `base64://${videoBuffer.toString("base64")}`;
logger.mark(`已开启视频文件 base64转换 正在进行${logger.yellow("base64转换中")}...`);
} else File = options?.useGroupFile ? file.filepath : `file://${file.filepath}`;
try {
if (options?.active) {
if (options.useGroupFile) {
const bot = karin.getBot(String(options.activeOption?.uin));
logger.mark(`${logger.blue("主动消息:")} 视频大小: ${newFileSize.toFixed(1)}MB 正在通过${logger.yellow("bot.uploadFile")}回复...`);
await bot.uploadFile(contact, File, file.originTitle ? `${file.originTitle}.mp4` : `${File.split("/").pop()}`);
} else {
logger.mark(`${logger.blue("主动消息:")} 视频大小: ${newFileSize.toFixed(1)}MB 正在通过${logger.yellow("karin.sendMsg")}回复...`);
const status = await karin.sendMsg(selfId, contact, [segment.video(File)]);
status.messageId ? sendStatus = true : sendStatus = false;
}
} else {
if (options?.useGroupFile) {
logger.mark(`${logger.blue("被动消息:")} 视频大小: ${newFileSize.toFixed(1)}MB 正在通过${logger.yellow("e.bot.uploadFile")}回复...`);
await event.bot.uploadFile(event.contact, File, file.originTitle ? `${file.originTitle}.mp4` : `${File.split("/").pop()}`);
} else {
logger.mark(`${logger.blue("被动消息:")} 视频大小: ${newFileSize.toFixed(1)}MB 正在通过${logger.yellow("e.reply")}回复...`);
const status = await event.reply(segment.video(File) || videoUrl);
status.messageId ? sendStatus = true : sendStatus = false;
}
}
return sendStatus;
} catch (error) {
if (options && options.active === false) {
await event.reply("视频文件上传失败" + JSON.stringify(error, null, 2));
}
logger.error("视频文件上传错误," + String(error));
return false;
} finally {
const filePath = file.filepath;
logger.mark(`临时预览地址:http://localhost:${process.env.HTTP_PORT}/api/kkk/video/${encodeURIComponent(filePath.split("/").pop() ?? "")}`);
Config.app.rmmp4 && logger.info(`文件 ${filePath} 将在 10 分钟后删除`);
setTimeout(async () => {
await Common.removeFile(filePath);
}, 10 * 60 * 1e3);
}
};
const downloadVideo = async (event, downloadOpt, uploadOpt) => {
const fileHeaders = await new Networks({ url: downloadOpt.video_url, headers: downloadOpt.headers ?? baseHeaders }).getHeaders();
const fileSizeContent = fileHeaders["content-range"]?.match(/\/(\d+)/) ? parseInt(fileHeaders["content-range"]?.match(/\/(\d+)/)[1], 10) : 0;
const fileSizeInMB = (fileSizeContent / (1024 * 1024)).toFixed(2);
const fileSize = parseInt(parseFloat(fileSizeInMB).toFixed(2));
if (Config.upload.usefilelimit && fileSize > Config.upload.filelimit) {
const message = segment.text(`视频:「${downloadOpt.title.originTitle ?? "Error: 文件名获取失败"}」大小 (${fileSizeInMB} MB) 超出最大限制(设定值:${Config.upload.filelimit} MB),已取消上传`);
const selfId = event.selfId || uploadOpt?.activeOption?.uin;
const contact = event.contact || karin.contactGroup(uploadOpt?.activeOption?.group_id) || karin.contactFriend(selfId);
await karin.sendMsg(selfId, contact, message);
return false;
}
let res = await downloadFile(downloadOpt.video_url, {
title: Config.app.rmmp4 ? downloadOpt.title.timestampTitle : processFilename(downloadOpt.title.originTitle, 50),
headers: downloadOpt.headers ?? baseHeaders
});
res = { ...res, ...downloadOpt.title };
res.totalBytes = Number((res.totalBytes / (1024 * 1024)).toFixed(2));
return await uploadFile(event, res, downloadOpt.video_url, uploadOpt);
};
const downloadFile = async (videoUrl, opt) => {
const startTime = Date.now();
const { filepath, totalBytes } = await new Networks({
url: videoUrl,
// 视频地址
headers: opt.headers ?? baseHeaders,
// 请求头
filepath: Common.tempDri.video + opt.title,
// 文件保存路径
timeout: 3e4
// 设置 30 秒超时
}).downloadStream((downloadedBytes, totalBytes2) => {
const barLength = 45;
function generateProgressBar(progressPercentage2) {
const filledLength = Math.floor(progressPercentage2 / 100 * barLength);
let progress = "";
progress += "█".repeat(filledLength);
progress += "░".repeat(Math.max(0, barLength - filledLength - 1));
return `[${progress}]`;
}
const progressPercentage = downloadedBytes / totalBytes2 * 100;
const red = Math.floor(255 - 255 * progressPercentage / 100);
const coloredPercentage = logger.chalk.rgb(red, 255, 0)(`${progressPercentage.toFixed(1)}%`);
const elapsedTime = (Date.now() - startTime) / 1e3;
const speed = downloadedBytes / elapsedTime;
const formattedSpeed = (speed / 1048576).toFixed(1) + " MB/s";
const remainingBytes = totalBytes2 - downloadedBytes;
const remainingTime = remainingBytes / speed;
const formattedRemainingTime = remainingTime > 60 ? `${Math.floor(remainingTime / 60)}min ${Math.floor(remainingTime % 60)}s` : `${remainingTime.toFixed(0)}s`;
const downloadedSizeMB = (downloadedBytes / 1048576).toFixed(1);
const totalSizeMB = (totalBytes2 / 1048576).toFixed(1);
console.log(
`⬇️ ${opt.title} ${generateProgressBar(progressPercentage)} ${coloredPercentage} ${downloadedSizeMB}/${totalSizeMB} MB | ${formattedSpeed} 剩余: ${formattedRemainingTime}\r`
);
}, 3);
return { filepath, totalBytes };
};
const processFilename = (filename, maxLength = 50) => {
const lastDotIndex = filename.lastIndexOf(".");
const hasExtension = lastDotIndex > 0 && lastDotIndex < filename.length - 1;
if (!hasExtension) {
return filename.substring(0, maxLength).replace(/[\\/:*?"<>|\r\n\s]/g, " ");
}
const nameWithoutExt = filename.substring(0, lastDotIndex);
const extension = filename.substring(lastDotIndex);
const processedName = nameWithoutExt.substring(0, maxLength).replace(/[\\/:*?"<>|\r\n\s]/g, " ");
return processedName + "..." + extension;
};
class Tools {
/**
* 插件缓存目录
*/
tempDri;
constructor() {
this.tempDri = {
/** 插件缓存目录 */
default: `${karinPathTemp}/${Root.pluginName}/`.replace(/\\/g, "/"),
/** 视频缓存文件 */
video: `${karinPathTemp}/${Root.pluginName}/kkkdownload/video/`.replace(/\\/g, "/"),
/** 图片缓存文件 */
images: `${karinPathTemp}/${Root.pluginName}/kkkdownload/images/`.replace(/\\/g, "/")
};
}
/**
* 获取引用消息
* @param e event 消息事件
* @returns 被引用的消息
*/
async getReplyMessage(e) {
if (e.replyId) {
const reply = await e.bot.getMsg(e.contact, e.replyId);
for (const v of reply.elements) {
if (v.type === "text") {
return v.text;
} else if (v.type === "json") {
return v.data;
}
}
}
return "";
}
/**
* 将中文数字转换为阿拉伯数字的函数
* @param chineseNumber 数字的中文
* @returns 中文数字对应的阿拉伯数字映射
*/
chineseToArabic(chineseNumber) {
const chineseToArabicMap = {
零: 0,
一: 1,
二: 2,
三: 3,
四: 4,
五: 5,
六: 6,
七: 7,
八: 8,
九: 9
};
const units = {
十: 10,
百: 100,
千: 1e3,
万: 1e4,
亿: 1e8
};
let result = 0;
let temp = 0;
let unit = 1;
for (let i = chineseNumber.length - 1; i >= 0; i--) {
const char = chineseNumber[i];
if (units[char] !== void 0) {
unit = units[char];
if (unit === 1e4 || unit === 1e8) {
result += temp * unit;
temp = 0;
}
} else {
const num = chineseToArabicMap[char];
if (unit > 1) {
temp += num * unit;
} else {
temp += num;
}
unit = 1;
}
}
return result + temp;
}
/**
* 格式化cookie字符串
* @param cookies cookie数组
* @returns 格式化后的cookie字符串
*/
formatCookies(cookies) {
return cookies.map((cookie) => {
const [nameValue] = cookie.split(";").map((part) => part.trim());
const [name, value] = nameValue.split("=");
return `${name}=${value}`;
}).join("; ");
}
/**
* 计算目标视频平均码率(单位:Kbps)
* @param targetSizeMB 目标视频大小(MB)
* @param duration 视频时长(秒)
* @returns
*/
calculateBitrate(targetSizeMB, duration) {
const targetSizeBytes = targetSizeMB * 1024 * 1024;
return targetSizeBytes * 8 / duration / 1024;
}
/**
* 获取视频文件大小(单位MB)
* @param filePath 视频文件绝对路径
* @returns
*/
async getVideoFileSize(filePath) {
try {
const stats = await fs.promises.stat(filePath);
const fileSizeInBytes = stats.size;
const fileSizeInMB = fileSizeInBytes / (1024 * 1024);
return fileSizeInMB;
} catch (error) {
console.error("获取文件大小时发生错误:", error);
throw error;
}
}
/**
* 根据配置文件的配置项,删除缓存文件
* @param path 文件的绝对路径
* @param force 是否强制删除,默认 `false`
* @returns
*/
async removeFile(path2, force = false) {
path2 = path2.replace(/\\/g, "/");
if (Config.app.rmmp4) {
try {
await fs.promises.unlink(path2);
logger.mark("缓存文件: ", path2 + " 删除成功!");
return true;
} catch (err) {
logger.error("缓存文件: ", path2 + " 删除失败!", err);
return false;
}
} else if (force) {
try {
await fs.promises.unlink(path2);
logger.mark("缓存文件: ", path2 + " 删除成功!");
return true;
} catch (err) {
logger.error("缓存文件: ", path2 + " 删除失败!", err);
return false;
}
}
return true;
}
/**
* 将时间戳转换为日期时间字符串
* @param timestamp 时间戳
* @returns 格式为YYYY-MM-DD HH:MM的日期时间字符串
*/
convertTimestampToDateTime(timestamp) {
const date = new Date(timestamp * 1e3);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
const hours = String(date.getHours()).padStart(2, "0");
const minutes = String(date.getMinutes()).padStart(2, "0");
return `${year}-${month}-${day} ${hours}:${minutes}`;
}
/**
* 获取当前时间:年-月-日 时:分:秒
* @returns
*/
getCurrentTime() {
const now = /* @__PURE__ */ new Date();
const year = now.getFullYear();
const month = now.getMonth() + 1;
const day = now.getDate();
const hour = now.getHours();
const minute = now.getMinutes();
const second = now.getSeconds();
const formattedMonth = month < 10 ? "0" + month : "" + month;
const formattedDay = day < 10 ? "0" + day : "" + day;
const formattedHour = hour < 10 ? "0" + hour : "" + hour;
const formattedMinute = minute < 10 ? "0" + minute : "" + minute;
const formattedSecond = second < 10 ? "0" + second : "" + second;
return `${year}-${formattedMonth}-${formattedDay} ${formattedHour}:${formattedMinute}:${formattedSecond}`;
}
/**
* 评论图、推送图是否使用深色模式
* @returns
*/
useDarkTheme() {
let dark = true;
const configTheme = Config.app.Theme;
if (configTheme === 0) {
const date = (/* @__PURE__ */ new Date()).getHours();
if (date >= 6 && date < 18) {
dark = false;
}
} else if (configTheme === 1) {
dark = false;
} else if (configTheme === 2) {
dark = true;
}
return dark;
}
/**
* 传入一个时间戳(单位:毫秒),返回距离当前时间的相对的时间字符串
* @param timestamp 时间戳
* @returns 距离这个时间戳过去的多久的字符串
*/
timeSince(timestamp) {
const now = Date.now();
const elapsed = now - timestamp;
const seconds = Math.floor(elapsed / 1e3);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const remainingSeconds = seconds % 60;
const remainingMinutes = minutes % 60;
if (hours > 0) {
return `${hours}小时${remainingMinutes}分钟${remainingSeconds}秒`;
} else if (minutes > 0) {
return `${minutes}分钟${remainingSeconds}秒`;
} else {
return `${seconds}秒`;
}
}
/**
* 验证视频请求
* @param filename 文件名
* @param res 响应对象
* @returns 返回安全解析后的路径
*/
validateVideoRequest(filename, res) {
if (!filename) {
createNotFoundResponse(res, "无效的文件名");
return null;
}
const intendedBaseDir = path.resolve(Common.tempDri.video);
const requestedPath = path.join(intendedBaseDir, filename);
const resolvedPath = path.normalize(requestedPath);
if (!resolvedPath.startsWith(intendedBaseDir + path.sep) || filename.includes("/") || filename.includes("\\")) {
logger.warn(`潜在的路径穿越尝试或无效文件名: ${filename}, 解析路径: ${resolvedPath}`);
createNotFoundResponse(res, "无效的文件名或路径");
return null;
}
if (path.basename(filename) !== filename) {
logger.warn(`文件名包含路径分隔符: ${filename}`);
createNotFoundResponse(res, "无效的文件名");
return null;
}
if (!fs.existsSync(resolvedPath)) {
createNotFoundResponse(res, "视频文件未找到");
return null;
}
return resolvedPath;
}
}
const Common = new Tools();
class Cfg {
/** 用户配置文件路径 */
dirCfgPath;
/** 默认配置文件路径 */
defCfgPath;
constructor() {
this.dirCfgPath = `${karinPathBase}/${Root.pluginName}/config`;
this.defCfgPath = `${Root.pluginPath}/config/default_config/`;
}
/** 初始化配置 */
initCfg() {
copyConfigSync(this.defCfgPath, this.dirCfgPath);
const files = filesByExt(this.dirCfgPath, ".yaml", "name");
for (const file of files) {
const config2 = YAML.parseDocument(fs.readFileSync(`${this.dirCfgPath}/${file}`, "utf8"));
const defConfig = YAML.parseDocument(fs.readFileSync(`${this.defCfgPath}/${file}`, "utf8"));
const { differences, result } = this.mergeObjectsWithPriority(config2, defConfig);
let needsUpdate = differences;
if (file === "pushlist.yaml") {
const updated = this.addSwitchFieldToPushlist(result);
if (updated) {
needsUpdate = true;
}
}
if (needsUpdate) {
fs.writeFileSync(`${this.dirCfgPath}/${file}`, result.toString({ lineWidth: -1 }));
}
}
setTimeout(() => {
const list = filesByExt(this.dirCfgPath, ".yaml", "abs");
list.forEach((file) => watch(file, (_old, _now) => {
}));
}, 2e3);
return this;
}
/**
* 获取默认配置和用户配置
* @param name 配置文件名
* @returns 返回合并后的配置
*/
getDefOrConfig(name) {
const def = this.getYaml("default_config", name);
const config2 = this.getYaml("config", name);
return { ...def, ...config2 };
}
/** 获取所有配置文件 */
async All() {
const douyinDB = await getDouyinDB();
const bilibiliDB = await getBilibiliDB();
const allConfig = {};
const files = fs.readdirSync(this.defCfgPath);
for (const file of files) {
const fileName = path.basename(file, ".yaml");
allConfig[fileName] = this.getDefOrConfig(fileName) || {};
}
if (allConfig.pushlist) {
try {
if (allConfig.pushlist.douyin) {
for (const item of allConfig.pushlist.douyin) {
const filterWords = await douyinDB.getFilterWords(item.sec_uid);
const filterTags = await douyinDB.getFilterTags(item.sec_uid);
const userInfo = await douyinDB.getDouyinUser(item.sec_uid);
if (userInfo) {
item.filterMode = userInfo.filterMode || "blacklist";
}
item.Keywords = filterWords;
item.Tags = filterTags;
}
}
if (allConfig.pushlist.bilibili) {
for (const item of allConfig.pushlist.bilibili) {
const filterWords = await bilibiliDB.getFilterWords(item.host_mid);
const filterTags = await bilibiliDB.getFilterTags(item.host_mid);
const userInfo = await bilibiliDB.getOrCreateBilibiliUser(item.host_mid);
if (userInfo) {
item.filterMode = userInfo.filterMode || "blacklist";
}
item.Keywords = filterWords;
item.Tags = filterTags;
}
}
} catch (error) {
logger.error(`从数据库获取过滤配置时出错: ${error}`);
}
}
return allConfig;
}
/**
* 获取 YAML 文件内容
* @param type 配置文件类型
* @param name 配置文件名
* @returns 返回 YAML 文件内容
*/
getYaml(type, name) {
const file = type === "config" ? `${this.dirCfgPath}/${name}.yaml` : `${this.defCfgPath}/${name}.yaml`;
return requireFileSync(file, { force: true });
}
/**
* 修改配置文件
* @param name 文件名
* @param key 键
* @param value 值
* @param type 配置文件类型,默认为用户配置文件 `config`
*/
Modify(name, key, value, type = "config") {
const path2 = type === "config" ? `${this.dirCfgPath}/${name}.yaml` : `${this.defCfgPath}/${name}.yaml`;
const yamlData = YAML.parseDocument(fs.readFileSync(path2, "utf8"));
const keys = key.split(".");
this.setNestedValue(yamlData.contents, keys, value);
fs.writeFileSync(path2, yamlData.toString({ lineWidth: -1 }), "utf8");
}
/**
* 修改整个配置文件,保留注释
* @param name 文件名
* @param config 完整的配置对象
* @param type 配置文件类型,默认为用户配置文件 `config`
*/
async ModifyPro(name, config2, type = "config") {
const douyinDB = await getDouyinDB();
const bilibiliDB = await getBilibiliDB();
const filePath = type === "config" ? `${this.dirCfgPath}/${name}.yaml` : `${this.defCfgPath}/${name}.yaml`;
try {
const existingContent = fs.readFileSync(filePath, "utf8");
const doc = YAML.parseDocument(existingContent);
let filterCfg = config2;
if (name === "pushlist" && ("douyin" in config2 || "bilibili" in config2)) {
const cleanedConfig = { ...config2 };
if ("douyin" in cleanedConfig) {
cleanedConfig.douyin = cleanedConfig.douyin.map((item) => {
const { Keywords, Tags, filterMode, ...rest } = item;
return rest;
});
}
if ("bilibili" in cleanedConfig) {
cleanedConfig.bilibili = cleanedConfig.bilibili.map((item) => {
const { Keywords, Tags, filterMode, ...rest } = item;
return rest;
});
}
filterCfg = cleanedConfig;
}
const newConfigNode = YAML.parseDocument(YAML.stringify(filterCfg)).contents;
this.deepMergeYaml(doc.contents, newConfigNode);
fs.writeFileSync(filePath, doc.toString({ lineWidth: -1 }), "utf8");
if ("douyin" in config2) {
await this.syncFilterConfigToDb(config2.douyin, douyinDB, "sec_uid");
logger.debug("已同步抖音过滤配置到数据库");
}
if ("bilibili" in config2) {
await this.syncFilterConfigToDb(config2.bilibili, bilibiliDB, "host_mid");
logger.debug("已同步B站过滤配置到数据库");
}
return true;
} catch (error) {
logger.error(`修改配置文件时发生错误:${error}`);
return false;
}
}
/**
* 同步过滤配置到数据库
* @param items 推送项列表
* @param db 数据库实例
* @param idField ID字段名称
*/
async syncFilterConfigToDb(items, db, idField) {
for (const item of items) {
const id = item[idField];
if (!id) continue;
if (item.filterMode) {
await db.updateFilterMode(id, item.filterMode);
}
if (item.Keywords && Array.isArray(item.Keywords)) {
const existingWords = await db.getFilterWords(id);
for (const word of existingWords) {
if (!item.Keywords.includes(word)) {
await db.removeFilterWord(id, word);
}
}
for (const word of item.Keywords) {
if (!existingWords.includes(word)) {
await db.addFilterWord(id, word);
}
}
}
if (item.Tags && Array.isArray(item.Tags)) {
const existingTags = await db.getFilterTags(id);
for (const tag of existingTags) {
if (!item.Tags.includes(tag)) {
await db.removeFilterTag(id, tag);
}
}
for (const tag of item.Tags) {
if (!existingTags.includes(tag)) {
await db.addFilterTag(id, tag);
}
}
}
}
}
/**
* 深度合并YAML节点(保留目标注释)
* @param target 目标节点(保留注释的原始节点)
* @param source 源节点(提供新值的节点)
*/
deepMergeYaml(target, source) {
if (YAML.isMap(target) && YAML.isMap(source)) {
for (const pair of source.items) {
const key = pair.key;
const sourceVal = pair.value;
const targetVal = target.get(key);
if (targetVal === void 0) {
target.set(key, sourceVal);
} else if (YAML.isMap(targetVal) && YAML.isMap(sourceVal)) {
this.deepMergeYaml(targetVal, sourceVal);
} else if (YAML.isSeq(targetVal) && YAML.isSeq(sourceVal)) {
targetVal.items = sourceVal.items;
targetVal.flow = sourceVal.flow;
} else {
target.set(key, sourceVal);
}
}
}
}
/**
* 在YAML映射中设置嵌套值
*
* 该函数用于在给定的YAML映射(map)中,根据指定的键路径(keys)设置值(value)
* 如果键路径不存在,该函数会创建必要的嵌套映射结构并设置值
*
* @param map YAML映射,作为设置值的目标
* @param keys 键路径,表示要设置的值的位置
* @param value 要设置的值
*/
setNestedValue(map, keys, value) {
if (keys.length === 1) {
map.set(keys[0], value);
return;
}
const subKey = keys[0];
let subMap = map.get(subKey);
if (!subMap || !YAML.isMap(subMap)) {
subMap = new YAML.YAMLMap();
map.set(subKey, subMap);
}
this.setNestedValue(subMap, keys.slice(1), value);
}
/**
* 为推送列表配置添加switch字段兼容
* @param doc YAML文档
* @returns 是否有更新
*/
addSwitchFieldToPushlist(doc) {
let hasUpdates = false;
const contents = doc.contents;
const douyinList = contents.get("douyin");
if (YAML.isSeq(douyinList)) {
for (const item of douyinList.items) {
if (YAML.isMap(item)) {
const switchField = item.get("switch");
if (switchField === void 0) {
item.set("switch", true);
hasUpdates = true;
}
}
}
}
const bilibiliList = contents.get("bilibili");
if (YAML.isSeq(bilibiliList)) {
for (const item of bilibiliList.items) {
if (YAML.isMap(item)) {
const switchField = item.get("switch");
if (switchField === void 0) {
item.set("switch", true);
hasUpdates = true;
}
}
}
}
return hasUpdates;
}
mergeObjectsWithPriority(userDoc, defaultDoc) {
let differences = false;
const mergeYamlNodes = (target, source) => {
if (YAML.isMap(target) && YAML.isMap(source)) {
for (const pair of source.items) {
const key = pair.key;
const value = pair.value;
const existing = target.get(key);
if (existing === void 0) {
differences = true;
target.set(key, value);
} else if (YAML.isMap(value) && YAML.isMap(existing)) {
mergeYamlNodes(existing, value);
} else if (existing !== value) {
differences = true;
target.set(key, value);
}
}
}
};
mergeYamlNodes(defaultDoc.contents, userDoc.contents);
return { differences, result: defaultDoc };
}
/**
* 同步配置到数据库
* 这个方法应该在所有模块都初始化完成后调用
*/
async syncConfigToDatabase() {
try {
const pushCfg = this.getYaml("config", "pushlist");
const douyinDB = await getDouyinDB();
const bilibiliDB = await getBilibiliDB();
if (pushCfg.bilibili) {
await bilibiliDB.syncConfigSubscriptions(pushCfg.bilibili);
}
if (pushCfg.douyin) {
await douyinDB.syncConfigSubscriptions(pushCfg.douyin);
}
logger.debug("[BilibiliDB] + [DouyinDB] 配置已同步到数据库");
} catch (error) {
logger.error("同步配置到数据库失败:", error);
}
}
}
const Config = new Proxy(new Cfg().initCfg(), {
get(target, prop) {
if (prop in target) return target[prop];
return target.getDefOrConfig(prop);
}
});
const mergeFile = async (type, options) => {
return await new FFmpeg(type).FFmpeg(options);
};
class FFmpeg {
type;
constructor(type) {
this.type = type;
}
async FFmpeg(opt) {
switch (this.type) {
case "二合一(视频 + 音频)": {
const result = await ffmpeg(`-y -i ${opt.path} -i ${opt.path2} -c copy ${opt.resultPath}`);
result.status ? logger.mark(`视频合成成功!文件地址:${opt.resultPath}`) : logger.error(result);
await opt.callback(result.status, opt.resultPath);
return result;
}
case "视频*3 + 音频": {
const result = await ffmpeg(`-y -stream_loop 2 -i ${opt.path} -i ${opt.path2} -filter_complex "[0:v]setpts=N/FRAME_RATE/TB[v];[0:a][1:a]amix=inputs=2:duration=shortest:dropout_transition=3[aout]" -map "[v]" -map "[aout]" -c:v libx264 -c:a aac -b:a 192k -shortest ${opt.resultPath}`);
result ? logger.mark(`视频合成成功!文件地址:${opt.resultPath}`) : logger.error(result);
await opt.callback(result.status, opt.resultPath);
return result;
}
case "获取指定视频文件时长": {
const { stdout } = await ffprobe(`-v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 ${opt.path}`);
return parseFloat(parseFloat(stdout.trim()).toFixed(2));
}
case "压缩视频": {
const result = await ffmpeg(`-y -i "${opt.path}" -b:v ${opt.targetBitrate}k -maxrate ${opt.maxRate ?? opt.targetBitrate * 1.5}k -bufsize ${opt.bufSize ?? opt.targetBitrate * 2}k -crf ${opt.crf ?? 35} -preset medium -c:v libx264 -vf "scale='if(gte(iw/ih,16/9),1280,-1)':'if(gte(iw/ih,16/9),-1,720)',scale=ceil(iw/2)*2:ceil(ih/2)*2" "${opt.resultPath}"`);
if (result.status) {
logger.mark(`视频已压缩并保存到: ${opt.resultPath}`);
Common.removeFile(opt.path);
} else {
logger.error(opt.path + " 压缩失败!");
logger.error(result);
}
return opt.resultPath;
}
}
}
}
const baseHeaders = {
Accept: "*/*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0"
};
class Networks {
url;
method;
headers;
type;
body;
axiosInstance;
timeout;
filepath;
maxRetries;
constructor(data) {
this.headers = data.headers ? Object.fromEntries(
Object.entries(data.headers).map(([key, value]) => [key, String(value)])
) : {};
this.url = data.url ?? "";
this.type = data.type ?? "json";
this.method = data.method ?? "GET";
this.body = data.body ?? null;
this.timeout = data.timeout ?? 15e3;
this.filepath = data.filepath ?? "";
this.maxRetries = 0;
this.axiosInstance = axios.create({
timeout: this.timeout,
headers: this.headers,
maxRedirects: 5,
validateStatus: (status) => {
return status >= 200 && status < 300 || status === 406 || status >= 500;
}
});
}
get config() {
const config2 = {
url: this.url,
method: this.method,
headers: this.headers,
responseType: this.type
};
if (this.method === "POST" && this.body) {
config2.data = this.body;
}
return config2;
}
/**
* 异步下载流方法
*
* @param progressCallback 下载进度回调函数,接收已下载字节数和总字节数作为参数
* @param retryCount 重试次数,默认为0
* @returns 返回一个Promise,解析为包含文件路径和总字节数的对象
*
* 此函数通过axios库发送HTTP请求,下载指定URL的资源,并将下载的资源流保存到本地文件系统中
* 它还提供了一个回调函数来报告下载进度,并在下载失败时根据配置自动重试
*/
async downloadStream(progressCallback, retryCount = 0) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await axios({
...this.config,
url: this.url,
responseType: "stream",
signal: controller.signal
});
clearTimeout(timeoutId);
if (!(response.status >= 200 && response.status < 300)) {
throw new Error(`无法获取 ${this.url}。状态: ${response.status} ${response.statusText}`);
}
const totalBytes = parseInt(response.headers["content-length"] ?? "0", 10);
if (isNaN(totalBytes)) {
throw new Error("无效的 content-length 头");
}
let downloadedBytes = 0;
let lastPrintedPercentage = -1;
const writer = fs.createWriteStream(this.filepath);
const printProgress = () => {
const progressPercentage = Math.floor(downloadedBytes / totalBytes * 100);
if (progressPercentage !== lastPrintedPercentage) {
progressCallback(downloadedBytes, totalBytes);
lastPrintedPercentage = progressPercentage;
}
};
const interval = totalBytes < 10 * 1024 * 1024 ? 1e3 : 500;
const intervalId = setInterval(printProgress, interval);
const onData = (chunk) => {
downloadedBytes += chunk.length;
};
response.data.on("data", onData);
await pipeline(
response.data,
writer
);
clearInterval(intervalId);
response.data.off("data", onData);
writer.end();
return { filepath: this.filepath, totalBytes };
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof AxiosError) {
logger.error(`请求在 ${this.timeout / 1e3} 秒后超时`);
} else {
logger.error("下载失败:", error);
}
if (retryCount < this.maxRetries) {
const delay = Math.min(Math.pow(2, retryCount) * 1e3, 1e3);
logger.warn(`正在重试下载... (${retryCount + 1}/${this.maxRetries}),将在 ${delay / 1e3} 秒后重试`);
await new Promise((resolve) => setTimeout(resolve, delay));
return this.downloadStream(progressCallback, retryCount + 1);
} else {
throw new Error(`在 ${this.maxRetries} 次尝试后下载失败: ${error}`);
}
}
}
async getfetch() {
try {
const result = await this.returnResult();
if (result.status === 504) {
return result;
}
return result;
} catch (error) {
logger.info(error);
return false;
}
}
async returnResult() {
let response = {};
try {
response = await this.axiosInstance(this.config);
} catch (error) {
logger.error(error);
}
return response;
}
/** 最终地址(跟随重定向) */
async getLongLink(url = "") {
let errorMsg = `获取链接重定向失败: ${this.url || url}`;
try {
const response = await this.axiosInstance.head(this.url || url);
return response.request.res.responseUrl;
} catch (error) {
const axiosError = error;
if (axiosError.response) {
if (axiosError.response.status === 302) {
const redirectUrl = axiosError.response.headers.location;
logger.info(`检测到302重定向,目标地址: ${redirectUrl}`);
return await this.getLongLink(redirectUrl);
} else if (axiosError.response.status === 403) {
errorMsg = `403 Forbidden 禁止访问!${this.url || url}`;
logger.error(errorMsg);
return errorMsg;
}
}
logger.error(errorMsg);
return errorMsg;
}
}
/** 获取首个302链接 */
async getLocation() {
try {
const response = await this.axiosInstance({
method: "GET",
url: this.url,
maxRedirects: 0,
// 禁止跟随重定向
validateStatus: (status) => status >= 300 && status < 400
// 仅处理3xx响应
});
return response.headers.location;
} catch (error) {
if (error instanceof AxiosError) {
logger.error(`获取 ${this.url} 重定向地址失败,接口响应状态码: ${error.response?.status}`);
throw new Error(error.stack);
}
}
}
/** 获取数据并处理数据的格式化,默认json */
async getData() {
try {
const result = await this.returnResult();
if (result.status === 504) {
return result;
}
if (result.status === 429) {
logger.error("HTTP 响应状态码: 429");
throw new Error("ratelimit triggered, 触发 https://www.douyin.com/ 的速率限制!!!");
}
return result.data;
} catch (error) {
if (error instanceof AxiosError) {
throw new Error(error.stack ?? error.message);
}
return false;
}
}
/**
* 获取响应头信息(仅首个字节)
* 适用于获取视频流的完整大小
* @returns 返回响应头信息
*/
async getHeaders() {
try {
const response = await this.axiosInstance({
...this.config,
method: "GET",
headers: {
...this.config.headers,
Range: "bytes=0-0"
}
});
return response.headers;
} catch (error) {
logger.error(error);
throw error;
}
}
/**
* 获取响应头信息(完整)
* @returns
*/
async getHeadersFull() {
try {
const response = await this.axiosInstance({
...this.config,
method: "GET"
});
return response.headers;
} catch (error) {
logger.error(error);
throw error;
}
}
}
function scale(pct = 1) {
const scale2 = Math.min(2, Math.max(0.5, Number(Config.app.renderScale) / 100));
pct = pct * scale2;
return `style=transform:scale(${pct})`;
}
async function Render(path2, params) {
const basePaths = {
douyin: "douyin/html",
bilibili: "bilibili/html",
admin: "admin/html",
kuaishou: "kuaishou/html",
help: "help/html",
apiError: "apiError/html"
};
const platform = Object.keys(basePaths).find((key) => path2.startsWith(key));
let newPath = path2.substring(platform.length);
if (newPath.startsWith("/")) {
newPath = newPath.substring(1);
}
path2 = `${basePaths[platform]}/${newPath}`;
const renderOpt = {
pageGotoParams: {
waitUntil: "load",
timeout: Config.app.RenderWaitTime * 1e3
},
name: `${Root.pluginName}/${platform}/${newPath}/`.replace(/\\/g, "/"),
file: `${Root.pluginPath}/resources/template/${path2}.html`,
type: "jpeg"
};
const img2 = await render.render({
...renderOpt,
multiPage: 12e3,
encoding: "base64",
data: {
...params,
_res_path: (join(Root.pluginPath, "/resources") + "/").replace(/\\/g, "/"),
_layout_path: (join(Root.pluginPath, "/resources", "template", "extend") + "/").replace(/\\/g, "/"),
defaultLayout: (join(Root.pluginPath, "/resources", "template", "extend", "html") + "/default.html").replace(/\\/g, "/"),
sys: {
scale: scale(params?.scale ?? 1)
},
pluResPath: `${Root.pluginPath}/resources/`,
copyright: Config.app.RemoveWatermark ? "" : `<span class="name">kkk</span><span class="version">${Root.pluginVersion} ${releaseType()}</span> Powered By <span class="name">Karin</span>`,
useDarkTheme: Common.useDarkTheme()
},
screensEval: "#container"
});
const ret = [];
for (const imgae of img2) {
ret.push(segment.image("base64://" + imgae));
}
return ret;
}
const releaseType = () => {
const versionPattern = /^\d+\.\d+\.\d+$/;
if (versionPattern.test(Root.pluginVersion)) {
return "Stable";
} else {
return "Preview";
}
};
let img$1;
class Bilibili extends Base {
e;
type;
STATUS;
isVIP;
Type;
islogin;
downloadfilename;
get botadapter() {
return this.e.bot?.adapter?.name;
}
constructor(e, data) {
super(e);
this.e = e;
this.isVIP = false;
this.Type = data?.type;
this.islogin = data?.USER?.STATUS === "isLogin";
this.downloadfilename = "";
this.headers.Referer = "https://api.bilibili.com/";
this.headers.Cookie = Config.cookies.bilibili;
}
async RESOURCES(iddata) {
Config.app.EmojiReply && await this.e.bot.setMsgReaction(this.e.contact, this.e.messageId, Config.app.EmojiReplyID, true);
Config.bilibili.tip && await this.e.reply("检测到B站链接,开始解析");
switch (this.Type) {
case "one_video": {
const infoData = await this.amagi.getBilibiliData("单个视频作品数据", { bvid: iddata.bvid, typeMode: "strict" });
const playUrlData = await this.amagi.getBilibiliData("单个视频下载信息数据", {
avid: infoData.data.data.aid,
cid: iddata.p ? infoData.data.data.pages[iddata.p - 1]?.cid ?? infoData.data.data.cid : infoData.data.data.cid,
typeMode: "strict"
});
this.islogin = (await checkCk()).Status === "isLogin";
const { owner, pic, title, stat, desc } = infoData.data.data;
const { name } = owner;
const { coin, like, share, view, favorite, danmaku } = stat;
this.downloadfilename = title.substring(0, 50).replace(/[\\/:*?"<>|\r\n\s]/g, " ");
const nockData = await new Networks({
url: bilibiliApiUrls.视频流信息({
avid: infoData.data.data.aid,
cid: iddata.p ? infoData.data.data.pages[iddata.p - 1]?.cid ?? infoData.data.data.cid : infoData.data.data.cid
}) + "&platform=html5",
headers: this.headers
}).getData();
const replyContent = [];
if (Config.bilibili.displayContent && Config.bilibili.displayContent.length > 0) {
const contentMap = {
cover: segment.image(pic),
title: segment.text(`
📺 标题: ${title}
`),
author: segment.text(`
👤 作者: ${name}
`),
stats: segment.text(this.formatVideoStats(view, danmaku, like, coin, share, favorite)),
desc: segment.text(`
📝 简介: ${desc}`)
};
const fixedOrder = ["cover", "title", "author", "stats", "desc"];
fixedOrder.forEach((item) => {
if (Config.bilibili.displayContent.includes(item) && contentMap[item]) {
replyContent.push(contentMap[item]);
}
});
if (replyContent.length > 0) {
await this.e.reply(replyContent);
}
}
let videoSize = "";
let correctList;
if (this.islogin) {
const simplify = playUrlData.data.data.dash.video.filter((item, index, self) => {
return self.findIndex((t) => {
return t.id === item.id;
}) === index;
});
playUrlData.data.data.dash.video = simplify;
correctList = await bilibiliProcessVideos({
accept_description: playUrlData.data.data.accept_description,
bvid: infoData.data.data.bvid
}, simplify, playUrlData.data.data.dash.audio[0].base_url);
playUrlData.data.data.dash.video = correctList.videoList;
playUrlData.data.data.accept_description = correctList.accept_description;
videoSize = await getvideosize(correctList.videoList[0].base_url, playUrlData.data.data.dash.audio[0].base_url, infoData.data.data.bvid);
} else {
videoSize = (playUrlData.data.data.durl[0].size / (1024 * 1024)).toFixed(2);
}
if (Config.bilibili.comment) {
const commentsData = await this.amagi.getBilibiliData("评论数据", {
number: Config.bilibili.numcomment,
type: 1,
oid: infoData.data.data.aid.toString(),
typeMode: "strict"
});
const commentsdata = bilibiliComments(commentsData.data);
if (!commentsdata?.length) {
await this.e.reply("这个视频没有评论 ~");
} else {
img$1 = await Render("bilibili/comment", {
Type: "视频",
CommentsData: commentsdata,
CommentLength: Config.bilibili.realCommentCount ? Count(infoData.data.data.stat.reply) : String(commentsdata.length),
share_url: "https://b23.tv/" + infoData.data.data.bvid,
Clarity: Config.bilibili.videopriority === true ? nockData.data.data.accept_description[0] : '"流畅 360P"',
VideoSize: Config.bilibili.videopriority === true ? (nockData.data.data.durl[0].size / (1024 * 1024)).toFixed(2) : videoSize,
ImageLength: 0,
shareurl: "https://b23.tv/" + infoData.data.data.bvid
});
await this.e.reply(img$1);
}
}
if (Config.upload.usefilelimit && Number(videoSize) > Number(Config.upload.filelimit) && !Config.upload.compress) {
await this.e.reply(`设定的最大上传大小为 ${Config.upload.filelimit}MB
当前解析到的视频大小为 ${Number(videoSize)}MB
视频太大了,还是去B站看吧~`, { reply: true });
} else {
await this.getvideo(
Config.bilibili.videopriority === true ? { playUrlData: nockData.data } : { infoData: infoData.data, playUrlData: playUrlData.data }
);
}
break;
}
case "bangumi_video_info": {
const videoInfo = await this.amagi.getBilibiliData("番剧基本信息数据", { [iddata.isEpid ? "ep_id" : "season_id"]: iddata.realid, typeMode: "strict" });
this.islogin = (await checkCk()).Status === "isLogin";
this.isVIP = (await checkCk()).isVIP;
const barray = [];
const msg = [];
for (let i = 0; i < videoInfo.data.result.episodes.length; i++) {
const totalEpisodes = videoInfo.data.result.episodes.length;
const long_title = videoInfo.data.result.episodes[i].long_title;
const badge = videoInfo.data.result.episodes[i].badge;
const short_link = videoInfo.data.result.episodes[i].short_link;
barray.push({
id: i + 1,
totalEpisodes,
long_title,
badge: badge === "" ? "暂无" : badge,
short_link
});
msg.push([
`
> ## 第${i + 1}集`,
`
> 标题: ${long_title}`,
`
> 类型: ${badge !== "预告" ? "正片" : "预告"}`,
`
> 🔒 播放要求: ${badge === "预告" || badge === "" ? "暂无" : badge}`,
this.botadapter !== "QQBot" ? `
> 🔗 分享链接: [🔗点击查看](${short_link})\r\r` : ""
]);
}
img$1 = await Render("bilibili/bangumi", {
saveId: "bangumi",
bangumiData: barray,
title: videoInfo.data.result.title
});
await this.e.reply([...img$1, segment.text("请在120秒内输入 第?集 选择集数")]);
await this.e.reply(segment.text("请在120秒内输入 第?集 选择集数"));
const context = await karin.ctx(this.e, { reply: true });
const regex = /第([一二三四五六七八九十百千万0-9]+)集/.exec(context.msg);
let Episode;
if (regex && regex[1]) {
Episode = regex[1];
if (/^[一二三四五六七八九十百千万]+$/.test(Episode)) {
Episode = Common.chineseToArabic(Episode).toString();
}
this.downloadfilename = videoInfo.data.result.episodes[Number(Episode) - 1].share_copy.substring(0, 50).replace(/[\\/:*?"<>|\r\n\s]/g, " ");
this.e.reply(`收到请求,第${Episode}集
${this.downloadfilename}
正在下载中`);
} else {
logger.debug(Episode);
this.e.reply("匹配内容失败,请重新发送链接再次解析");
return true;
}
const bangumidataBASEURL = bilibiliApiUrls.番剧视频流信息({
cid: videoInfo.data.result.episodes[Number(Episode) - 1].cid,
ep_id: videoInfo.data.result.episodes[Number(Episode) - 1].ep_id.toString()
});
const Params = await genParams(bangumidataBASEURL);
if (!this.islogin) await this.e.reply("B站ck未配置或已失效,无法获取视频流,可尝试【#B站登录】以配置新ck");
const playUrlData = await new Networks({
url: bangumidataBASEURL + Params,
headers: this.headers
}).getData();
if (videoInfo.data.result.episodes[Number(Episode) - 1].badge === "会员" && !this.isVIP) {
logger.warn("该CK不是大会员,无法获取视频流");
return true;
}
if (Config.bilibili.videoQuality === 0) {
const simplify = playUrlData.result.dash.video.filter((item, index, self) => {
return self.findIndex((t) => {
return t.id === item.id;
}) === index;
});
playUrlData.result.dash.video = simplify;
const correctList = await bilibiliProcessVideos({
accept_description: playUrlData.result.accept_description,
bvid: videoInfo.data.result.season_id.toString()
}, simplify, playUrlData.result.dash.a