This commit is contained in:
Jerry 2025-09-10 22:19:21 +08:00
parent c5ce5acfec
commit 55d4e17598
6 changed files with 165 additions and 921 deletions

View File

@ -157,6 +157,8 @@ export class RCtools extends plugin {
this.forceOverseasServer = this.toolsConfig.forceOverseasServer;
// 解析图片是否合并转发
this.globalImageLimit = this.toolsConfig.globalImageLimit;
//💩💩💩
this.nickName = '真寻';
}
/**
@ -283,12 +285,12 @@ export class RCtools extends plugin {
segment.image(user_cover),
segment.image(keyframe),
[
`${this.identifyPrefix}识别:哔哩哔哩直播${title}`,
`${description ? `📝 简述:${description.replace(`<p>`, '').replace(`</p>`, '')}` : ''}`,
`${tags ? `🔖 标签:${tags}` : ''}`,
`📍 分区:${parent_area_name ? `${parent_area_name}` : ''}${area_name ? `-${area_name}` : ''}`,
`${live_time ? `⏰ 直播时间:${live_time}` : ''}`,
`📺 独立播放器: https://www.bilibili.com/blackboard/live/live-activity-player.html?enterTheRoom=0&cid=${streamId}`,
`哼哼~${this.nickName}发现了一个哔哩哔哩直播!${title}`,
`${description ? `简单描述一下: ${description.replace(`<p>`, '').replace(`</p>`, '')}` : ''}`,
`${tags ? `标签是: ${tags}` : ''}`,
`属于: ${parent_area_name ? `${parent_area_name}` : ''}${area_name ? `-${area_name}` : ''}`,
`${live_time ? `直播时间是: ${live_time}` : ''}`,
`想看的话可以戳这里看奥: https://www.bilibili.com/blackboard/live/live-activity-player.html?enterTheRoom=0&cid=${streamId}`,
]
.filter((item) => item.trim() !== '')
.join('\n'),
@ -310,7 +312,7 @@ export class RCtools extends plugin {
url.includes('bilibili.com\/dynamic')
) {
if (_.isEmpty(this.biliSessData)) {
e.reply('检测到没有填写biliSessData无法解析动态');
e.reply(`看起来${this.nickName}暂时没有biliSessData呢..没法解析动态了..`);
return true;
}
url = this.biliDynamic(e, url, this.biliSessData);
@ -391,7 +393,7 @@ export class RCtools extends plugin {
summary &&
e.reply(
await Bot.makeForwardMsg(
textArrayToMakeForward(e, [`「R插件 x bilibili」联合为您总结内容`, summary])
textArrayToMakeForward(e, [`诺,${this.nickName}已经把内容给你整理好了噢:`, summary])
)
);
}
@ -399,7 +401,7 @@ export class RCtools extends plugin {
if (isLimitDuration) {
const durationInMinutes = (durationForCheck / 60).toFixed(0); // 使用 durationForCheck
biliInfo.push(
`${DIVIDING_LINE.replace('{}', '限制说明')}\n当前视频时长约:${durationInMinutes}分钟,\n大于管理员设置的最大时长 ${(this.biliDuration / 60).toFixed(2).replace(/\.00$/, '')} 分钟`
`${DIVIDING_LINE.replace('{}', '这视频真代派')}\n当前视频时长约:${durationInMinutes}分钟,\n大于${this.nickName}管理员设置的最大时长 ${(this.biliDuration / 60).toFixed(2).replace(/\.00$/, '')} 分钟噢..`
);
e.reply(biliInfo);
return true;
@ -485,16 +487,16 @@ export class RCtools extends plugin {
if (this.biliDisplayIntro) {
// 过滤简介中的一些链接
const filteredDesc = await filterBiliDescLink(desc);
combineContent += `\n📝 简介:${truncateString(filteredDesc, this.toolsConfig.biliIntroLenLimit || BILI_DEFAULT_INTRO_LEN_LIMIT)}`;
combineContent += `\n简介:${truncateString(filteredDesc, this.toolsConfig.biliIntroLenLimit || BILI_DEFAULT_INTRO_LEN_LIMIT)}`;
}
// 是否显示在线人数
if (this.biliDisplayOnline) {
// 拼接在线人数
const onlineTotal = await this.biliOnlineTotal(bvid, cid);
combineContent += `\n🏄‍♂️️ 当前视频有 ${onlineTotal.total} 人在观看,其中 ${onlineTotal.count} 人在网页端观看`;
combineContent += `\n吼吼吼~当前视频有 ${onlineTotal.total} 人在观看,其中 ${onlineTotal.count} 人在网页端观看!`;
}
let finalTitle = `${this.identifyPrefix}识别:哔哩哔哩,${displayTitle}`;
let finalTitle = `哼哼~${this.nickName}发现了一个哔哩哔哩视频! 名字叫做${displayTitle}`;
// 如果有多P标题并且它和主标题不一样则添加
if (partTitle && partTitle !== displayTitle) {
finalTitle += `|${pParam}P: ${partTitle}`;
@ -555,10 +557,10 @@ export class RCtools extends plugin {
e.reply(
[
segment.image(resp.result.cover),
`${this.identifyPrefix}识别:哔哩哔哩番剧,${title}\n🎯 评分: ${result?.rating?.score ?? '-'} / ${result?.rating?.count ?? '-'}\n📺 ${result.new_ep.desc}, ${result.seasons[0].new_ep.index_show}\n`,
`哼哼~${this.nickName}发现了哔哩哔哩的一部番剧!${title}\n评分竟然高达:${result?.rating?.score ?? '-'} / ${result?.rating?.count ?? '-'}\n ${result.new_ep.desc}, ${result.seasons[0].new_ep.index_show}\n`,
`${formatBiliInfo(dataProcessMap)}`,
`\n\n🪶 在线观看: ${await urlTransformShortLink(ANIME_SERIES_SEARCH_LINK + title)}`,
`\n🌸 在线观看: ${await urlTransformShortLink(ANIME_SERIES_SEARCH_LINK2 + title)}`,
`\n\n在线观看: ${await urlTransformShortLink(ANIME_SERIES_SEARCH_LINK + title)}`,
`\n在线观看: ${await urlTransformShortLink(ANIME_SERIES_SEARCH_LINK2 + title)}`,
],
true
);
@ -595,7 +597,6 @@ export class RCtools extends plugin {
// 发送视频
return this.sendVideoToUpload(e, `${tempPath}.mp4`);
}
e.reply('🚧 R插件提醒你开启但未检测到当前环境有【BBDown】即将使用默认下载方式 ( ◡̀_◡́)ᕤ');
}
// =================默认下载方式=====================
try {
@ -626,7 +627,7 @@ export class RCtools extends plugin {
} catch (err) {
// 错误处理
logger.error('[R插件][哔哩哔哩视频发送]下载错误,具体原因为:', err);
e.reply('解析失败,请重试一下');
e.reply('呜呜..解析失败了..请重试一下');
}
});
}
@ -673,7 +674,7 @@ export class RCtools extends plugin {
if (resp.dynamicSrc.length > 0 || resp.dynamicDesc) {
// 先发送动态描述文本
if (resp.dynamicDesc) {
e.reply(`${this.identifyPrefix}识别:哔哩哔哩动态\n${resp.dynamicDesc}`);
e.reply(`${this.nickName}发现了一条哔哩哔哩动态!\n${resp.dynamicDesc}`);
}
// 处理图片消息
@ -694,7 +695,7 @@ export class RCtools extends plugin {
}
}
} else {
await e.reply(`${this.identifyPrefix}识别:哔哩哔哩动态, 但是失败!`);
await e.reply(`${this.nickName}发现了一条哔哩哔哩动态, 但是解析失败!`);
}
});
return url;
@ -798,9 +799,7 @@ export class RCtools extends plugin {
logger.debug(
`[R插件][General Adapter Debug] Adapter object: ${JSON.stringify(adapter, null, 2)}`
);
e.reply(
`${this.identifyPrefix}识别:${adapter.name}${adapter.desc ? `, ${adapter.desc}` : ''}`
);
e.reply(`${this.nickName}识别: ${adapter.name}${adapter.desc ? `, ${adapter.desc}` : ''}`);
logger.debug(adapter);
logger.debug(
`[R插件][General Adapter Debug] adapter.images: ${JSON.stringify(adapter.images)}`
@ -831,7 +830,7 @@ export class RCtools extends plugin {
logger.debug(
`[R插件][General Adapter Debug] No images or video found for ${adapter.name}. Replying with failure message.`
);
e.reply('解析失败无法获取到资源');
e.reply('解析失败..无法获取到资源');
}
} catch (err) {
logger.error('解析失败 ', err);
@ -882,7 +881,7 @@ export class RCtools extends plugin {
} catch (e) {
realContent = content;
}
const normalMsg = `${this.identifyPrefix}识别:米游社,${subject}\n${realContent?.describe || ''}`;
const normalMsg = `${this.nickName}发现了一条米游社! ${subject}\n${realContent?.describe || ''}`;
const replyMsg = cover ? [segment.image(cover), normalMsg] : normalMsg;
e.reply(replyMsg);
// 图片
@ -1340,7 +1339,7 @@ export class RCtools extends plugin {
// 正常发送视频
if (videoSize > videoSizeLimit) {
e.reply(
`当前视频大小:${videoSize}MB\n大于设置的最大限制${videoSizeLimit}MB\n改为上传群文件`
`当前视频大小:${videoSize}MB\n大于${this.nickName}管理员设置的最大限制${videoSizeLimit}MB..\n改为上传群文件`
);
await this.uploadGroupFile(e, path); // uploadGroupFile 内部会处理删除
} else {

View File

@ -1,162 +0,0 @@
import axios from "axios";
import fs from "node:fs";
import path from "path";
import child_process from "node:child_process";
import util from "util";
/**
* 去除JSON的一些转义 \\" -> \" ->"
* @param str
*/
function escapeSpecialChars(str) {
return str.replace(/\\\\"/g, '\\"').replace(/\\"/g, '"');
}
const parseVideoName = videoInfo => {
const acfunId = "ac" + (videoInfo?.dougaId || "");
const acfunTitle = videoInfo?.title;
const acfunAuthor = videoInfo?.user.name;
const uploadTime = videoInfo?.createTime;
const description = videoInfo?.description;
const raw = [acfunId, acfunTitle, acfunAuthor, uploadTime, description]
.map(d => d || "")
.join("_")
.slice(0, 100);
return raw;
};
const parseVideoNameFixed = videoInfo => {
const f = parseVideoName(videoInfo);
const t = f.replaceAll(" ", "-");
return t;
};
async function parseUrl(videoUrlAddress) {
// eg https://www.acfun.cn/v/ac4621380?quickViewId=videoInfo_new&ajaxpipe=1
const urlSuffix = "?quickViewId=videoInfo_new&ajaxpipe=1";
const url = videoUrlAddress + urlSuffix;
const raw = await axios.get(url).then(resp => {
return resp.data;
});
// Split
const strsRemoveHeader = raw.split("window.pageInfo = window.videoInfo =");
const strsRemoveTail = strsRemoveHeader[1].split("</script>");
const strJson = strsRemoveTail[0];
const strJsonEscaped = escapeSpecialChars(strJson);
/** Object videoInfo */
const videoInfo = JSON.parse(strJsonEscaped);
const videoName = parseVideoNameFixed(videoInfo);
const ksPlayJson = videoInfo.currentVideoInfo.ksPlayJson;
/** Object ksPlay */
const ksPlay = JSON.parse(ksPlayJson);
const representations = ksPlay.adaptationSet[0].representation;
const urlM3u8s = representations.map(d => d.url);
return { urlM3u8s, videoName };
}
async function parseM3u8(m3u8Url) {
const m3u8File = await axios.get(m3u8Url).then(resp => resp.data);
/** 分离ts文件链接 */
const rawPieces = m3u8File.split(/\n#EXTINF:.{8},\n/);
/** 过滤头部 */
const m3u8RelativeLinks = rawPieces.slice(1);
/** 修改尾部 去掉尾部多余的结束符 */
const patchedTail = m3u8RelativeLinks[m3u8RelativeLinks.length - 1].split("\n")[0];
m3u8RelativeLinks[m3u8RelativeLinks.length - 1] = patchedTail;
/** 完整链接直接加m3u8Url的通用前缀 */
const m3u8Prefix = m3u8Url.split("/").slice(0, -1).join("/");
const m3u8FullUrls = m3u8RelativeLinks.map(d => m3u8Prefix + "/" + d);
/** aria2c下载的文件名就是取url最后一段去掉末尾url参数(?之后是url参数) */
const tsNames = m3u8RelativeLinks.map(d => d.split("?")[0]);
/** 文件夹名,去掉文件名末尾分片号 */
let outputFolderName = tsNames[0].slice(0, -9);
/** 输出最后合并的文件名加个通用mp4后缀 */
const outputFileName = outputFolderName + ".mp4";
return {
m3u8FullUrls,
tsNames,
outputFolderName,
outputFileName,
};
}
// 下载m3u8
async function downloadM3u8Videos(m3u8FullUrls, outputFolderName) {
/** 新建下载文件夹 在当前运行目录下 */
const outPath = outputFolderName;
/** 批下载 */
const strDownloadParamFiles = m3u8FullUrls.map(async (d, i) => {
return new Promise((resolve, reject) => {
const writer = fs.createWriteStream(outPath + `${i}.ts`);
axios
.get(d, {
headers: {
"User-Agent":
"Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Mobile Safari/537.36",
},
responseType: "stream",
})
.then(dres => {
dres.data.pipe(writer);
writer.on("finish", () => resolve(true));
writer.on("error", () => reject);
});
});
});
/** 写入下载链接列表文件 */
// fs.writeFileSync(path.resolveControl(outPath, "urls.txt"), str下载参数文件);
return Promise.all(strDownloadParamFiles);
}
async function mergeAcFileToMp4(tsNames, FullFileName, outputFileName, shouldDelete = true) {
/** 合并参数列表 格式file path */
const concatStrs = tsNames.map(
(d, i) => `file ${path.resolve(FullFileName, i + ".ts").replace(/\\/g, "/")}`,
);
const ffmpegList = path.resolve(FullFileName, "file.txt");
fs.writeFileSync(ffmpegList, concatStrs.join("\n"));
const outPath = path.resolve(outputFileName);
// 判断当前环境
let env;
if (process.platform === "win32") {
env = process.env;
} else if (process.platform === "linux") {
env = {
...process.env,
PATH: "/usr/local/bin:" + child_process.execSync("echo $PATH").toString(),
};
} else {
console.log("暂时不支持当前操作系统!");
}
const execFile = util.promisify(child_process.execFile);
try {
const cmd = "ffmpeg";
const args = ["-y", "-f", "concat", "-safe", "0", "-i", ffmpegList, "-c", "copy", outPath];
await execFile(cmd, args, { env });
if (shouldDelete) {
fs.unlink(FullFileName, f => f);
}
return { outputFileName };
} catch (err) {
logger.error(err);
}
}
export { parseUrl, parseM3u8, downloadM3u8Videos, mergeAcFileToMp4 };

View File

@ -1,94 +0,0 @@
import axios from "axios";
import { downloadAudio, generateRandomStr } from "./common.js";
/**
* 获取音频
* @param id
* @param path
* @param songName
* @returns {Promise<any>}
*/
async function getBodianAudio(id, path, songName = "temp") {
// 音乐数据
const API = `https://bd-api.kuwo.cn/api/service/music/audioUrl/${id}?format=mp3&br=320kmp3&songType=&fromList=&weListenUid=&weListenDevId=`;
const headers = {
"User-Agent": "bodian/106 CFNetwork/1399 Darwin/22.1.0",
devId: `95289318-8847-43D5-8477-85296654785${String.fromCharCode(
65 + Math.floor(Math.random() * 26),
)}`,
Host: "bd-api.kuwo.cn",
plat: "ip",
ver: "3.1.0",
"Cache-Control": "no-cache",
channel: "appstore",
};
const resp = await axios.get(API, {
headers,
});
const respJson = resp.data;
const audioUrl = respJson.data.audioUrl;
return await downloadAudio(audioUrl, path, songName)
.catch(err => {
console.error(`下载音乐失败,错误信息为: ${ err.message }`);
});
}
/**
* 获取MV地址
* @param id
* @returns {Promise<(fid: string, pid: string) => Promise<void>>}
*/
async function getBodianMv(id) {
// mv数据
const API = `https://bd-api.kuwo.cn/api/service/mv/info?musicId=${id}&wifi=1&noWifi=1&uid=-1&token=`
const headers = {
"User-Agent": "Dart/2.18 (dart:io)",
plat: "ar",
ver: "3.1.0",
host: "bd-api.kuwo.cn",
channel: "aliopen",
devId: generateRandomStr(16)
}
return await axios.get(API, {
headers
}).then(async resp => {
const res = resp.data;
// 如果没有,直接返回
if (res.data.lowUrl === null || res.data.highUrl === null) {
return;
}
// 波点音乐信息
return res.data.mv;
}).catch(err => {
logger.error("波点音乐错误");
});
}
/**
* 获取音乐信息
* @returns {Promise<void>}
*/
async function getBodianMusicInfo(id) {
const API = `https://bd-api.kuwo.cn/api/service/music/info?musicId=${id}&uid=-1&token=`
const headers = {
"User-Agent": "Dart/2.18 (dart:io)",
plat: "ar",
ver: "3.1.0",
host: "bd-api.kuwo.cn",
channel: "aliopen",
devId: generateRandomStr(16)
}
return await axios.get(API, {
headers
}).then(async resp => {
return resp.data?.data;
}).catch(err => {
logger.error("波点音乐错误");
});
}
export {
getBodianAudio,
getBodianMv,
getBodianMusicInfo
}

View File

@ -1,236 +1,15 @@
import axios from "axios";
import { exec } from "child_process";
import { HttpsProxyAgent } from 'https-proxy-agent';
import fetch from "node-fetch";
import fs from "node:fs";
import os from "os";
import path from 'path';
import { BILI_DOWNLOAD_METHOD, COMMON_USER_AGENT, SHORT_LINKS, TEN_THOUSAND } from "../constants/constant.js";
import { mkdirIfNotExists } from "./file.js";
/**
* 生成随机字符串
*
* @param {number} [randomlength=16] 生成的字符串长度默认为16
* @returns {string} 生成的随机字符串
*
* @description
* 此函数生成一个指定长度的随机字符串
* 字符串由大小写字母数字和等号组成
* 使用 Array.from 和箭头函数来创建随机字符数组然后用 join 方法连接
*
* @example
* const randomString = generateRandomStr(); // 生成默认长度16的随机字符串
* const randomString20 = generateRandomStr(20); // 生成长度为20的随机字符串
*/
export function generateRandomStr(randomlength = 16) {
const base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789=';
return Array.from({ length: randomlength }, () => base_str.charAt(Math.floor(Math.random() * base_str.length))).join('');
}
/**
* 下载mp3
* @param mp3Url MP3地址
* @param filePath 下载目录
* @param title 音乐名
* @param redirect 是否要重定向
* @param audioType 建议填写 mp3 / m4a / flac 类型
* @returns {Promise<unknown>}
*/
export async function downloadAudio(mp3Url, filePath, title = "temp", redirect = "manual", audioType = "mp3") {
// 如果没有目录就创建一个
await mkdirIfNotExists(filePath)
// 补充保存文件名
filePath += `/${ title }.${ audioType }`;
if (fs.existsSync(filePath)) {
logger.info(`音频已存在`);
fs.unlinkSync(filePath);
}
try {
const response = await axios({
method: 'get',
url: mp3Url,
responseType: 'stream',
headers: {
"User-Agent": "Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Mobile Safari/537.36"
}
});
// 开始下载
const writer = fs.createWriteStream(filePath);
response.data.pipe(writer);
return new Promise((resolve, reject) => {
writer.on('finish', () => resolve(filePath));
writer.on('error', reject);
});
} catch (error) {
logger.error(`下载音乐失败,错误信息为: ${ error.message }`);
throw error;
}
}
/**
* 下载图片网关
* @param {Object} options 参数对象
* @param {string} options.img 图片的URL
* @param {string} options.dir 保存图片的目录
* @param {string} [options.fileName] 自定义文件名 (可选)
* @param {boolean} [options.isProxy] 是否使用代理 (可选)
* @param {Object} [options.headersExt] 自定义请求头 (可选)
* @param {Object} [options.proxyInfo] 代理信息 (可选)
* @returns {Promise<string>}
*/
export async function downloadImg({
img,
dir,
fileName = "",
isProxy = false,
headersExt = {},
proxyInfo = {},
downloadMethod = 0,
}) {
const downloadImgParams = {
img,
dir,
fileName,
isProxy,
headersExt,
proxyInfo,
}
logger.info(logger.yellow(`[R插件][图片下载] 当前使用的方法:${ BILI_DOWNLOAD_METHOD[downloadMethod].label }`));
if (downloadMethod === 0) {
return normalDownloadImg(downloadImgParams);
} else if (downloadMethod >= 1) {
return downloadImgWithAria2(downloadImgParams);
}
}
/**
* 正常下载图片
* @param {Object} options 参数对象
* @param {string} options.img 图片的URL
* @param {string} options.dir 保存图片的目录
* @param {string} [options.fileName] 自定义文件名 (可选)
* @param {boolean} [options.isProxy] 是否使用代理 (可选)
* @param {Object} [options.headersExt] 自定义请求头 (可选)
* @param {Object} [options.proxyInfo] 代理信息 (可选)
* @returns {Promise<string>}
*/
async function normalDownloadImg({
img,
dir,
fileName = "",
isProxy = false,
headersExt = {},
proxyInfo = {}
}) {
if (fileName === "") {
fileName = img.split("/").pop();
}
const filepath = `${ dir }/${ fileName }`;
await mkdirIfNotExists(dir)
const writer = fs.createWriteStream(filepath);
const axiosConfig = {
headers: {
"User-Agent": COMMON_USER_AGENT,
...headersExt
},
responseType: "stream",
};
// 添加🪜
if (isProxy) {
axiosConfig.httpsAgent = new HttpsProxyAgent({
host: proxyInfo.proxyAddr,
port: proxyInfo.proxyPort
});
}
try {
const res = await axios.get(img, axiosConfig);
res.data.pipe(writer);
return new Promise((resolve, reject) => {
writer.on("finish", () => {
writer.close(() => {
resolve(filepath);
});
});
writer.on("error", err => {
fs.unlink(filepath, () => {
reject(err);
});
});
});
} catch (err) {
logger.error(`图片下载失败, 原因:${ err }`);
}
}
/**
* 下载一张网络图片(使用aria2加速下载)
* @param {Object} options 参数对象
* @param {string} options.img 图片的URL
* @param {string} options.dir 保存图片的目录
* @param {string} [options.fileName] 自定义文件名 (可选)
* @param {boolean} [options.isProxy] 是否使用代理 (可选)
* @param {Object} [options.headersExt] 自定义请求头 (可选)
* @param {Object} [options.proxyInfo] 代理信息 (可选)
* @param {number} [options.numThread] 线程数 (可选)
* @returns {Promise<unknown>}
*/
async function downloadImgWithAria2({
img,
dir,
fileName = "",
isProxy = false,
headersExt = {},
proxyInfo = {},
numThread = 1,
}) {
if (fileName === "") {
fileName = img.split("/").pop();
}
const filepath = path.resolve(dir, fileName);
await mkdirIfNotExists(dir);
// 构建 aria2c 命令
let aria2cCmd = `aria2c "${ img }" --dir="${ dir }" --out="${ fileName }" --max-connection-per-server=${ numThread } --split=${ numThread } --min-split-size=1M --continue`;
// 如果需要代理
if (isProxy) {
aria2cCmd += ` --all-proxy="http://${ proxyInfo.proxyAddr }:${ proxyInfo.proxyPort }"`;
}
// 添加自定义headers
if (headersExt && Object.keys(headersExt).length > 0) {
for (const [headerName, headerValue] of Object.entries(headersExt)) {
aria2cCmd += ` --header="${ headerName }: ${ headerValue }"`;
}
}
return new Promise((resolve, reject) => {
exec(aria2cCmd, (error, stdout, stderr) => {
if (error) {
logger.error(`图片下载失败, 原因:${ error.message }`);
reject(error);
return;
}
resolve(filepath);
});
});
}
import { exec } from 'child_process';
import fetch from 'node-fetch';
import os from 'os';
import { SHORT_LINKS, TEN_THOUSAND } from '../constants/constant.js';
/**
* 千位数的数据处理
* @param data
* @return {string|*}
*/
const dataProcessing = data => {
return Number(data) >= TEN_THOUSAND ? (data / TEN_THOUSAND).toFixed(1) + "万" : data;
const dataProcessing = (data) => {
return Number(data) >= TEN_THOUSAND ? (data / TEN_THOUSAND).toFixed(1) + '万' : data;
};
/**
@ -239,7 +18,9 @@ const dataProcessing = data => {
* @return {string}
*/
export function formatBiliInfo(data) {
return Object.keys(data).map(key => `${ key }${ dataProcessing(data[key]) }`).join(' | ');
return Object.keys(data)
.map((key) => `${key}${dataProcessing(data[key])}`)
.join(' | ');
}
/**
@ -258,34 +39,7 @@ export function secondsToTime(seconds) {
// return `${pad(minutes, 2)}:${pad(secs, 2)}`;
// 完整的 HH:MM:SS 格式
return `${ pad(hours, 2) }:${ pad(minutes, 2) }:${ pad(secs, 2) }`;
}
/**
* 判断字符串是否是中文全局判断
* @param str
* @returns {boolean}
*/
export function isChinese(str) {
return /^[\u4e00-\u9fff]+$/.test(str);
}
/**
* 判断字符串是否包含中文
* @param str
* @returns {boolean}
*/
export function containsChinese(str) {
return /[\u4e00-\u9fff]/.test(str);
}
/**
* 判断字符串是否包含中文 && 检测标点符号
* @param str
* @returns {boolean}
*/
export function containsChineseOrPunctuation(str) {
return /[\u4e00-\u9fff\uff00-\uffef]/.test(str);
return `${pad(hours, 2)}:${pad(minutes, 2)}:${pad(secs, 2)}`;
}
/**
@ -300,56 +54,6 @@ export function truncateString(inputString, maxLength = 50) {
: inputString.substring(0, maxLength) + '...';
}
/**
* 测试当前是否存在🪜
* @returns {Promise<Boolean>}
*/
export async function testProxy(host = '127.0.0.1', port = 7890) {
// 创建一个代理隧道
const httpsAgent = new HttpsProxyAgent(`http://${ host }:${ port }`);
try {
// 通过代理服务器发起请求
await axios.get('https://www.google.com', { httpsAgent });
logger.mark(logger.yellow('[R插件][梯子测试模块] 检测到梯子'));
return true;
} catch (error) {
logger.error('[R插件][梯子测试模块] 检测不到梯子');
return false;
}
}
export function formatSeconds(seconds) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${ minutes }${ remainingSeconds }`;
}
/**
* 重试 axios 请求
* @param requestFunction
* @param retries
* @param delay
* @returns {*}
*/
export async function retryAxiosReq(requestFunction, retries = 3, delay = 1000) {
try {
const response = await requestFunction();
if (!response.data) {
throw new Error('请求空数据');
}
return response.data;
} catch (error) {
if (retries > 0) {
logger.mark(`[R插件][重试模块]重试中... (${ 3 - retries + 1 }/3) 次`);
await new Promise(resolve => setTimeout(resolve, delay));
return retryAxiosReq(requestFunction, retries - 1, delay);
} else {
throw error;
}
}
}
/**
* 重试 fetch 请求
* @param {string} url 请求的URL
@ -367,8 +71,10 @@ export async function retryFetch(url, options, retries = 3, delay = 1000) {
return response;
} catch (error) {
if (retries > 0) {
logger.mark(`[R插件][重试模块] 请求失败: ${error.message},重试中... (${3 - retries + 1}/3) 次`);
await new Promise(resolve => setTimeout(resolve, delay));
logger.mark(
`[R插件][重试模块] 请求失败: ${error.message},重试中... (${3 - retries + 1}/3) 次`
);
await new Promise((resolve) => setTimeout(resolve, delay));
return retryFetch(url, options, retries - 1, delay);
} else {
throw error;
@ -400,7 +106,7 @@ export function estimateReadingTime(text, wpm = 200) {
const readingTimeMinutes = wordCount / wpm;
return {
minutes: Math.ceil(readingTimeMinutes),
words: wordCount
words: wordCount,
};
}
@ -412,56 +118,20 @@ export function estimateReadingTime(text, wpm = 200) {
export function checkToolInCurEnv(someCommand) {
// 根据操作系统选择命令
return new Promise((resolve, reject) => {
const command = os.platform() === 'win32' ? `where ${ someCommand }` : `which ${ someCommand }`;
const command = os.platform() === 'win32' ? `where ${someCommand}` : `which ${someCommand}`;
exec(command, (error, stdout, stderr) => {
if (error) {
logger.error(`[R插件][命令环境检测]未找到${ someCommand }: ${ stderr || error.message }`);
logger.error(`[R插件][命令环境检测]未找到${someCommand}: ${stderr || error.message}`);
resolve(false);
return;
}
logger.info(`[R插件][命令环境检测]找到${ someCommand }: ${ stdout.trim() }`);
logger.info(`[R插件][命令环境检测]找到${someCommand}: ${stdout.trim()}`);
resolve(true);
});
});
}
/**
* debug JSON 数据保存到本地文件
* eg. saveJsonToFile(data, 'data.json', (err) => {})
* @param {Object} jsonData - 要保存的 JSON 数据
* @param {string} filename - 目标文件名
* @param {function} callback - 可选的回调函数处理写入完成后的操作
*/
export function saveJsonToFile(jsonData, filename = "data.json") {
// 转换 JSON 数据为字符串
const jsonString = JSON.stringify(jsonData, null, 2); // 第二个参数是 replacer第三个参数是缩进
// 保存到文件
return fs.writeFile(filename, jsonString, 'utf8', (err) => {
if (err) {
logger.error('Error writing file', err);
} else {
logger.info('File successfully written');
}
});
}
/**
* 删除文件名中的特殊符号待完善
* @param filename
* @returns {string}
*/
export function cleanFilename(filename) {
// 1. 去除特殊字符
// 2. 去除特定词汇
filename = filename.replace(/[\/\?<>\\:\*\|".…《》()]/g, '')
.replace(/电影|主题曲/g, '')
.trim();
return filename;
}
/**
* 转换短链接
* @param url
@ -469,16 +139,16 @@ export function cleanFilename(filename) {
*/
export async function urlTransformShortLink(url) {
const data = {
url: `${ encodeURI(url) }`
url: `${encodeURI(url)}`,
};
const resp = await fetch(SHORT_LINKS, {
method: 'POST',
headers: {
'Accept': 'application/json',
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
}).then(response => response.json());
body: JSON.stringify(data),
}).then((response) => response.json());
return await resp.data.short_url;
}

View File

@ -1,33 +1,6 @@
import path from 'path';
import { exec } from 'child_process';
import fs from "fs";
/**
* 提取关键帧
* @param inputFilePath
* @param outputFolderPath
* @param frameCount
* @returns {Promise<unknown>}
*/
export async function extractKeyframes(inputFilePath, outputFolderPath, frameCount = 20) {
return new Promise((resolve, reject) => {
// 创建输出文件夹路径
const outputFilePattern = path.join(outputFolderPath, 'keyframe_%03d.jpg');
// 构建FFmpeg命令
const ffmpegCommand = `ffmpeg -i "${inputFilePath}" -vf "select=eq(pict_type\\,I)" -vsync drop -vframes ${frameCount} -qscale:v 2 "${outputFilePattern}"`;
// 执行FFmpeg命令
exec(ffmpegCommand, (error, stdout, stderr) => {
if (error) {
reject(`[R插件][ffmpeg工具]执行FFmpeg命令时出错: ${ stderr }`);
} else {
logger.info(`[R插件][ffmpeg工具]关键帧成功提取到 ${ outputFolderPath }`);
resolve(outputFolderPath);
}
});
});
}
import fs from 'fs';
/**
* 使用 ffmpeg FLV 文件转换为 MP4 文件

View File

@ -1,5 +1,5 @@
import { promises as fs } from "fs";
import path from "path";
import { promises as fs } from 'fs';
import path from 'path';
// 常量提取
const mimeTypes = {
@ -17,24 +17,10 @@ const mimeTypes = {
* @param err
*/
function handleError(err) {
logger.error(`错误: ${ err.message }\n堆栈: ${ err.stack }`);
logger.error(`错误: ${err.message}\n堆栈: ${err.stack}`);
throw err;
}
/**
* 异步的方式检查文件是否存在
* @param filePath
* @returns {Promise<boolean>}
*/
export async function checkFileExists(filePath) {
try {
await fs.access(filePath);
return true; // 文件存在
} catch (error) {
return false; // 文件不存在
}
}
/**
* 检查文件是否存在并且删除
* @param {string} file - 文件路径
@ -44,7 +30,7 @@ export async function checkAndRemoveFile(file) {
try {
await fs.access(file);
await fs.unlink(file);
logger.info(`文件 ${ file } 删除成功。`);
logger.info(`文件 ${file} 删除成功。`);
} catch (err) {
if (err.code !== 'ENOENT') {
handleError(err);
@ -63,7 +49,7 @@ export async function mkdirIfNotExists(dir) {
} catch (err) {
if (err.code === 'ENOENT') {
await fs.mkdir(dir, { recursive: true });
logger.info(`目录 ${ dir } 创建成功。`);
logger.info(`目录 ${dir} 创建成功。`);
} else {
handleError(err);
}
@ -89,7 +75,7 @@ export async function deleteFolderRecursive(folderPath) {
});
await Promise.allSettled(actions);
logger.info(`文件夹 ${ folderPath } 中的所有文件删除成功。`);
logger.info(`文件夹 ${folderPath} 中的所有文件删除成功。`);
return files.length;
} catch (error) {
handleError(error);
@ -109,131 +95,3 @@ export async function readCurrentDir(dirPath) {
handleError(err);
}
}
/**
* 拷贝文件
* @param {string} srcDir - 源文件目录
* @param {string} destDir - 目标文件目录
* @param {string[]} [specificFiles=[]] - 过滤文件不填写就拷贝全部
* @returns {Promise<string[]>} 拷贝的文件列表
*/
export async function copyFiles(srcDir, destDir, specificFiles = []) {
try {
await mkdirIfNotExists(destDir);
const files = await readCurrentDir(srcDir);
const filesToCopy = specificFiles.length > 0
? files.filter(file => specificFiles.includes(file))
: files;
logger.info(`[R插件][拷贝文件] 正在将 ${ srcDir } 的文件拷贝到 ${ destDir }`);
const copiedFiles = [];
for (const file of filesToCopy) {
const srcFile = path.join(srcDir, file);
const destFile = path.join(destDir, file);
await fs.copyFile(srcFile, destFile);
copiedFiles.push(file);
}
logger.info(`[R插件][拷贝文件] 拷贝完成`);
return copiedFiles;
} catch (error) {
handleError(error);
return [];
}
}
/**
* 转换路径图片为base64格式
* @param {string} filePath - 图片路径
* @returns {Promise<string>} Base64字符串
*/
export async function toBase64(filePath) {
try {
const fileData = await fs.readFile(filePath);
const base64Data = fileData.toString('base64');
return `data:${ getMimeType(filePath) };base64,${ base64Data }`;
} catch (error) {
handleError(error);
}
}
/**
* 辅助函数根据文件扩展名获取MIME类型
* @param {string} filePath - 文件路径
* @returns {string} MIME类型
*/
function getMimeType(filePath) {
const ext = path.extname(filePath).toLowerCase();
return mimeTypes[ext] || 'application/octet-stream';
}
/**
* 获取文件夹中的图片和视频文件
* @param {string} folderPath - 要检测的文件夹路径
* @returns {Promise<Object>} 包含图片和视频文件名的对象
*/
export async function getMediaFilesAndOthers(folderPath) {
try {
const files = await fs.readdir(folderPath);
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'];
const videoExtensions = ['.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm'];
const images = [];
const videos = [];
const others = [];
files.forEach(file => {
const ext = path.extname(file).toLowerCase();
if (imageExtensions.includes(ext)) {
images.push(file);
} else if (videoExtensions.includes(ext)) {
videos.push(file);
} else {
others.push(file);
}
});
return { images, videos, others };
} catch (err) {
handleError(err);
}
}
/**
* 将文件路径解析为标准格式
* @param {string|string[]} input - 输入的文件路径,支持单个字符串路径或路径数组
* @returns {Array<Object>} 返回解析后的文件信息数组,每个对象包含:
* - dir: 文件所在目录的完整路径
* - fileName: 完整的文件名(包含扩展名)
* - extension: 文件扩展名( .js.txt )
* - baseFileName: 不含扩展名的文件名
*
* @example
* // 单个文件路径
* splitPaths('/root/test.txt')
* // 返回: [{
* // dir: '/root',
* // fileName: 'test.txt',
* // extension: '.txt',
* // baseFileName: 'test'
* // }]
*
* // 多个文件路径
* splitPaths(['/root/a.js', '/root/b.css'])
* @returns {{fileName: string, extension: string, dir: string, baseFileName: string}[]} 返回一个包含文件信息的对象数组
*/
export function splitPaths(input) {
const paths = Array.isArray(input) ? input : [input];
return paths.map(filePath => {
const dir = path.dirname(filePath);
const fileName = path.basename(filePath);
const extension = path.extname(fileName);
const baseFileName = path.basename(fileName, extension); // 去除扩展名的文件名
return { dir, fileName, extension, baseFileName };
});
}