diff --git a/apps/query.js b/apps/query.js
index 028d49e..4da9c51 100644
--- a/apps/query.js
+++ b/apps/query.js
@@ -46,10 +46,6 @@ export class query extends plugin {
reg: "^#累了$",
fnc: "cospro",
},
- {
- reg: "^#青年大学习$",
- fnc: "youthLearning",
- },
{
reg: "^#搜书(.*)$",
fnc: "searchBook",
@@ -181,105 +177,6 @@ export class query extends plugin {
return true;
}
- // 青年大学习
- async youthLearning(e) {
- await axios
- .get(
- "https://qczj.h5yunban.com/qczj-youth-learning/cgi-bin/common-api/course/current",
- {
- headers: {
- "User-Agent":
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 Edg/95.0.1020.53",
- },
- },
- )
- .then(resp => {
- // logger.info(resp.data);
- return resp.data.result.uri.replace("index.html", "m.html");
- })
- .then(async uri => {
- axios
- .get(uri, {
- headers: {
- "User-Agent":
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36 Edg/95.0.1020.53",
- },
- })
- .then(resp => {
- const content = resp.data;
- const resList = content.match(/
<\/div>/g);
- const valueList = resList.map(item => {
- return item.match(/data-a="(\d+)"/)[1];
- });
- let result = [];
- // 转换ABCD
- const digitToLetter = {
- 0: "A",
- 1: "B",
- 2: "C",
- 3: "D",
- };
- for (let i = 0; i < valueList.length; i += 4) {
- const group = valueList.slice(i, i + 4);
- if (group.length < 4) {
- continue;
- }
-
- const letters = group
- .map((d, indx) => {
- if (d === "1") {
- return digitToLetter[indx];
- }
- })
- .join("");
- result.push(letters);
- }
- // 封装答案
- let ans = "";
- for (let i = 0; i < result.length; i++) {
- ans += `${ i + 1 }. ${ result[i] }\n`;
- }
- e.reply(ans);
- const imgMatch = uri.match(/[^\/]+/g);
- const imgId = imgMatch[imgMatch.length - 2];
-
- axios
- .get(`https://h5.cyol.com/special/daxuexi/${ imgId }/images/end.jpg`, {
- 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(resp => {
- const filePath = "./youthLearning.png";
- const writer = fs.createWriteStream(filePath);
- resp.data.pipe(writer);
- return new Promise((resolve, reject) => {
- writer.on("finish", () => {
- writer.close(() => {
- resolve(filePath);
- });
- });
- writer.on("error", err => {
- fs.unlink(filePath, () => {
- reject(err);
- });
- });
- });
- })
- .then(filePath => {
- e.reply(segment.image(fs.readFileSync(filePath)));
- fs.unlinkSync(filePath, err => {
- if (err) throw err;
- logger.error("删除青年大学习文件失败");
- });
- });
- });
- });
- return true;
- }
-
async cospro(e) {
let [res1, res2] = (
await Promise.allSettled([
diff --git a/apps/tools.js b/apps/tools.js
index d5ad2c8..ab8e6ac 100644
--- a/apps/tools.js
+++ b/apps/tools.js
@@ -186,6 +186,8 @@ export class tools extends plugin {
});
// 并发队列
this.queue = new PQueue({concurrency: Number(this.toolsConfig.queueConcurrency)});
+ // 视频下载的并发数量
+ this.videoDownloadConcurrency = this.toolsConfig.videoDownloadConcurrency;
}
// 翻译插件
@@ -260,7 +262,7 @@ export class tools extends plugin {
);
const path = `${ this.getCurDownloadPath(e) }/temp.mp4`;
await this.downloadVideo(resUrl).then(() => {
- this.video_file(e, path)
+ this.sendVideoToUpload(e, path)
});
} else if (urlType === "image") {
// 无水印图片列表
@@ -464,7 +466,7 @@ export class tools extends plugin {
.then(data => {
this.downBili(`${ path }temp`, data.videoUrl, data.audioUrl)
.then(_ => {
- this.video_file(e, `${ path }temp.mp4`)
+ this.sendVideoToUpload(e, `${ path }temp.mp4`)
})
.catch(err => {
logger.error(err);
@@ -754,7 +756,7 @@ export class tools extends plugin {
parseM3u8(res.urlM3u8s[res.urlM3u8s.length - 1]).then(res2 => {
downloadM3u8Videos(res2.m3u8FullUrls, path).then(_ => {
mergeAcFileToMp4(res2.tsNames, path, `${ path }out.mp4`).then(_ => {
- this.video_file(e, `${ path }out.mp4`)
+ this.sendVideoToUpload(e, `${ path }out.mp4`)
});
});
});
@@ -814,7 +816,7 @@ export class tools extends plugin {
// 创建文件,如果不存在
path = `${ this.getCurDownloadPath(e) }/`;
}
- this.video_file(e, `${ path }/temp.mp4`)
+ this.sendVideoToUpload(e, `${ path }/temp.mp4`)
});
return true;
} else if (type === "normal") {
@@ -1093,7 +1095,7 @@ export class tools extends plugin {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"referer": "https://weibo.com/",
}).then(path => {
- this.video_file(e, `${ path }/temp.mp4`)
+ this.sendVideoToUpload(e, `${ path }/temp.mp4`)
});
} catch (err) {
e.reply("视频资源获取失败");
@@ -1127,7 +1129,7 @@ export class tools extends plugin {
// 视频:https://www.kuaishou.com/short-video/3xhjgcmir24m4nm
const url = adapter.video;
this.downloadVideo(url).then(path => {
- this.video_file(e, `${ path }/temp.mp4`)
+ this.sendVideoToUpload(e, `${ path }/temp.mp4`)
});
} else {
e.reply("解析失败:无法获取到资源");
@@ -1262,7 +1264,7 @@ export class tools extends plugin {
// 暂时选取分辨率较低的video进行解析
const videoUrl = resolutions[i].url;
this.downloadVideo(videoUrl).then(path => {
- this.video_file(e, `${ path }/temp.mp4`)
+ this.sendVideoToUpload(e, `${ path }/temp.mp4`)
});
break;
}
@@ -1377,7 +1379,7 @@ export class tools extends plugin {
e.reply([segment.image(cover), `识别:微视,${ title }`]);
this.downloadVideo(noWatermarkDownloadUrl).then(path => {
- this.video_file(e, `${ path }/temp.mp4`)
+ this.sendVideoToUpload(e, `${ path }/temp.mp4`)
});
} catch (err) {
logger.error(err);
@@ -1441,7 +1443,7 @@ export class tools extends plugin {
}
if (shortVideoInfo.noWatermarkDownloadUrl) {
this.downloadVideo(shortVideoInfo.noWatermarkDownloadUrl).then(path => {
- this.video_file(e, `${ path }/temp.mp4`)
+ this.sendVideoToUpload(e, `${ path }/temp.mp4`)
});
}
} catch (error) {
@@ -1529,46 +1531,113 @@ export class tools extends plugin {
}
/**
- * 工具:根URL据下载视频 / 音频
- * @param url 下载地址
- * @param isProxy 是否需要魔法
- * @param headers 覆盖头节点
- * @returns {Promise}
+ * 工具:根据URL多线程下载视频 / 音频
+ * @param url
+ * @param isProxy
+ * @param headers
+ * @param numThreads
+ * @returns {Promise}
*/
- async downloadVideo(url, isProxy = false, headers = null) {
+ async downloadVideo(url, isProxy = false, headers = null, numThreads = 1) {
const { groupPath, target } = this.getGroupPathAndTarget.call(this);
-
await mkdirIfNotExists(groupPath);
+ const userAgent = "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";
+ // 用户设置优先策略,逻辑解释:如果使用了这个函数优先查看用户是否设置了大于1的线程,如果设置了优先使用,没设置就开发者设定的函数设置
+ numThreads = this.videoDownloadConcurrency !== 1 ? this.videoDownloadConcurrency : numThreads;
- const userAgent =
- "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 axiosConfig = {
- headers: headers || { "User-Agent": userAgent },
- responseType: "stream",
- ...(isProxy && {
- httpAgent: tunnel.httpOverHttp({
- proxy: { host: this.proxyAddr, port: this.proxyPort },
+ // 如果是用户设置了单线程,则不分片下载
+ if (numThreads === 1) {
+ const axiosConfig = {
+ headers: headers || { "User-Agent": userAgent },
+ responseType: "stream",
+ ...(isProxy && {
+ httpAgent: tunnel.httpOverHttp({
+ proxy: { host: this.proxyAddr, port: this.proxyPort },
+ }),
+ httpsAgent: tunnel.httpsOverHttp({
+ proxy: { host: this.proxyAddr, port: this.proxyPort },
+ }),
}),
- httpsAgent: tunnel.httpsOverHttp({
- proxy: { host: this.proxyAddr, port: this.proxyPort },
- }),
- }),
- };
+ };
- try {
- await checkAndRemoveFile(target);
+ try {
+ await checkAndRemoveFile(target);
- const res = await axios.get(url, axiosConfig);
- logger.mark(`开始下载: ${ url }`);
- const writer = fs.createWriteStream(target);
- res.data.pipe(writer);
+ const res = await axios.get(url, axiosConfig);
+ logger.mark(`开始下载: ${ url }`);
+ const writer = fs.createWriteStream(target);
+ res.data.pipe(writer);
- return new Promise((resolve, reject) => {
- writer.on("finish", () => resolve(groupPath));
- writer.on("error", reject);
- });
- } catch (err) {
- logger.error(`下载视频发生错误!\ninfo:${ err }`);
+ return new Promise((resolve, reject) => {
+ writer.on("finish", () => resolve(groupPath));
+ writer.on("error", reject);
+ });
+ } catch (err) {
+ logger.error(`下载视频发生错误!\ninfo:${ err }`);
+ }
+ } else {
+ // 多线程分片下载
+ try {
+ await checkAndRemoveFile(target);
+ const sizeRes = await axios.head(url);
+ const contentLength = sizeRes.headers['content-length'];
+ const chunkSize = Math.ceil(contentLength / numThreads);
+ let promises = [];
+
+ for (let i = 0; i < numThreads; i++) {
+ let start = i * chunkSize;
+ let end = (i + 1) * chunkSize - 1;
+ if (i === numThreads - 1) end = ''; // Last chunk goes till the end
+ const axiosConfig = {
+ headers: {
+ "User-Agent": userAgent,
+ "Range": `bytes=${start}-${end}`,
+ ...headers
+ },
+ responseType: 'stream',
+ ...(isProxy && {
+ httpAgent: tunnel.httpOverHttp({
+ proxy: { host: 'proxyAddress', port: 'proxyPort' },
+ }),
+ httpsAgent: tunnel.httpsOverHttp({
+ proxy: { host: 'proxyAddress', port: 'proxyPort' },
+ }),
+ }),
+ };
+ promises.push(axios.get(url, axiosConfig));
+ }
+
+ const writer = fs.createWriteStream(target, { flags: 'a' });
+
+ // 同时下载所有部分
+ await Promise.all(promises.map(async (promise, index) => {
+ const res = await promise;
+ logger.mark(`开始下载部分: ${index + 1}`);
+ res.data.pipe(writer, { end: false });
+ await new Promise((resolve, reject) => {
+ res.data.on('end', () => {
+ logger.mark(`部分 ${index + 1} 下载完成`);
+ resolve();
+ });
+ res.data.on('error', reject);
+ });
+ }));
+
+ // 注意这里不应该在每个分块结束后立即调用writer.end()
+ // 我们只在所有分块都已经pipe完毕后调用writer.end()
+ writer.end();
+ await new Promise((resolve, reject) => {
+ writer.on('finish', () => {
+ logger.mark(`所有部分下载完成,文件已保存至 ${target}`);
+ resolve(target); // 返回目标文件路径
+ });
+ writer.on('error', reject);
+ });
+ } catch (err) {
+ logger.error(`下载视频发生错误!\ninfo:${err}`);
+ // 处理或抛出错误
+ throw err;
+ }
}
}
@@ -1629,20 +1698,21 @@ export class tools extends plugin {
}
/**
- * 上传视频文件
- * @param {*} e
- * @param {*} path
+ * 发送转上传视频
+ * @param e 交互事件
+ * @param path 视频所在路径
+ * @param videoSizeLimit 发送转上传视频的大小限制,默认70MB
*/
- async video_file(e, path) {
- if(!fs.existsSync(path)) return e.reply('视频不存在')
- const stats = fs.statSync(path)
- const Video_size = (stats.size / (1024 * 1024)).toFixed(2)
- if (Video_size > 70) {
- e.reply(`当前视频大小:${ Video_size }MB,\n大于设置的最大限制,\n改为上传群文件`)
- if(this.e.bot?.sendUni) {
- this.e.group.fs.upload(path)
+ async sendVideoToUpload(e, path, videoSizeLimit = 70) {
+ if (!fs.existsSync(path)) return e.reply('视频不存在');
+ const stats = fs.statSync(path);
+ const videoSize = (stats.size / (1024 * 1024)).toFixed(2);
+ if (videoSize > videoSizeLimit) {
+ e.reply(`当前视频大小:${ videoSize }MB,\n大于设置的最大限制,\n改为上传群文件`);
+ if (this.e.bot?.sendUni) {
+ this.e.group.fs.upload(path);
} else {
- this.e.group.sendFile(path)
+ this.e.group.sendFile(path);
}
} else {
e.reply(segment.video(path));
diff --git a/config/tools.yaml b/config/tools.yaml
index 97ce3f0..1d7c3c2 100644
--- a/config/tools.yaml
+++ b/config/tools.yaml
@@ -11,4 +11,6 @@ biliDuration: 480 # 哔哩哔哩限制的最大视频时长(默认8分钟)
douyinCookie: '' # douyin's cookie, 格式:odin_tt=xxx;sessionid_ss=xxx;ttwid=xxx;passport_csrf_token=xxx;msToken=xxx;
-queueConcurrency: 1 # 【目前只涉及哔哩哔哩的下载】根据服务器性能设置可以并发下载的个数,如果你的服务器比较强劲,就选择4~12,较弱就一个一个下载,选择1
\ No newline at end of file
+queueConcurrency: 1 # 【目前只涉及哔哩哔哩的下载】根据服务器性能设置可以并发下载的个数,如果你的服务器比较强劲,就选择4~12,较弱就一个一个下载,选择1
+
+videoDownloadConcurrency: 1 # 下载视频是否使用多线程,如果不使用默认是1,如果使用根据服务器进行选择,如果不确定是否可以用4即可,高性能服务器随意4~12都可以,看CPU的实力
\ No newline at end of file
diff --git a/guoba.support.js b/guoba.support.js
index d95ee85..b6267be 100644
--- a/guoba.support.js
+++ b/guoba.support.js
@@ -121,15 +121,26 @@ export function supportGuoba() {
},
{
field: "tools.queueConcurrency",
- label: "并发下载个数",
+ label: "允许多用户下载个数",
bottomHelpMessage:
- "【目前只涉及哔哩哔哩的下载】根据服务器性能设置可以并发下载的个数,如果你的服务器比较强劲,就选择4~12,较弱就一个一个下载,选择1",
+ "【目前只涉及哔哩哔哩的下载功能】根据服务器性能设置可以并发下载的个数,如果你的服务器比较强劲,就选择4~12,较弱就一个一个下载,选择1",
component: "Input",
required: false,
componentProps: {
placeholder: "如果你的服务器比较强劲,就写4~12(比如4,就是可以4个人同时下载),较弱就一个一个下载,写1",
},
},
+ {
+ field: "tools.videoDownloadConcurrency",
+ label: "使用下载视频的并发个数",
+ bottomHelpMessage:
+ "与【允许多用户下载个数】不同,这个功能影响下载速度。默认是1,使用根据服务器性能进行选择,如果不确定是否可以用1即可,高性能服务器随意4~12都可以,看CPU的实力",
+ component: "Input",
+ required: false,
+ componentProps: {
+ placeholder: "不确定用1即可,高性能服务器随意4~12都可以,看CPU的实力",
+ },
+ },
],
getConfigData() {
const toolsData = {