mirror of
https://github.com/Jerryplusy/rc-plugin.git
synced 2025-10-14 16:19:18 +00:00
💩
This commit is contained in:
parent
c5ce5acfec
commit
55d4e17598
@ -157,6 +157,8 @@ export class RCtools extends plugin {
|
|||||||
this.forceOverseasServer = this.toolsConfig.forceOverseasServer;
|
this.forceOverseasServer = this.toolsConfig.forceOverseasServer;
|
||||||
// 解析图片是否合并转发
|
// 解析图片是否合并转发
|
||||||
this.globalImageLimit = this.toolsConfig.globalImageLimit;
|
this.globalImageLimit = this.toolsConfig.globalImageLimit;
|
||||||
|
//💩💩💩
|
||||||
|
this.nickName = '真寻';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -283,12 +285,12 @@ export class RCtools extends plugin {
|
|||||||
segment.image(user_cover),
|
segment.image(user_cover),
|
||||||
segment.image(keyframe),
|
segment.image(keyframe),
|
||||||
[
|
[
|
||||||
`${this.identifyPrefix}识别:哔哩哔哩直播,${title}`,
|
`哼哼~${this.nickName}发现了一个哔哩哔哩直播!,${title}`,
|
||||||
`${description ? `📝 简述:${description.replace(`<p>`, '').replace(`</p>`, '')}` : ''}`,
|
`${description ? `简单描述一下: ${description.replace(`<p>`, '').replace(`</p>`, '')}` : ''}`,
|
||||||
`${tags ? `🔖 标签:${tags}` : ''}`,
|
`${tags ? `标签是: ${tags}` : ''}`,
|
||||||
`📍 分区:${parent_area_name ? `${parent_area_name}` : ''}${area_name ? `-${area_name}` : ''}`,
|
`属于: ${parent_area_name ? `${parent_area_name}` : ''}${area_name ? `-${area_name}` : ''}`,
|
||||||
`${live_time ? `⏰ 直播时间:${live_time}` : ''}`,
|
`${live_time ? `直播时间是: ${live_time}` : ''}`,
|
||||||
`📺 独立播放器: https://www.bilibili.com/blackboard/live/live-activity-player.html?enterTheRoom=0&cid=${streamId}`,
|
`想看的话可以戳这里看奥: https://www.bilibili.com/blackboard/live/live-activity-player.html?enterTheRoom=0&cid=${streamId}`,
|
||||||
]
|
]
|
||||||
.filter((item) => item.trim() !== '')
|
.filter((item) => item.trim() !== '')
|
||||||
.join('\n'),
|
.join('\n'),
|
||||||
@ -310,7 +312,7 @@ export class RCtools extends plugin {
|
|||||||
url.includes('bilibili.com\/dynamic')
|
url.includes('bilibili.com\/dynamic')
|
||||||
) {
|
) {
|
||||||
if (_.isEmpty(this.biliSessData)) {
|
if (_.isEmpty(this.biliSessData)) {
|
||||||
e.reply('检测到没有填写biliSessData,无法解析动态');
|
e.reply(`看起来${this.nickName}暂时没有biliSessData呢..没法解析动态了..`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
url = this.biliDynamic(e, url, this.biliSessData);
|
url = this.biliDynamic(e, url, this.biliSessData);
|
||||||
@ -391,7 +393,7 @@ export class RCtools extends plugin {
|
|||||||
summary &&
|
summary &&
|
||||||
e.reply(
|
e.reply(
|
||||||
await Bot.makeForwardMsg(
|
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) {
|
if (isLimitDuration) {
|
||||||
const durationInMinutes = (durationForCheck / 60).toFixed(0); // 使用 durationForCheck
|
const durationInMinutes = (durationForCheck / 60).toFixed(0); // 使用 durationForCheck
|
||||||
biliInfo.push(
|
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);
|
e.reply(biliInfo);
|
||||||
return true;
|
return true;
|
||||||
@ -485,16 +487,16 @@ export class RCtools extends plugin {
|
|||||||
if (this.biliDisplayIntro) {
|
if (this.biliDisplayIntro) {
|
||||||
// 过滤简介中的一些链接
|
// 过滤简介中的一些链接
|
||||||
const filteredDesc = await filterBiliDescLink(desc);
|
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) {
|
if (this.biliDisplayOnline) {
|
||||||
// 拼接在线人数
|
// 拼接在线人数
|
||||||
const onlineTotal = await this.biliOnlineTotal(bvid, cid);
|
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标题,并且它和主标题不一样,则添加
|
// 如果有多P标题,并且它和主标题不一样,则添加
|
||||||
if (partTitle && partTitle !== displayTitle) {
|
if (partTitle && partTitle !== displayTitle) {
|
||||||
finalTitle += `|${pParam}P: ${partTitle}`;
|
finalTitle += `|${pParam}P: ${partTitle}`;
|
||||||
@ -555,10 +557,10 @@ export class RCtools extends plugin {
|
|||||||
e.reply(
|
e.reply(
|
||||||
[
|
[
|
||||||
segment.image(resp.result.cover),
|
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)}`,
|
`${formatBiliInfo(dataProcessMap)}`,
|
||||||
`\n\n🪶 在线观看: ${await urlTransformShortLink(ANIME_SERIES_SEARCH_LINK + title)}`,
|
`\n\n在线观看: ${await urlTransformShortLink(ANIME_SERIES_SEARCH_LINK + title)}`,
|
||||||
`\n🌸 在线观看: ${await urlTransformShortLink(ANIME_SERIES_SEARCH_LINK2 + title)}`,
|
`\n在线观看: ${await urlTransformShortLink(ANIME_SERIES_SEARCH_LINK2 + title)}`,
|
||||||
],
|
],
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
@ -595,7 +597,6 @@ export class RCtools extends plugin {
|
|||||||
// 发送视频
|
// 发送视频
|
||||||
return this.sendVideoToUpload(e, `${tempPath}.mp4`);
|
return this.sendVideoToUpload(e, `${tempPath}.mp4`);
|
||||||
}
|
}
|
||||||
e.reply('🚧 R插件提醒你:开启但未检测到当前环境有【BBDown】,即将使用默认下载方式 ( ◡̀_◡́)ᕤ');
|
|
||||||
}
|
}
|
||||||
// =================默认下载方式=====================
|
// =================默认下载方式=====================
|
||||||
try {
|
try {
|
||||||
@ -626,7 +627,7 @@ export class RCtools extends plugin {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// 错误处理
|
// 错误处理
|
||||||
logger.error('[R插件][哔哩哔哩视频发送]下载错误,具体原因为:', 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.dynamicSrc.length > 0 || resp.dynamicDesc) {
|
||||||
// 先发送动态描述文本
|
// 先发送动态描述文本
|
||||||
if (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 {
|
} else {
|
||||||
await e.reply(`${this.identifyPrefix}识别:哔哩哔哩动态, 但是失败!`);
|
await e.reply(`${this.nickName}发现了一条哔哩哔哩动态, 但是解析失败!`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return url;
|
return url;
|
||||||
@ -798,9 +799,7 @@ export class RCtools extends plugin {
|
|||||||
logger.debug(
|
logger.debug(
|
||||||
`[R插件][General Adapter Debug] Adapter object: ${JSON.stringify(adapter, null, 2)}`
|
`[R插件][General Adapter Debug] Adapter object: ${JSON.stringify(adapter, null, 2)}`
|
||||||
);
|
);
|
||||||
e.reply(
|
e.reply(`${this.nickName}识别: ${adapter.name}${adapter.desc ? `, ${adapter.desc}` : ''}`);
|
||||||
`${this.identifyPrefix}识别:${adapter.name}${adapter.desc ? `, ${adapter.desc}` : ''}`
|
|
||||||
);
|
|
||||||
logger.debug(adapter);
|
logger.debug(adapter);
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`[R插件][General Adapter Debug] adapter.images: ${JSON.stringify(adapter.images)}`
|
`[R插件][General Adapter Debug] adapter.images: ${JSON.stringify(adapter.images)}`
|
||||||
@ -831,7 +830,7 @@ export class RCtools extends plugin {
|
|||||||
logger.debug(
|
logger.debug(
|
||||||
`[R插件][General Adapter Debug] No images or video found for ${adapter.name}. Replying with failure message.`
|
`[R插件][General Adapter Debug] No images or video found for ${adapter.name}. Replying with failure message.`
|
||||||
);
|
);
|
||||||
e.reply('解析失败:无法获取到资源');
|
e.reply('解析失败..无法获取到资源');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('解析失败 ', err);
|
logger.error('解析失败 ', err);
|
||||||
@ -882,7 +881,7 @@ export class RCtools extends plugin {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
realContent = content;
|
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;
|
const replyMsg = cover ? [segment.image(cover), normalMsg] : normalMsg;
|
||||||
e.reply(replyMsg);
|
e.reply(replyMsg);
|
||||||
// 图片
|
// 图片
|
||||||
@ -1340,7 +1339,7 @@ export class RCtools extends plugin {
|
|||||||
// 正常发送视频
|
// 正常发送视频
|
||||||
if (videoSize > videoSizeLimit) {
|
if (videoSize > videoSizeLimit) {
|
||||||
e.reply(
|
e.reply(
|
||||||
`当前视频大小:${videoSize}MB,\n大于设置的最大限制:${videoSizeLimit}MB,\n改为上传群文件`
|
`当前视频大小:${videoSize}MB,\n大于${this.nickName}管理员设置的最大限制${videoSizeLimit}MB..\n改为上传群文件`
|
||||||
);
|
);
|
||||||
await this.uploadGroupFile(e, path); // uploadGroupFile 内部会处理删除
|
await this.uploadGroupFile(e, path); // uploadGroupFile 内部会处理删除
|
||||||
} else {
|
} else {
|
||||||
|
162
utils/acfun.js
162
utils/acfun.js
@ -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 };
|
|
@ -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
|
|
||||||
}
|
|
366
utils/common.js
366
utils/common.js
@ -1,236 +1,15 @@
|
|||||||
import axios from "axios";
|
import { exec } from 'child_process';
|
||||||
import { exec } from "child_process";
|
import fetch from 'node-fetch';
|
||||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
import os from 'os';
|
||||||
import fetch from "node-fetch";
|
import { SHORT_LINKS, TEN_THOUSAND } from '../constants/constant.js';
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 千位数的数据处理
|
* 千位数的数据处理
|
||||||
* @param data
|
* @param data
|
||||||
* @return {string|*}
|
* @return {string|*}
|
||||||
*/
|
*/
|
||||||
const dataProcessing = data => {
|
const dataProcessing = (data) => {
|
||||||
return Number(data) >= TEN_THOUSAND ? (data / TEN_THOUSAND).toFixed(1) + "万" : data;
|
return Number(data) >= TEN_THOUSAND ? (data / TEN_THOUSAND).toFixed(1) + '万' : data;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -239,7 +18,9 @@ const dataProcessing = data => {
|
|||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
export function formatBiliInfo(data) {
|
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(' | ');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -261,33 +42,6 @@ export function secondsToTime(seconds) {
|
|||||||
return `${pad(hours, 2)}:${pad(minutes, 2)}:${pad(secs, 2)}`;
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 超过某个长度的字符串换为...
|
* 超过某个长度的字符串换为...
|
||||||
* @param inputString
|
* @param inputString
|
||||||
@ -300,56 +54,6 @@ export function truncateString(inputString, maxLength = 50) {
|
|||||||
: inputString.substring(0, maxLength) + '...';
|
: 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 请求
|
* 重试 fetch 请求
|
||||||
* @param {string} url 请求的URL
|
* @param {string} url 请求的URL
|
||||||
@ -367,8 +71,10 @@ export async function retryFetch(url, options, retries = 3, delay = 1000) {
|
|||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (retries > 0) {
|
if (retries > 0) {
|
||||||
logger.mark(`[R插件][重试模块] 请求失败: ${error.message},重试中... (${3 - retries + 1}/3) 次`);
|
logger.mark(
|
||||||
await new Promise(resolve => setTimeout(resolve, delay));
|
`[R插件][重试模块] 请求失败: ${error.message},重试中... (${3 - retries + 1}/3) 次`
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
return retryFetch(url, options, retries - 1, delay);
|
return retryFetch(url, options, retries - 1, delay);
|
||||||
} else {
|
} else {
|
||||||
throw error;
|
throw error;
|
||||||
@ -400,7 +106,7 @@ export function estimateReadingTime(text, wpm = 200) {
|
|||||||
const readingTimeMinutes = wordCount / wpm;
|
const readingTimeMinutes = wordCount / wpm;
|
||||||
return {
|
return {
|
||||||
minutes: Math.ceil(readingTimeMinutes),
|
minutes: Math.ceil(readingTimeMinutes),
|
||||||
words: wordCount
|
words: wordCount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -426,42 +132,6 @@ export function checkToolInCurEnv(someCommand) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
* @param url
|
||||||
@ -469,16 +139,16 @@ export function cleanFilename(filename) {
|
|||||||
*/
|
*/
|
||||||
export async function urlTransformShortLink(url) {
|
export async function urlTransformShortLink(url) {
|
||||||
const data = {
|
const data = {
|
||||||
url: `${ encodeURI(url) }`
|
url: `${encodeURI(url)}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
const resp = await fetch(SHORT_LINKS, {
|
const resp = await fetch(SHORT_LINKS, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Accept': 'application/json',
|
Accept: 'application/json',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data),
|
||||||
}).then(response => response.json());
|
}).then((response) => response.json());
|
||||||
return await resp.data.short_url;
|
return await resp.data.short_url;
|
||||||
}
|
}
|
||||||
|
@ -1,33 +1,6 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
import fs from "fs";
|
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用 ffmpeg 将 FLV 文件转换为 MP4 文件
|
* 使用 ffmpeg 将 FLV 文件转换为 MP4 文件
|
||||||
|
146
utils/file.js
146
utils/file.js
@ -1,5 +1,5 @@
|
|||||||
import { promises as fs } from "fs";
|
import { promises as fs } from 'fs';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
|
|
||||||
// 常量提取
|
// 常量提取
|
||||||
const mimeTypes = {
|
const mimeTypes = {
|
||||||
@ -21,20 +21,6 @@ function handleError(err) {
|
|||||||
throw err;
|
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 - 文件路径
|
* @param {string} file - 文件路径
|
||||||
@ -109,131 +95,3 @@ export async function readCurrentDir(dirPath) {
|
|||||||
handleError(err);
|
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 };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user