diff --git a/apps/RCtools.js b/apps/RCtools.js index f96a0a4..f9de848 100644 --- a/apps/RCtools.js +++ b/apps/RCtools.js @@ -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 { diff --git a/utils/acfun.js b/utils/acfun.js deleted file mode 100644 index f71d5e8..0000000 --- a/utils/acfun.js +++ /dev/null @@ -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(""); - 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 }; diff --git a/utils/bodian.js b/utils/bodian.js deleted file mode 100644 index 686bb14..0000000 --- a/utils/bodian.js +++ /dev/null @@ -1,94 +0,0 @@ -import axios from "axios"; -import { downloadAudio, generateRandomStr } from "./common.js"; - -/** - * 获取音频 - * @param id - * @param path - * @param songName - * @returns {Promise} - */ -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>} - */ -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} - */ -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 -} diff --git a/utils/common.js b/utils/common.js index 146e56f..89fae80 100644 --- a/utils/common.js +++ b/utils/common.js @@ -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} - */ -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} - */ -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} - */ -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} - */ -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(' | '); } /** @@ -248,44 +29,17 @@ export function formatBiliInfo(data) { * @return {string} */ export function secondsToTime(seconds) { - const pad = (num, size) => num.toString().padStart(size, '0'); + const pad = (num, size) => num.toString().padStart(size, '0'); - let hours = Math.floor(seconds / 3600); - let minutes = Math.floor((seconds % 3600) / 60); - let secs = seconds % 60; + let hours = Math.floor(seconds / 3600); + let minutes = Math.floor((seconds % 3600) / 60); + let secs = seconds % 60; - // 如果你只需要分钟和秒钟,你可以返回下面这行: - // return `${pad(minutes, 2)}:${pad(secs, 2)}`; + // 如果你只需要分钟和秒钟,你可以返回下面这行: + // 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); + // 完整的 HH:MM:SS 格式 + return `${pad(hours, 2)}:${pad(minutes, 2)}:${pad(secs, 2)}`; } /** @@ -295,59 +49,9 @@ export function containsChineseOrPunctuation(str) { * @returns {*|string} */ export function truncateString(inputString, maxLength = 50) { - return maxLength === 0 || maxLength === -1 || inputString.length <= maxLength - ? inputString - : inputString.substring(0, maxLength) + '...'; -} - -/** - * 测试当前是否存在🪜 - * @returns {Promise} - */ -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; - } - } + return maxLength === 0 || maxLength === -1 || inputString.length <= maxLength + ? inputString + : inputString.substring(0, maxLength) + '...'; } /** @@ -359,21 +63,23 @@ export async function retryAxiosReq(requestFunction, retries = 3, delay = 1000) * @returns {Promise} */ export async function retryFetch(url, options, retries = 3, delay = 1000) { - try { - const response = await fetch(url, options); - if (!response.ok) { - throw new Error(`请求失败,状态码: ${response.status}`); - } - return response; - } catch (error) { - if (retries > 0) { - 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; - } + try { + const response = await fetch(url, options); + if (!response.ok) { + throw new Error(`请求失败,状态码: ${response.status}`); } + return response; + } catch (error) { + if (retries > 0) { + 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; + } + } } /** @@ -383,9 +89,9 @@ export async function retryFetch(url, options, retries = 3, delay = 1000) { * @return {number} The number of words in the text */ export function countChineseCharacters(text) { - const chineseCharacterRegex = /[\u4e00-\u9fa5]/g; - const matches = text.match(chineseCharacterRegex); - return matches ? matches.length : 0; + const chineseCharacterRegex = /[\u4e00-\u9fa5]/g; + const matches = text.match(chineseCharacterRegex); + return matches ? matches.length : 0; } /** @@ -396,12 +102,12 @@ export function countChineseCharacters(text) { * @return {Object} An object containing the estimated reading time in minutes and the word count. */ export function estimateReadingTime(text, wpm = 200) { - const wordCount = countChineseCharacters(text); - const readingTimeMinutes = wordCount / wpm; - return { - minutes: Math.ceil(readingTimeMinutes), - words: wordCount - }; + const wordCount = countChineseCharacters(text); + const readingTimeMinutes = wordCount / wpm; + return { + minutes: Math.ceil(readingTimeMinutes), + words: wordCount, + }; } /** @@ -410,56 +116,20 @@ export function estimateReadingTime(text, wpm = 200) { * @returns {Promise} */ export function checkToolInCurEnv(someCommand) { - // 根据操作系统选择命令 - return new Promise((resolve, reject) => { - const command = os.platform() === 'win32' ? `where ${ someCommand }` : `which ${ someCommand }`; + // 根据操作系统选择命令 + return new Promise((resolve, reject) => { + const command = os.platform() === 'win32' ? `where ${someCommand}` : `which ${someCommand}`; - exec(command, (error, stdout, stderr) => { - if (error) { - logger.error(`[R插件][命令环境检测]未找到${ someCommand }: ${ stderr || error.message }`); - resolve(false); - return; - } - logger.info(`[R插件][命令环境检测]找到${ someCommand }: ${ stdout.trim() }`); - resolve(true); - }); + exec(command, (error, stdout, stderr) => { + if (error) { + logger.error(`[R插件][命令环境检测]未找到${someCommand}: ${stderr || error.message}`); + resolve(false); + return; + } + 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; + }); } /** @@ -468,17 +138,17 @@ export function cleanFilename(filename) { * @returns {Promise} */ export async function urlTransformShortLink(url) { - const data = { - url: `${ encodeURI(url) }` - }; + const data = { + url: `${encodeURI(url)}`, + }; - const resp = await fetch(SHORT_LINKS, { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data) - }).then(response => response.json()); - return await resp.data.short_url; + const resp = await fetch(SHORT_LINKS, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }).then((response) => response.json()); + return await resp.data.short_url; } diff --git a/utils/ffmpeg-util.js b/utils/ffmpeg-util.js index ff5f5d4..fd800dc 100644 --- a/utils/ffmpeg-util.js +++ b/utils/ffmpeg-util.js @@ -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} - */ -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 文件 @@ -36,28 +9,28 @@ export async function extractKeyframes(inputFilePath, outputFolderPath, frameCou * @returns {Promise} - 返回一个 Promise,成功时返回输出文件路径,失败时返回错误信息 */ export function convertFlvToMp4(inputFilePath, outputFilePath) { - return new Promise((resolve, reject) => { - const resolvedInputPath = path.resolve(inputFilePath); - const resolvedOutputPath = path.resolve(outputFilePath); + return new Promise((resolve, reject) => { + const resolvedInputPath = path.resolve(inputFilePath); + const resolvedOutputPath = path.resolve(outputFilePath); - // 检查文件是否存在 - fs.access(resolvedInputPath, fs.constants.F_OK, (err) => { - if (err) { - reject(`[R插件][ffmpeg工具]输入文件不存在: ${resolvedInputPath}`); - return; - } + // 检查文件是否存在 + fs.access(resolvedInputPath, fs.constants.F_OK, (err) => { + if (err) { + reject(`[R插件][ffmpeg工具]输入文件不存在: ${resolvedInputPath}`); + return; + } - const command = `ffmpeg -y -i "${resolvedInputPath}" "${resolvedOutputPath}"`; - logger.info(`[R插件][ffmpeg工具]执行命令:${command}`); + const command = `ffmpeg -y -i "${resolvedInputPath}" "${resolvedOutputPath}"`; + logger.info(`[R插件][ffmpeg工具]执行命令:${command}`); - // 执行 ffmpeg 转换 - exec(command, (error, stdout, stderr) => { - if (error) { - reject(`[R插件][ffmpeg工具]执行 ffmpeg 命令时出错: ${error.message}`); - return; - } - resolve(resolvedOutputPath); - }); - }); + // 执行 ffmpeg 转换 + exec(command, (error, stdout, stderr) => { + if (error) { + reject(`[R插件][ffmpeg工具]执行 ffmpeg 命令时出错: ${error.message}`); + return; + } + resolve(resolvedOutputPath); + }); }); + }); } diff --git a/utils/file.js b/utils/file.js index 14d7e69..5f73b59 100644 --- a/utils/file.js +++ b/utils/file.js @@ -1,15 +1,15 @@ -import { promises as fs } from "fs"; -import path from "path"; +import { promises as fs } from 'fs'; +import path from 'path'; // 常量提取 const mimeTypes = { - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.png': 'image/png', - '.gif': 'image/gif', - '.pdf': 'application/pdf', - '.txt': 'text/plain', - // 添加其他文件类型和MIME类型的映射 + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png': 'image/png', + '.gif': 'image/gif', + '.pdf': 'application/pdf', + '.txt': 'text/plain', + // 添加其他文件类型和MIME类型的映射 }; /** @@ -17,22 +17,8 @@ const mimeTypes = { * @param err */ function handleError(err) { - logger.error(`错误: ${ err.message }\n堆栈: ${ err.stack }`); - throw err; -} - -/** - * 异步的方式检查文件是否存在 - * @param filePath - * @returns {Promise} - */ -export async function checkFileExists(filePath) { - try { - await fs.access(filePath); - return true; // 文件存在 - } catch (error) { - return false; // 文件不存在 - } + logger.error(`错误: ${err.message}\n堆栈: ${err.stack}`); + throw err; } /** @@ -41,15 +27,15 @@ export async function checkFileExists(filePath) { * @returns {Promise} */ export async function checkAndRemoveFile(file) { - try { - await fs.access(file); - await fs.unlink(file); - logger.info(`文件 ${ file } 删除成功。`); - } catch (err) { - if (err.code !== 'ENOENT') { - handleError(err); - } + try { + await fs.access(file); + await fs.unlink(file); + logger.info(`文件 ${file} 删除成功。`); + } catch (err) { + if (err.code !== 'ENOENT') { + handleError(err); } + } } /** @@ -58,16 +44,16 @@ export async function checkAndRemoveFile(file) { * @returns {Promise} */ export async function mkdirIfNotExists(dir) { - try { - await fs.access(dir); - } catch (err) { - if (err.code === 'ENOENT') { - await fs.mkdir(dir, { recursive: true }); - logger.info(`目录 ${ dir } 创建成功。`); - } else { - handleError(err); - } + try { + await fs.access(dir); + } catch (err) { + if (err.code === 'ENOENT') { + await fs.mkdir(dir, { recursive: true }); + logger.info(`目录 ${dir} 创建成功。`); + } else { + handleError(err); } + } } /** @@ -76,25 +62,25 @@ export async function mkdirIfNotExists(dir) { * @returns {Promise} */ export async function deleteFolderRecursive(folderPath) { - try { - const files = await readCurrentDir(folderPath); - const actions = files.map(async (file) => { - const curPath = path.join(folderPath, file); - const stat = await fs.lstat(curPath); - if (stat.isDirectory()) { - return deleteFolderRecursive(curPath); - } else { - return fs.unlink(curPath); - } - }); + try { + const files = await readCurrentDir(folderPath); + const actions = files.map(async (file) => { + const curPath = path.join(folderPath, file); + const stat = await fs.lstat(curPath); + if (stat.isDirectory()) { + return deleteFolderRecursive(curPath); + } else { + return fs.unlink(curPath); + } + }); - await Promise.allSettled(actions); - logger.info(`文件夹 ${ folderPath } 中的所有文件删除成功。`); - return files.length; - } catch (error) { - handleError(error); - return 0; - } + await Promise.allSettled(actions); + logger.info(`文件夹 ${folderPath} 中的所有文件删除成功。`); + return files.length; + } catch (error) { + handleError(error); + return 0; + } } /** @@ -103,137 +89,9 @@ export async function deleteFolderRecursive(folderPath) { * @returns {Promise} 返回一个包含文件名的数组 */ export async function readCurrentDir(dirPath) { - try { - return await fs.readdir(dirPath); - } catch (err) { - handleError(err); - } -} - -/** - * 拷贝文件 - * @param {string} srcDir - 源文件目录 - * @param {string} destDir - 目标文件目录 - * @param {string[]} [specificFiles=[]] - 过滤文件,不填写就拷贝全部 - * @returns {Promise} 拷贝的文件列表 - */ -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} 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} 包含图片和视频文件名的对象 - */ -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} 返回解析后的文件信息数组,每个对象包含: - * - 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 }; - }); + try { + return await fs.readdir(dirPath); + } catch (err) { + handleError(err); + } }