mirror of
https://github.com/Jerryplusy/rc-plugin.git
synced 2025-10-13 23:59:19 +00:00
💩
This commit is contained in:
parent
55d4e17598
commit
ed5b216eac
@ -1,17 +1,6 @@
|
||||
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类型的映射
|
||||
};
|
||||
|
||||
/**
|
||||
* 通用错误处理函数
|
||||
* @param err
|
||||
|
@ -1,214 +1,218 @@
|
||||
import {
|
||||
GENERAL_REQ_LINK,
|
||||
GENERAL_REQ_LINK_2, GENERAL_REQ_LINK_3
|
||||
} from "../constants/tools.js";
|
||||
import { GENERAL_REQ_LINK, GENERAL_REQ_LINK_3 } from '../constants/tools.js';
|
||||
|
||||
/**
|
||||
* 第三方接口适配器,用于大面积覆盖解析视频的内容
|
||||
*/
|
||||
class GeneralLinkAdapter {
|
||||
constructor() {}
|
||||
|
||||
constructor() {
|
||||
/**
|
||||
* 暂时用这个来处理短链接
|
||||
* @param url
|
||||
* @param includeRedirect
|
||||
* @returns {Promise<string|Response>}
|
||||
*/
|
||||
async fetchUrl(url, includeRedirect = false) {
|
||||
let response = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
|
||||
},
|
||||
});
|
||||
return includeRedirect ? response.url : response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 【辅助函数】创造一个第三方接口的链接
|
||||
* @param externalInterface 第三方接口:这个链接来自常量 constants/tools.js @GENERAL_REQ_LINK / ...
|
||||
* @param requestURL 请求的链接
|
||||
* @returns {*}
|
||||
*/
|
||||
createReqLink(externalInterface, requestURL) {
|
||||
// 这里必须使用{ ...GENERAL_REQ_LINK_2 }赋值,不然就是对象的引用赋值,会造成全局数据问题!
|
||||
let reqLink = { ...externalInterface };
|
||||
reqLink.link = reqLink.link.replace('{}', requestURL);
|
||||
return reqLink;
|
||||
}
|
||||
|
||||
async ks(link) {
|
||||
// 例子:https://www.kuaishou.com/short-video/3xkfs8p4pnd67p4?authorId=3xkznsztpwetngu&streamSource=find&area=homexxbrilliant
|
||||
// https://v.m.chenzhongtech.com/fw/photo/3xburnkmj3auazc
|
||||
// https://v.kuaishou.com/1ff8QP
|
||||
let msg =
|
||||
/(?:https?:\/\/)?(www|v)\.(kuaishou|m\.chenzhongtech)\.com\/[A-Za-z\d._?%&+\-=\/#]*/g.exec(
|
||||
link
|
||||
)[0];
|
||||
// 跳转短号
|
||||
if (msg.includes('v.kuaishou')) {
|
||||
msg = await this.fetchUrl(msg, true);
|
||||
}
|
||||
let video_id;
|
||||
if (msg.includes('/fw/photo/')) {
|
||||
video_id = msg.match(/\/fw\/photo\/([^/?]+)/)[1];
|
||||
} else if (msg.includes('short-video')) {
|
||||
video_id = msg.match(/short-video\/([^/?]+)/)[1];
|
||||
} else {
|
||||
throw Error('无法提取快手的信息,请重试或者换一个视频!');
|
||||
}
|
||||
const reqLink = this.createReqLink(
|
||||
GENERAL_REQ_LINK,
|
||||
`https://www.kuaishou.com/short-video/${video_id}`
|
||||
);
|
||||
// 提取视频
|
||||
return {
|
||||
name: '快手',
|
||||
reqLink,
|
||||
};
|
||||
}
|
||||
|
||||
async xigua(link) {
|
||||
// 1. https://v.ixigua.com/ienrQ5bR/
|
||||
// 2. https://www.ixigua.com/7270448082586698281
|
||||
// 3. https://m.ixigua.com/video/7270448082586698281
|
||||
let msg = /(?:https?:\/\/)?(www|v|m)\.ixigua\.com\/[A-Za-z\d._?%&+\-=\/#]*/g.exec(link)[0];
|
||||
// 跳转短号
|
||||
if (msg.includes('v.ixigua')) {
|
||||
msg = await this.fetchUrl(msg, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂时用这个来处理短链接
|
||||
* @param url
|
||||
* @param includeRedirect
|
||||
* @returns {Promise<string|Response>}
|
||||
*/
|
||||
async fetchUrl(url, includeRedirect = false) {
|
||||
let response = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
|
||||
}
|
||||
});
|
||||
return includeRedirect ? response.url : response;
|
||||
}
|
||||
const id = /ixigua\.com\/(\d+)/.exec(msg)[1] || /\/video\/(\d+)/.exec(msg)[1];
|
||||
const videoReq = `https://www.ixigua.com/${id}`;
|
||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK, videoReq);
|
||||
return { name: '西瓜', reqLink };
|
||||
}
|
||||
|
||||
/**
|
||||
* 【辅助函数】创造一个第三方接口的链接
|
||||
* @param externalInterface 第三方接口:这个链接来自常量 constants/tools.js @GENERAL_REQ_LINK / ...
|
||||
* @param requestURL 请求的链接
|
||||
* @returns {*}
|
||||
*/
|
||||
createReqLink(externalInterface, requestURL) {
|
||||
// 这里必须使用{ ...GENERAL_REQ_LINK_2 }赋值,不然就是对象的引用赋值,会造成全局数据问题!
|
||||
let reqLink = { ...externalInterface };
|
||||
reqLink.link = reqLink.link.replace("{}", requestURL);
|
||||
return reqLink;
|
||||
}
|
||||
async pipixia(link) {
|
||||
const msg = /https:\/\/h5\.pipix\.com\/(s|item)\/[A-Za-z0-9]+/.exec(link)?.[0];
|
||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
|
||||
return { name: '皮皮虾', reqLink };
|
||||
}
|
||||
|
||||
async ks(link) {
|
||||
// 例子:https://www.kuaishou.com/short-video/3xkfs8p4pnd67p4?authorId=3xkznsztpwetngu&streamSource=find&area=homexxbrilliant
|
||||
// https://v.m.chenzhongtech.com/fw/photo/3xburnkmj3auazc
|
||||
// https://v.kuaishou.com/1ff8QP
|
||||
let msg = /(?:https?:\/\/)?(www|v)\.(kuaishou|m\.chenzhongtech)\.com\/[A-Za-z\d._?%&+\-=\/#]*/g.exec(link)[0];
|
||||
// 跳转短号
|
||||
if (msg.includes("v.kuaishou")) {
|
||||
msg = await this.fetchUrl(msg, true);
|
||||
}
|
||||
let video_id;
|
||||
if (msg.includes('/fw/photo/')) {
|
||||
video_id = msg.match(/\/fw\/photo\/([^/?]+)/)[1];
|
||||
} else if (msg.includes("short-video")) {
|
||||
video_id = msg.match(/short-video\/([^/?]+)/)[1];
|
||||
} else {
|
||||
throw Error("无法提取快手的信息,请重试或者换一个视频!");
|
||||
}
|
||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK, `https://www.kuaishou.com/short-video/${ video_id }`);
|
||||
// 提取视频
|
||||
async pipigx(link) {
|
||||
const msg = /https:\/\/h5\.pipigx\.com\/pp\/post\/[A-Za-z0-9]+/.exec(link)?.[0];
|
||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
|
||||
return { name: '皮皮搞笑', reqLink };
|
||||
}
|
||||
|
||||
async tieba(link) {
|
||||
const msg = /https:\/\/tieba\.baidu\.com\/p\/[A-Za-z0-9]+/.exec(link)?.[0];
|
||||
// 这里必须使用{ ...GENERAL_REQ_LINK_2 }赋值,不然就是对象的引用赋值,会造成全局数据问题!
|
||||
// !!!这里加了 '\?' 是因为 API 问题
|
||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg + '"?');
|
||||
return { name: '贴吧', reqLink };
|
||||
}
|
||||
|
||||
async qqSmallWorld(link) {
|
||||
const msg = /https:\/\/s.xsj\.qq\.com\/[A-Za-z0-9]+/.exec(link)?.[0];
|
||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
|
||||
return { name: 'QQ小世界', reqLink };
|
||||
}
|
||||
|
||||
async jike(link) {
|
||||
// https://m.okjike.com/originalPosts/6583b4421f0812cca58402a6?s=ewoidSI6ICI1YTgzMTY4ZmRmNDA2MDAwMTE5N2MwZmQiCn0=
|
||||
const msg = /https:\/\/m.okjike.com\/originalPosts\/[A-Za-z0-9]+/.exec(link)?.[0];
|
||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK_3, msg);
|
||||
return { name: '即刻', reqLink };
|
||||
}
|
||||
|
||||
async douyinBackup(link) {
|
||||
const msg = /(http:\/\/|https:\/\/)v.douyin.com\/[A-Za-z\d._?%&+\-=\/#]*/.exec(link)?.[0];
|
||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
|
||||
return { name: '抖音动图', reqLink };
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化通用适配器
|
||||
* @param link 通用链接
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async init(link) {
|
||||
logger.mark('[R插件][通用解析]', link);
|
||||
const handlers = new Map([
|
||||
[/(kuaishou.com|chenzhongtech.com)/, this.ks.bind(this)],
|
||||
[/ixigua.com/, this.xigua.bind(this)],
|
||||
[/h5.pipix.com/, this.pipixia.bind(this)],
|
||||
[/h5.pipigx.com/, this.pipigx.bind(this)],
|
||||
[/tieba.baidu.com/, this.tieba.bind(this)],
|
||||
[/xsj.qq.com/, this.qqSmallWorld.bind(this)],
|
||||
[/m.okjike.com/, this.jike.bind(this)],
|
||||
[/v.douyin.com/, this.douyinBackup.bind(this)],
|
||||
]);
|
||||
|
||||
for (let [regex, handler] of handlers) {
|
||||
if (regex.test(link)) {
|
||||
return handler(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用解析适配器,将其他的第三方接口转换为统一的接口
|
||||
* @param adapter 通用解析适配器
|
||||
* @param sign 通用解析标识:1、2 【在适配器的reqLink中】
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async resolve(adapter, sign) {
|
||||
// 发送GET请求
|
||||
return fetch(adapter.reqLink.link, {
|
||||
headers: {
|
||||
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.7',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
Pragma: 'no-cache',
|
||||
'Sec-Fetch-Dest': 'document',
|
||||
'Sec-Fetch-Mode': 'navigate',
|
||||
'Sec-Fetch-Site': 'none',
|
||||
'Sec-Fetch-User': '?1',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
|
||||
},
|
||||
timeout: 10000,
|
||||
}).then(async (resp) => {
|
||||
const data = await resp.json();
|
||||
if (sign === 1) {
|
||||
// @link GENERAL_REQ_LINK
|
||||
return {
|
||||
name: "快手",
|
||||
reqLink
|
||||
name: adapter.name,
|
||||
images: data.data?.imageUrl,
|
||||
video: data.data?.url,
|
||||
};
|
||||
}
|
||||
} else if (sign === 2) {
|
||||
// @link GENERAL_REQ_LINK_2
|
||||
return {
|
||||
name: adapter.name,
|
||||
|
||||
async xigua(link) {
|
||||
// 1. https://v.ixigua.com/ienrQ5bR/
|
||||
// 2. https://www.ixigua.com/7270448082586698281
|
||||
// 3. https://m.ixigua.com/video/7270448082586698281
|
||||
let msg = /(?:https?:\/\/)?(www|v|m)\.ixigua\.com\/[A-Za-z\d._?%&+\-=\/#]*/g.exec(link)[0];
|
||||
// 跳转短号
|
||||
if (msg.includes("v.ixigua")) {
|
||||
msg = await this.fetchUrl(msg, true);
|
||||
}
|
||||
images: data.data?.images,
|
||||
video: data.data?.videoUrl,
|
||||
desc: data.data?.desc,
|
||||
};
|
||||
} else if (sign === 3) {
|
||||
console.log(data);
|
||||
return {
|
||||
name: adapter.name,
|
||||
images: data?.images.map((item) => item.url),
|
||||
};
|
||||
} else {
|
||||
throw Error('[R插件][通用解析]错误Sign标识');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const id = /ixigua\.com\/(\d+)/.exec(msg)[1] || /\/video\/(\d+)/.exec(msg)[1];
|
||||
const videoReq = `https://www.ixigua.com/${ id }`;
|
||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK, videoReq);
|
||||
return { name: "西瓜", reqLink };
|
||||
}
|
||||
|
||||
async pipixia(link) {
|
||||
const msg = /https:\/\/h5\.pipix\.com\/(s|item)\/[A-Za-z0-9]+/.exec(link)?.[0];
|
||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
|
||||
return { name: "皮皮虾", reqLink };
|
||||
}
|
||||
|
||||
async pipigx(link) {
|
||||
const msg = /https:\/\/h5\.pipigx\.com\/pp\/post\/[A-Za-z0-9]+/.exec(link)?.[0];
|
||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
|
||||
return { name: "皮皮搞笑", reqLink };
|
||||
}
|
||||
|
||||
async tieba(link) {
|
||||
const msg = /https:\/\/tieba\.baidu\.com\/p\/[A-Za-z0-9]+/.exec(link)?.[0];
|
||||
// 这里必须使用{ ...GENERAL_REQ_LINK_2 }赋值,不然就是对象的引用赋值,会造成全局数据问题!
|
||||
// !!!这里加了 '\?' 是因为 API 问题
|
||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg + "\"?")
|
||||
return { name: "贴吧", reqLink };
|
||||
}
|
||||
|
||||
async qqSmallWorld(link) {
|
||||
const msg = /https:\/\/s.xsj\.qq\.com\/[A-Za-z0-9]+/.exec(link)?.[0];
|
||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
|
||||
return { name: "QQ小世界", reqLink };
|
||||
}
|
||||
|
||||
async jike(link) {
|
||||
// https://m.okjike.com/originalPosts/6583b4421f0812cca58402a6?s=ewoidSI6ICI1YTgzMTY4ZmRmNDA2MDAwMTE5N2MwZmQiCn0=
|
||||
const msg = /https:\/\/m.okjike.com\/originalPosts\/[A-Za-z0-9]+/.exec(link)?.[0];
|
||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK_3, msg);
|
||||
return { name: "即刻", reqLink };
|
||||
}
|
||||
|
||||
async douyinBackup(link) {
|
||||
const msg = /(http:\/\/|https:\/\/)v.douyin.com\/[A-Za-z\d._?%&+\-=\/#]*/.exec(link)?.[0];
|
||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
|
||||
return { name: "抖音动图", reqLink };
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化通用适配器
|
||||
* @param link 通用链接
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
async init(link) {
|
||||
logger.mark("[R插件][通用解析]", link)
|
||||
const handlers = new Map([
|
||||
[/(kuaishou.com|chenzhongtech.com)/, this.ks.bind(this)],
|
||||
[/ixigua.com/, this.xigua.bind(this)],
|
||||
[/h5.pipix.com/, this.pipixia.bind(this)],
|
||||
[/h5.pipigx.com/, this.pipigx.bind(this)],
|
||||
[/tieba.baidu.com/, this.tieba.bind(this)],
|
||||
[/xsj.qq.com/, this.qqSmallWorld.bind(this)],
|
||||
[/m.okjike.com/, this.jike.bind(this)],
|
||||
[/v.douyin.com/, this.douyinBackup.bind(this)],
|
||||
]);
|
||||
|
||||
for (let [regex, handler] of handlers) {
|
||||
if (regex.test(link)) {
|
||||
return handler(link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用解析适配器,将其他的第三方接口转换为统一的接口
|
||||
* @param adapter 通用解析适配器
|
||||
* @param sign 通用解析标识:1、2 【在适配器的reqLink中】
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async resolve(adapter, sign) {
|
||||
// 发送GET请求
|
||||
return fetch(adapter.reqLink.link, {
|
||||
headers: {
|
||||
'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.7',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'Pragma': 'no-cache',
|
||||
'Sec-Fetch-Dest': 'document',
|
||||
'Sec-Fetch-Mode': 'navigate',
|
||||
'Sec-Fetch-Site': 'none',
|
||||
'Sec-Fetch-User': '?1',
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
|
||||
|
||||
},
|
||||
timeout: 10000
|
||||
}).then(async resp => {
|
||||
const data = await resp.json();
|
||||
if (sign === 1) {
|
||||
// @link GENERAL_REQ_LINK
|
||||
return {
|
||||
name: adapter.name,
|
||||
images: data.data?.imageUrl,
|
||||
video: data.data?.url,
|
||||
}
|
||||
} else if (sign === 2) {
|
||||
// @link GENERAL_REQ_LINK_2
|
||||
return {
|
||||
name: adapter.name,
|
||||
images: data.data?.images,
|
||||
video: data.data?.videoUrl,
|
||||
desc: data.data?.desc
|
||||
}
|
||||
} else if (sign === 3) {
|
||||
console.log(data)
|
||||
return {
|
||||
name: adapter.name,
|
||||
images: data?.images.map(item => item.url),
|
||||
}
|
||||
} else {
|
||||
throw Error("[R插件][通用解析]错误Sign标识");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过工厂方式创建一个通用解析的JSON对象
|
||||
* @param link
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
static async create(link) {
|
||||
// 先正则匹配到函数进行出策略处理
|
||||
const adapter = await new GeneralLinkAdapter();
|
||||
const adapterHandler = await adapter.init(link);
|
||||
// 对处理完的信息进行通用解析
|
||||
return adapter.resolve(adapterHandler, adapterHandler.reqLink.sign);
|
||||
}
|
||||
/**
|
||||
* 通过工厂方式创建一个通用解析的JSON对象
|
||||
* @param link
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
static async create(link) {
|
||||
// 先正则匹配到函数进行出策略处理
|
||||
const adapter = await new GeneralLinkAdapter();
|
||||
const adapterHandler = await adapter.init(link);
|
||||
// 对处理完的信息进行通用解析
|
||||
return adapter.resolve(adapterHandler, adapterHandler.reqLink.sign);
|
||||
}
|
||||
}
|
||||
|
||||
export default GeneralLinkAdapter
|
||||
export default GeneralLinkAdapter;
|
||||
|
153
utils/kugou.js
153
utils/kugou.js
@ -1,153 +0,0 @@
|
||||
// 获取MV信息的函数
|
||||
export async function getKugouMv(msg, page_limit, count_limit, n) {
|
||||
const url = `https://mobiles.kugou.com/api/v3/search/mv?format=json&keyword=${encodeURIComponent(
|
||||
msg
|
||||
)}&page=${page_limit}&pagesize=${count_limit}&showtype=1`;
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const json = await response.json();
|
||||
const info_list = json.data.info;
|
||||
let data_list = [];
|
||||
|
||||
if (n !== "") {
|
||||
const info = info_list[n];
|
||||
const json_data2 = await getMvData(info.hash);
|
||||
const mvdata_list = json_data2.mvdata;
|
||||
|
||||
let mvdata = null;
|
||||
if ("sq" in mvdata_list) {
|
||||
mvdata = mvdata_list["sq"];
|
||||
} else if ("le" in mvdata_list) {
|
||||
mvdata = mvdata_list["le"];
|
||||
} else if ("rq" in mvdata_list) {
|
||||
mvdata = mvdata_list["rq"];
|
||||
}
|
||||
|
||||
data_list = [
|
||||
{
|
||||
name: info["filename"],
|
||||
singername: info["singername"],
|
||||
duration: new Date(info["duration"] * 1000)
|
||||
.toISOString()
|
||||
.substr(14, 5),
|
||||
file_size: `${(mvdata["filesize"] / (1024 * 1024)).toFixed(2)} MB`,
|
||||
mv_url: mvdata["downurl"],
|
||||
cover_url: info["imgurl"].replace("/{size}", ""),
|
||||
// 下面这些字段可能需要你从其他地方获取,因为它们不是直接从这个API返回的
|
||||
// "play_count": json.play_count,
|
||||
// "like_count": json.like_count,
|
||||
// "comment_count": json.comment_count,
|
||||
// "collect_count": json.collect_count,
|
||||
// "publish_date": json.publish_date
|
||||
},
|
||||
];
|
||||
} else {
|
||||
data_list = info_list.map((info) => ({
|
||||
name: info["filename"],
|
||||
singername: info["singername"],
|
||||
duration: new Date(info["duration"] * 1000).toISOString().substr(14, 5),
|
||||
cover_url: info["imgurl"].replace("/{size}", ""),
|
||||
}));
|
||||
}
|
||||
|
||||
return data_list;
|
||||
} catch (error) {
|
||||
console.error("Error fetching MV data:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 获取歌曲信息的函数
|
||||
export async function getKugouSong(msg, page_limit, count_limit, n) {
|
||||
const url = `https://mobiles.kugou.com/api/v3/search/song?format=json&keyword=${encodeURIComponent(
|
||||
msg
|
||||
)}&page=${page_limit}&pagesize=${count_limit}&showtype=1`;
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const json = await response.json();
|
||||
const info_list = json.data.info;
|
||||
// console.log(info_list)
|
||||
let data_list = [];
|
||||
|
||||
if (n !== "") {
|
||||
const info = info_list[n];
|
||||
const song_hash = info.hash;
|
||||
let song_url = "付费歌曲暂时无法获取歌曲下载链接";
|
||||
let json_data2 = {};
|
||||
if (song_hash !== "") {
|
||||
json_data2 = await getMp3Data(song_hash);
|
||||
song_url = json_data2.error ? song_url : json_data2.url;
|
||||
}
|
||||
|
||||
data_list = [
|
||||
{
|
||||
name: info.filename,
|
||||
singername: info.singername,
|
||||
duration: new Date(info.duration * 1000).toISOString().substr(14, 5),
|
||||
file_size: `${(json_data2.fileSize / (1024 * 1024)).toFixed(2)} MB`,
|
||||
song_url: song_url,
|
||||
album_img: json_data2.album_img?.replace("/{size}", ""),
|
||||
// "mv_url": await get_kugou_mv(msg, page_limit, count_limit, n) 这可能会导致递归调用,视具体情况而定
|
||||
},
|
||||
];
|
||||
} else {
|
||||
data_list = info_list.map((info) => ({
|
||||
name: info.filename,
|
||||
singername: info.singername,
|
||||
duration: new Date(info.duration * 1000).toISOString().substr(14, 5),
|
||||
hash: info.hash,
|
||||
mvhash: info.mvhash ? info.mvhash : null,
|
||||
}));
|
||||
}
|
||||
|
||||
// 发送响应
|
||||
return {
|
||||
code: 200,
|
||||
text: "解析成功",
|
||||
type: "歌曲解析",
|
||||
now: new Date().toISOString(),
|
||||
data: data_list,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error fetching song data:", error);
|
||||
return { code: 500, text: "服务器内部错误" };
|
||||
}
|
||||
}
|
||||
|
||||
// 获取MP3数据的函数
|
||||
async function getMp3Data(song_hash) {
|
||||
const url = `https://m.kugou.com/app/i/getSongInfo.php?hash=${song_hash}&cmd=playInfo`;
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/6.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/14.2 Chrome/87.0.4280.141 Mobile Safari/537.36"
|
||||
},
|
||||
redirect: 'follow',
|
||||
method: 'GET',
|
||||
});
|
||||
const json = await response.json();
|
||||
return json;
|
||||
} catch (error) {
|
||||
console.error("Error fetching MP3 data:", error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// 获取MV数据的函数
|
||||
async function getMvData(mv_hash) {
|
||||
const url = `http://m.kugou.com/app/i/mv.php?cmd=100&hash=${mv_hash}&ismp3=1&ext=mp4`;
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/6.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/14.2 Chrome/87.0.4280.141 Mobile Safari/537.36"
|
||||
},
|
||||
redirect: 'follow',
|
||||
method: 'GET',
|
||||
});
|
||||
const json = await response.json();
|
||||
return json;
|
||||
} catch (error) {
|
||||
console.error("Error fetching MV data:", error);
|
||||
return {};
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import { SUMMARY_CONTENT_ESTIMATOR_PATTERNS } from "../constants/constant.js";
|
||||
|
||||
/**
|
||||
* 内容评估器
|
||||
* @link {linkShareSummary}
|
||||
* @param link 链接
|
||||
*/
|
||||
export function contentEstimator(link) {
|
||||
for (const pattern of SUMMARY_CONTENT_ESTIMATOR_PATTERNS) {
|
||||
if (pattern.reg.test(link)) {
|
||||
return {
|
||||
name: pattern.name,
|
||||
summaryLink: pattern.reg.exec(link)?.[0]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
logger.error("[R插件][总结模块] 内容评估出错...");
|
||||
throw Error("内容评估出错...");
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
import { retryFetch } from "./common.js";
|
||||
import { PearAPI_CRAWLER, PearAPI_DEEPSEEK } from "../constants/tools.js";
|
||||
|
||||
/**
|
||||
* LLM 爬虫
|
||||
* @param summaryLink
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function llmRead(summaryLink) {
|
||||
const llmCrawler = await retryFetch(PearAPI_CRAWLER.replace("{}", summaryLink));
|
||||
return (await llmCrawler.json())?.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* DeepSeek对话
|
||||
* @param content
|
||||
* @param prompt
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function deepSeekChat(content, prompt) {
|
||||
const deepseekFreeSummary = await fetch(PearAPI_DEEPSEEK, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": prompt
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": content,
|
||||
}]
|
||||
}),
|
||||
});
|
||||
return (await deepseekFreeSummary.json())?.message;
|
||||
}
|
@ -1,118 +0,0 @@
|
||||
import axios from "axios";
|
||||
|
||||
export class OpenaiBuilder {
|
||||
constructor() {
|
||||
this.baseURL = "https://api.moonshot.cn"; // 默认模型
|
||||
this.apiKey = ""; // 默认API密钥
|
||||
this.prompt = "描述一下这个图片"; // 默认提示
|
||||
this.model = 'claude-3-haiku-20240307'
|
||||
this.provider = "kimi"; // 默认提供商
|
||||
}
|
||||
|
||||
setBaseURL(baseURL) {
|
||||
this.baseURL = baseURL;
|
||||
return this;
|
||||
}
|
||||
|
||||
setApiKey(apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
setPrompt(prompt) {
|
||||
this.prompt = prompt;
|
||||
return this;
|
||||
}
|
||||
|
||||
setModel(model) {
|
||||
this.model = model;
|
||||
return this;
|
||||
}
|
||||
|
||||
setProvider(provider) {
|
||||
this.provider = provider;
|
||||
return this;
|
||||
}
|
||||
|
||||
setPath(path) {
|
||||
this.path = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
async build() {
|
||||
// logger.info(this.baseURL, this.apiKey)
|
||||
// 创建客户端
|
||||
this.client = axios.create({
|
||||
baseURL: this.baseURL,
|
||||
timeout: 100000,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + this.apiKey
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用与OpenAI兼容的API(如Kimi/Moonshot)。
|
||||
* @param {Array<Object>} messages - 发送给模型的消息列表。
|
||||
* @param {Array<Object>} [tools=[]] - (可选) 一个描述可供模型使用的工具的数组。
|
||||
* @returns {Promise<Object>} 返回一个包含模型响应的对象。如果模型决定调用工具,则包含 'tool_calls' 字段;否则,包含 'ans' 文本响应。
|
||||
*/
|
||||
async chat(messages, tools = []) {
|
||||
if (this.provider === 'deepseek') {
|
||||
const content = messages.find(m => m.role === 'user')?.content;
|
||||
const ans = await deepSeekChat(content, this.prompt);
|
||||
return {
|
||||
"model": "deepseek",
|
||||
"ans": ans
|
||||
}
|
||||
}
|
||||
|
||||
// 准备发送给API的消息
|
||||
let requestMessages = [...messages];
|
||||
// 检查是否已存在系统提示
|
||||
const hasSystemPrompt = requestMessages.some(m => m.role === 'system');
|
||||
|
||||
// 如果没有系统提示并且builder中已设置,则添加
|
||||
if (!hasSystemPrompt && this.prompt) {
|
||||
requestMessages.unshift({
|
||||
role: 'system',
|
||||
content: this.prompt,
|
||||
});
|
||||
}
|
||||
|
||||
// 构建API请求的负载
|
||||
const payload = {
|
||||
model: this.model, // 使用在builder中设置的模型
|
||||
messages: requestMessages,
|
||||
};
|
||||
|
||||
// 如果提供了工具,将其添加到负载中,并让模型自动决定是否使用
|
||||
if (tools && tools.length > 0) {
|
||||
payload.tools = tools;
|
||||
payload.tool_choice = "auto";
|
||||
}
|
||||
|
||||
// 发送POST请求到聊天完成端点
|
||||
const completion = await this.client.post("/v1/chat/completions", payload);
|
||||
const message = completion.data.choices[0].message;
|
||||
|
||||
// 从响应中获取实际使用的模型名称
|
||||
const modelName = completion.data.model;
|
||||
|
||||
// 如果模型的响应中包含工具调用
|
||||
if (message.tool_calls) {
|
||||
return {
|
||||
"model": modelName,
|
||||
"tool_calls": message.tool_calls
|
||||
}
|
||||
}
|
||||
|
||||
// 否则,返回包含文本答案的响应
|
||||
return {
|
||||
"model": modelName,
|
||||
"ans": message.content
|
||||
}
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@
|
||||
* console.log(exists); // true or false
|
||||
*/
|
||||
export async function redisExistKey(key) {
|
||||
return redis.exists(key);
|
||||
return redis.exists(key);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -19,7 +19,7 @@ export async function redisExistKey(key) {
|
||||
* console.log(value); // { ... }
|
||||
*/
|
||||
export async function redisGetKey(key) {
|
||||
return JSON.parse(await redis.get(key));
|
||||
return JSON.parse(await redis.get(key));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -31,23 +31,5 @@ export async function redisGetKey(key) {
|
||||
* await redisSetKey('myKey', { foo: 'bar' });
|
||||
*/
|
||||
export async function redisSetKey(key, value = {}) {
|
||||
return redis.set(
|
||||
key,
|
||||
JSON.stringify(value),
|
||||
);
|
||||
return redis.set(key, JSON.stringify(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否存在这个key然后再取值,如果没有就返回null
|
||||
* @param key
|
||||
* @returns {Promise<Object|Array>}
|
||||
* @example
|
||||
* const value = await redisExistAndGetKey('myKey');
|
||||
* console.log(value); // { ... } or null
|
||||
*/
|
||||
export async function redisExistAndGetKey(key) {
|
||||
if (await redisExistKey(key)) {
|
||||
return redisGetKey(key);
|
||||
}
|
||||
return null;
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import { exec } from 'child_process';
|
||||
import path from 'path'
|
||||
|
||||
/**
|
||||
* 执行 TDL 进行下载
|
||||
* @param url
|
||||
* @param curPath
|
||||
* @param isOversea
|
||||
* @param proxyAddr
|
||||
* @param videoDownloadConcurrency
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function startTDL(url, curPath, isOversea, proxyAddr, videoDownloadConcurrency = 1) {
|
||||
return new Promise((resolve, reject) => {
|
||||
curPath = path.resolve(curPath);
|
||||
const proxyStr = isOversea ? `` : `--proxy ${ proxyAddr }`;
|
||||
const concurrencyStr = videoDownloadConcurrency > 1 ? `-t ${ videoDownloadConcurrency } -l ${ videoDownloadConcurrency }` : '';
|
||||
const command = `tdl dl -u ${ url } -d ${ curPath } ${ concurrencyStr } ${ proxyStr }`
|
||||
logger.mark(`[R插件][TDL] ${ command }`);
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`[R插件][TDL]执行出错: ${ error.message }`);
|
||||
return;
|
||||
}
|
||||
if (stderr) {
|
||||
reject(`[R插件][TDL]错误信息: ${ stderr }`);
|
||||
return;
|
||||
}
|
||||
resolve(stdout);
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存小飞机内容到小飞机的收藏
|
||||
* @param url
|
||||
* @param isOversea
|
||||
* @param proxyAddr
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
export async function saveTDL(url, isOversea, proxyAddr) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const proxyStr = isOversea ? `` : `--proxy ${ proxyAddr }`;
|
||||
const command = `tdl forward --from ${ url } ${ proxyStr }`
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(`[R插件][TDL保存]执行出错: ${ error.message }`);
|
||||
return;
|
||||
}
|
||||
if (stderr) {
|
||||
reject(`[R插件][TDL保存]错误信息: ${ stderr }`);
|
||||
return;
|
||||
}
|
||||
resolve(stdout);
|
||||
})
|
||||
})
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
export function genVerifyFp() {
|
||||
const baseStr = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
||||
const t = baseStr.length;
|
||||
let milliseconds = Date.now(); // 获取当前的时间戳(毫秒)
|
||||
let base36 = "";
|
||||
|
||||
// 将时间戳转换为base36
|
||||
while (milliseconds > 0) {
|
||||
let remainder = milliseconds % 36;
|
||||
if (remainder < 10) {
|
||||
base36 = remainder.toString() + base36;
|
||||
} else {
|
||||
base36 = String.fromCharCode('a'.charCodeAt(0) + remainder - 10) + base36;
|
||||
}
|
||||
milliseconds = Math.floor(milliseconds / 36);
|
||||
}
|
||||
|
||||
const r = base36;
|
||||
let o = new Array(36).fill("");
|
||||
o[8] = o[13] = o[18] = o[23] = "_";
|
||||
o[14] = "4";
|
||||
|
||||
// 生成随机字符
|
||||
for (let i = 0; i < 36; i++) {
|
||||
if (!o[i]) {
|
||||
let n = Math.floor(Math.random() * t);
|
||||
if (i === 19) {
|
||||
n = (3 & n) | 8;
|
||||
}
|
||||
o[i] = baseStr[n];
|
||||
}
|
||||
}
|
||||
|
||||
return "verify_" + r + "_" + o.join("");
|
||||
}
|
||||
|
@ -1,140 +0,0 @@
|
||||
import { tencentTransMap } from "../constants/constant.js";
|
||||
import fetch from "node-fetch";
|
||||
import _ from 'lodash'
|
||||
|
||||
// 定义翻译策略接口
|
||||
class TranslateStrategy {
|
||||
async translate(query, targetLanguage) {
|
||||
throw new Error("This method should be implemented by subclasses");
|
||||
}
|
||||
}
|
||||
|
||||
// 企鹅翻译策略
|
||||
class TencentTranslateStrategy extends TranslateStrategy {
|
||||
constructor(config) {
|
||||
super();
|
||||
this.config = config;
|
||||
this.url = "https://transmart.qq.com/api/imt";
|
||||
this.commonHeaders = {
|
||||
"USER-AGENT": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/111.0"
|
||||
};
|
||||
this.clientKey = "browser-firefox-111.0.0-Mac OS-d35fca23-eb48-45ba-9913-114f1177b02b-1679376552800";
|
||||
}
|
||||
|
||||
async detectLanguage(query) {
|
||||
try {
|
||||
const response = await fetch(this.url, {
|
||||
method: "POST",
|
||||
headers: this.commonHeaders,
|
||||
body: JSON.stringify({
|
||||
"header": {
|
||||
"fn": "text_analysis",
|
||||
"client_key": this.clientKey
|
||||
},
|
||||
"text": query,
|
||||
"type": "plain",
|
||||
"normalize": {
|
||||
"merge_broken_line": false
|
||||
}
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
return data.header.ret_code === 'succ' ? data.language : "en";
|
||||
} catch (error) {
|
||||
logger.error("Error detecting language:", error);
|
||||
return "en";
|
||||
}
|
||||
}
|
||||
|
||||
async translate(query, targetLanguage) {
|
||||
try {
|
||||
const sourceLanguage = await this.detectLanguage(query);
|
||||
const response = await fetch(this.url, {
|
||||
method: "POST",
|
||||
headers: this.commonHeaders,
|
||||
body: JSON.stringify({
|
||||
"header": {
|
||||
"fn": "auto_translation",
|
||||
"client_key": this.clientKey
|
||||
},
|
||||
"type": "plain",
|
||||
"model_category": "normal",
|
||||
"text_domain": "general",
|
||||
"source": {
|
||||
"lang": sourceLanguage,
|
||||
"text_list": ["", query, ""]
|
||||
},
|
||||
"target": {
|
||||
"lang": tencentTransMap[targetLanguage]
|
||||
}
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
return data.header.ret_code === 'succ' ? data.auto_translation?.[1] : "翻译失败";
|
||||
} catch (error) {
|
||||
logger.error("Error translating text:", error);
|
||||
return "翻译失败";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deepl翻译策略
|
||||
class DeeplTranslateStrategy extends TranslateStrategy {
|
||||
constructor(config) {
|
||||
super();
|
||||
this.config = config;
|
||||
this.deeplUrls = this.config.deeplApiUrls.includes(",") ? this.config.deeplApiUrls.split(",") : [this.config.deeplApiUrls];
|
||||
}
|
||||
|
||||
async translate(query, targetLanguage) {
|
||||
const url = this.deeplUrls[Math.floor(Math.random() * this.deeplUrls.length)];
|
||||
logger.info(`[R插件][Deepl翻译]:当前使用的API:${url}`);
|
||||
try {
|
||||
const source_lang = await new TencentTranslateStrategy(this.config).detectLanguage(query);
|
||||
logger.info(`[R插件][Deepl翻译]:检测到的源语言:${source_lang}`);
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...this.commonHeaders
|
||||
},
|
||||
body: JSON.stringify({
|
||||
text: query,
|
||||
source_lang,
|
||||
target_lang: tencentTransMap[targetLanguage]
|
||||
}),
|
||||
|
||||
});
|
||||
const data = await response.json();
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
logger.error("Error translating text:", error);
|
||||
return "翻译失败";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 主逻辑
|
||||
export default class Translate {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this.strategy = null;
|
||||
}
|
||||
|
||||
selectStrategy() {
|
||||
if (!_.isEmpty(this.config.deeplApiUrls)) {
|
||||
logger.info("[R插件][翻译策略]:当前选择 Deepl翻译")
|
||||
return new DeeplTranslateStrategy(this.config);
|
||||
} else {
|
||||
logger.info("[R插件][翻译策略]:当前选择 企鹅翻译")
|
||||
return new TencentTranslateStrategy(this.config);
|
||||
}
|
||||
}
|
||||
|
||||
async translate(query, targetLanguage) {
|
||||
if (!this.strategy) {
|
||||
this.strategy = this.selectStrategy();
|
||||
}
|
||||
return this.strategy.translate(query, targetLanguage);
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
// Base62 encode function in JavaScript
|
||||
const ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
const base62_encode = (number) => {
|
||||
if (number === 0) return '0';
|
||||
let result = '';
|
||||
while (number > 0) {
|
||||
result = ALPHABET[number % 62] + result;
|
||||
number = Math.floor(number / 62);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// Convert mid to id
|
||||
export const mid2id = (mid) => {
|
||||
mid = mid.toString().split('').reverse().join(''); // Reverse the input string
|
||||
const size = Math.ceil(mid.length / 7);
|
||||
let result = [];
|
||||
|
||||
for (let i = 0; i < size; i++) {
|
||||
let s = mid.slice(i * 7, (i + 1) * 7).split('').reverse().join(''); // Chunk and reverse each chunk
|
||||
s = base62_encode(parseInt(s, 10)); // Encode each chunk using base62
|
||||
if (i < size - 1 && s.length < 4) {
|
||||
// Pad with leading zeros if necessary
|
||||
s = '0'.repeat(4 - s.length) + s;
|
||||
}
|
||||
result.push(s);
|
||||
}
|
||||
|
||||
result.reverse(); // Reverse the result array to maintain order
|
||||
return result.join(''); // Join the array into a single string
|
||||
};
|
@ -1,46 +0,0 @@
|
||||
/**
|
||||
* 用于YouTube的格式化
|
||||
* @param seconds
|
||||
* @returns {string}
|
||||
*/
|
||||
export function ytbFormatTime(seconds) {
|
||||
// 计算小时、分钟和秒
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = seconds % 60;
|
||||
// 将小时、分钟和秒格式化为两位数
|
||||
const formattedHours = String(hours).padStart(2, '0');
|
||||
const formattedMinutes = String(minutes).padStart(2, '0');
|
||||
const formattedSeconds = String(secs).padStart(2, '0');
|
||||
// 构造时间范围字符串
|
||||
return `00:00:00-${formattedHours}:${formattedMinutes}:${formattedSeconds}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除链接中的不需要的参数
|
||||
* @param url
|
||||
* @returns {*}
|
||||
*/
|
||||
export function removeParams(url) {
|
||||
return url
|
||||
.replace(/&list=[^&]*/g, '')
|
||||
.replace(/&start_radio=[^&]*/g, '')
|
||||
.replace(/&index=[^&]*/g, '')
|
||||
.replace(/&si=[^&]*/g, '');
|
||||
}
|
||||
|
||||
export function convertToSeconds(timeStr) {
|
||||
const parts = timeStr.split(':').map(Number);
|
||||
if (parts.length === 2) {
|
||||
const [minutes, seconds] = parts;
|
||||
return minutes * 60 + seconds;
|
||||
} else if (parts.length === 3) {
|
||||
const [hours, minutes, seconds] = parts;
|
||||
return hours * 3600 + minutes * 60 + seconds;
|
||||
}
|
||||
return timeStr;
|
||||
}
|
||||
|
||||
export async function autoSelectMusicOrVideoSend() {
|
||||
|
||||
}
|
@ -1,194 +0,0 @@
|
||||
import { exec } from 'child_process';
|
||||
|
||||
/**
|
||||
* 构建梯子参数
|
||||
* @param isOversea
|
||||
* @param proxy
|
||||
* @returns {string|string}
|
||||
*/
|
||||
function constructProxyParam(isOversea, proxy) {
|
||||
return isOversea ? '' : `--proxy ${proxy}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造cookie参数
|
||||
* 目前只支持YouTube构造cookie,否则就必须修改`url.includes("youtu")`
|
||||
* @param url
|
||||
* @param cookiePath
|
||||
* @returns {string}
|
||||
*/
|
||||
function constructCookiePath(url, cookiePath) {
|
||||
return cookiePath !== '' && url.includes('youtu') ? `--cookies ${cookiePath}` : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* yt-dlp获取标题的时候可能需要的一个编码参数,也在一定程度上解决部分window系统乱码问题
|
||||
* @param url
|
||||
* @returns {string}
|
||||
*/
|
||||
function constructEncodingParam(url) {
|
||||
return '--encoding UTF-8'; // 始终为标题获取使用 UTF-8 编码
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取时长
|
||||
* @param url
|
||||
* @param isOversea
|
||||
* @param proxy
|
||||
* @param cookiePath
|
||||
* @returns string
|
||||
*/
|
||||
export function ytDlpGetDuration(url, isOversea, proxy, cookiePath = '') {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 构造 cookie 参数
|
||||
const cookieParam = constructCookiePath(url, cookiePath);
|
||||
const command = `yt-dlp --get-duration --skip-download ${cookieParam} ${constructProxyParam(isOversea, proxy)} "${url}"`;
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
logger.error(
|
||||
`[R插件][yt-dlp审计] Error executing ytDlpGetDuration: ${error}. Stderr: ${stderr}`
|
||||
);
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(stdout.trim());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取标题
|
||||
* @param url
|
||||
* @param isOversea
|
||||
* @param proxy
|
||||
* @param cookiePath
|
||||
* @returns string
|
||||
*/
|
||||
export async function ytDlpGetTilt(url, isOversea, proxy, cookiePath = '') {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 构造 cookie 参数
|
||||
const cookieParam = constructCookiePath(url, cookiePath);
|
||||
// 构造 编码 参数
|
||||
const encodingParam = constructEncodingParam(url);
|
||||
const command = `yt-dlp --get-title --skip-download ${cookieParam} ${constructProxyParam(isOversea, proxy)} "${url}" ${encodingParam}`;
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
logger.error(
|
||||
`[R插件][yt-dlp审计] Error executing ytDlpGetTilt: ${error}. Stderr: ${stderr}`
|
||||
);
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(stdout.trim());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取封面
|
||||
* @param path
|
||||
* @param url
|
||||
* @param isOversea
|
||||
* @param proxy
|
||||
* @param cookiePath
|
||||
* @param thumbnailFilenamePrefix 缩略图文件名前缀 (不含扩展名)
|
||||
*/
|
||||
export function ytDlpGetThumbnail(
|
||||
path,
|
||||
url,
|
||||
isOversea,
|
||||
proxy,
|
||||
cookiePath = '',
|
||||
thumbnailFilenamePrefix = 'thumbnail'
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cookieParam = constructCookiePath(url, cookiePath);
|
||||
const finalThumbnailName = thumbnailFilenamePrefix || 'thumbnail';
|
||||
const command = `yt-dlp --write-thumbnail --convert-thumbnails png --skip-download ${cookieParam} ${constructProxyParam(isOversea, proxy)} "${url}" -P "${path}" -o "${finalThumbnailName}.%(ext)s"`;
|
||||
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
logger.error(
|
||||
`[R插件][yt-dlp审计] Error executing ytDlpGetThumbnail: ${error}. Stderr: ${stderr}`
|
||||
);
|
||||
return reject(error);
|
||||
}
|
||||
// 从yt-dlp的输出中提取文件名
|
||||
const match = stdout.match(/Writing thumbnail to: (.*)/);
|
||||
if (match && match[1]) {
|
||||
const thumbnailPath = match[1].trim();
|
||||
// 只返回文件名部分
|
||||
const thumbnailFilename = thumbnailPath.split(/[\\/]/).pop();
|
||||
logger.info(`[R插件][yt-dlp审计] Thumbnail downloaded: ${thumbnailFilename}`);
|
||||
resolve(thumbnailFilename);
|
||||
} else {
|
||||
// 兜底方案:如果无法从输出中解析,则按原逻辑拼接
|
||||
logger.warn(
|
||||
'[R插件][yt-dlp审计] Could not parse thumbnail filename from stdout. Falling back to default.'
|
||||
);
|
||||
// 尝试查找文件,因为yt-dlp可能没有输出我们期望的格式
|
||||
const expectedPngPath = `${finalThumbnailName}.png`;
|
||||
resolve(expectedPngPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* yt-dlp 工具类
|
||||
* @returns {Promise<void>}
|
||||
* @param path 下载路径
|
||||
* @param url 下载链接
|
||||
* @param isOversea 是否是海外用户
|
||||
* @param proxy 代理地址
|
||||
* @param merge 是否合并输出为 mp4 格式 (仅适用于视频合并需求)
|
||||
* @param graphics YouTube画质参数
|
||||
* @param timeRange 截取时间段
|
||||
* @param maxThreads 最大并发
|
||||
* @param outputFilename 输出文件名 (不含扩展名)
|
||||
* @param cookiePath Cookie所在位置
|
||||
*/
|
||||
export async function ytDlpHelper(
|
||||
path,
|
||||
url,
|
||||
isOversea,
|
||||
proxy,
|
||||
maxThreads,
|
||||
outputFilename,
|
||||
merge = false,
|
||||
graphics,
|
||||
timeRange,
|
||||
cookiePath = ''
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let command = '';
|
||||
// 构造 cookie 参数
|
||||
const cookieParam = constructCookiePath(url, cookiePath);
|
||||
// 确保 outputFilename 不为空,提供一个默认值以防万一
|
||||
const finalOutputFilename = outputFilename || 'temp_download';
|
||||
|
||||
if (url.includes('music')) {
|
||||
// 这里是 YouTube Music的处理逻辑
|
||||
// e.g yt-dlp -x --audio-format mp3 https://youtu.be/5wEtefq9VzM -o test.mp3
|
||||
command = `yt-dlp -x --audio-format flac -f ba ${cookieParam} ${constructProxyParam(isOversea, proxy)} -P "${path}" -o "${finalOutputFilename}.flac" "${url}"`;
|
||||
} else {
|
||||
// 正常情况下的处理逻辑
|
||||
const fParam = url.includes('youtu')
|
||||
? `--download-sections "*${timeRange}" -f "bv${graphics}[ext=mp4]+ba[ext=m4a]" `
|
||||
: '';
|
||||
|
||||
command = `yt-dlp -N ${maxThreads} ${fParam} --concurrent-fragments ${maxThreads} ${cookieParam} ${constructProxyParam(isOversea, proxy)} -P "${path}" -o "${finalOutputFilename}.%(ext)s" "${url}"`;
|
||||
}
|
||||
|
||||
logger.info(`[R插件][yt-dlp审计] ${command}`);
|
||||
|
||||
exec(command, (error, stdout) => {
|
||||
if (error) {
|
||||
logger.error(`[R插件][yt-dlp审计] 执行命令时出错: ${error}`);
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(stdout);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
@ -1,169 +1,14 @@
|
||||
import os from "os";
|
||||
|
||||
/**
|
||||
* 将只有 text 类型的数组转换为原生的 {Bot.makeForwardMsg}
|
||||
* @param e
|
||||
* @param textArray {string[]}
|
||||
*/
|
||||
export function textArrayToMakeForward(e, textArray) {
|
||||
return textArray.map(item => {
|
||||
return {
|
||||
message: { type: "text", text: item },
|
||||
nickname: e.sender.card || e.user_id,
|
||||
user_id: e.user_id,
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送群组音乐卡片
|
||||
* @param e
|
||||
* @param platformType 音乐平台
|
||||
* @param musicId 音乐id
|
||||
*/
|
||||
export async function sendMusicCard(e, platformType, musicId) {
|
||||
await e.bot.sendApi('send_group_msg', {
|
||||
group_id: e.group_id,
|
||||
message: [
|
||||
{
|
||||
type: 'music',
|
||||
data: {
|
||||
type: platformType,
|
||||
id: musicId
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取群文件最新的图片
|
||||
* @param e
|
||||
* @param count 获取群聊条数
|
||||
* @returns {Promise<*|string>}
|
||||
*/
|
||||
export async function getLatestImage(e, count = 10) {
|
||||
// 获取最新的聊天记录,阈值为5
|
||||
const latestChat = await e.bot.sendApi("get_group_msg_history", {
|
||||
"group_id": e.group_id,
|
||||
"count": count
|
||||
});
|
||||
const messages = latestChat.data.messages;
|
||||
// 找到最新的图片
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages?.[i]?.message;
|
||||
if (message?.[0]?.type === "image") {
|
||||
return message?.[0].data?.url;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取群文件Url地址
|
||||
* @param e
|
||||
* @param count 获取群聊条数
|
||||
*/
|
||||
export async function getGroupFileUrl(e, count = 10) {
|
||||
const latestChat = await e.bot.sendApi("get_group_msg_history", {
|
||||
"group_id": e.group_id,
|
||||
"count": count
|
||||
});
|
||||
const messages = latestChat.data.messages;
|
||||
let file_id = "";
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages?.[i]?.message;
|
||||
if (message?.[0]?.type === "file") {
|
||||
file_id = message?.[0].data?.file_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (file_id === "") {
|
||||
logger.info('未找到群文件')
|
||||
return "";
|
||||
}
|
||||
// 获取文件信息
|
||||
let latestFileUrl = await e.bot.sendApi("get_group_file_url", {
|
||||
"group_id": e.group_id,
|
||||
"file_id": file_id
|
||||
});
|
||||
let cleanPath = decodeURIComponent(latestFileUrl.data.url)
|
||||
// 适配 低版本 Napcat 例如:3.6.4
|
||||
if (cleanPath.startsWith("https")) {
|
||||
// https://njc-download.ftn.qq.com/....
|
||||
const urlObj = new URL(cleanPath);
|
||||
// 检查URL中是否包含 fname 参数
|
||||
if (urlObj.searchParams.has('fname')) {
|
||||
// 获取 fname 参数的值
|
||||
const originalFname = urlObj.searchParams.get('fname');
|
||||
|
||||
// 提取 file_id(第一个"."后面的内容)
|
||||
const fileId = file_id.split('.').slice(1).join('.'); // 分割并去掉第一个部分
|
||||
urlObj.searchParams.set('fname', `${originalFname}${fileId}`);
|
||||
return {
|
||||
cleanPath: urlObj.toString(),
|
||||
file_id
|
||||
};
|
||||
}
|
||||
} else if (cleanPath.startsWith('file:///')) {
|
||||
cleanPath = cleanPath.replace('file:///', '')
|
||||
}
|
||||
|
||||
return { cleanPath, file_id };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取群回复
|
||||
* @param e
|
||||
*/
|
||||
export async function getReplyMsg(e) {
|
||||
const msgList = await e.bot.sendApi("get_group_msg_history", {
|
||||
"group_id": e.group_id,
|
||||
"count": 1
|
||||
});
|
||||
let msgId = msgList.data.messages[0]?.message[0]?.data.id
|
||||
let msg = await e.bot.sendApi("get_msg",{
|
||||
"message_id" : msgId
|
||||
})
|
||||
return msg.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取机器人信息
|
||||
* @param e
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getBotLoginInfo(e) {
|
||||
return await e.bot.sendApi("get_login_info");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取运行状态
|
||||
* @param e
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getBotStatus(e) {
|
||||
return await e.bot.sendApi("get_status");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取版本信息
|
||||
* @param e
|
||||
* @returns {Promise<*>}
|
||||
*/
|
||||
export async function getBotVersionInfo(e) {
|
||||
return await e.bot.sendApi("get_version_info");
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送私聊消息
|
||||
* @param e
|
||||
* @param message
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function sendPrivateMsg(e, message) {
|
||||
e.bot.sendApi("send_private_msg", {
|
||||
user_id: e.user_id,
|
||||
message: message,
|
||||
})
|
||||
return textArray.map((item) => {
|
||||
return {
|
||||
message: { type: 'text', text: item },
|
||||
nickname: e.sender.card || e.user_id,
|
||||
user_id: e.user_id,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user