mirror of
https://github.com/Jerryplusy/rc-plugin.git
synced 2025-10-14 08:09:19 +00:00
💩
This commit is contained in:
parent
55d4e17598
commit
ed5b216eac
@ -1,17 +1,6 @@
|
|||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import path from 'path';
|
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
|
* @param err
|
||||||
|
@ -1,15 +1,10 @@
|
|||||||
import {
|
import { GENERAL_REQ_LINK, GENERAL_REQ_LINK_3 } from '../constants/tools.js';
|
||||||
GENERAL_REQ_LINK,
|
|
||||||
GENERAL_REQ_LINK_2, GENERAL_REQ_LINK_3
|
|
||||||
} from "../constants/tools.js";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 第三方接口适配器,用于大面积覆盖解析视频的内容
|
* 第三方接口适配器,用于大面积覆盖解析视频的内容
|
||||||
*/
|
*/
|
||||||
class GeneralLinkAdapter {
|
class GeneralLinkAdapter {
|
||||||
|
constructor() {}
|
||||||
constructor() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 暂时用这个来处理短链接
|
* 暂时用这个来处理短链接
|
||||||
@ -20,8 +15,9 @@ class GeneralLinkAdapter {
|
|||||||
async fetchUrl(url, includeRedirect = false) {
|
async fetchUrl(url, includeRedirect = false) {
|
||||||
let response = await fetch(url, {
|
let response = await fetch(url, {
|
||||||
headers: {
|
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',
|
'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;
|
return includeRedirect ? response.url : response;
|
||||||
}
|
}
|
||||||
@ -35,7 +31,7 @@ class GeneralLinkAdapter {
|
|||||||
createReqLink(externalInterface, requestURL) {
|
createReqLink(externalInterface, requestURL) {
|
||||||
// 这里必须使用{ ...GENERAL_REQ_LINK_2 }赋值,不然就是对象的引用赋值,会造成全局数据问题!
|
// 这里必须使用{ ...GENERAL_REQ_LINK_2 }赋值,不然就是对象的引用赋值,会造成全局数据问题!
|
||||||
let reqLink = { ...externalInterface };
|
let reqLink = { ...externalInterface };
|
||||||
reqLink.link = reqLink.link.replace("{}", requestURL);
|
reqLink.link = reqLink.link.replace('{}', requestURL);
|
||||||
return reqLink;
|
return reqLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,24 +39,30 @@ class GeneralLinkAdapter {
|
|||||||
// 例子:https://www.kuaishou.com/short-video/3xkfs8p4pnd67p4?authorId=3xkznsztpwetngu&streamSource=find&area=homexxbrilliant
|
// 例子:https://www.kuaishou.com/short-video/3xkfs8p4pnd67p4?authorId=3xkznsztpwetngu&streamSource=find&area=homexxbrilliant
|
||||||
// https://v.m.chenzhongtech.com/fw/photo/3xburnkmj3auazc
|
// https://v.m.chenzhongtech.com/fw/photo/3xburnkmj3auazc
|
||||||
// https://v.kuaishou.com/1ff8QP
|
// https://v.kuaishou.com/1ff8QP
|
||||||
let msg = /(?:https?:\/\/)?(www|v)\.(kuaishou|m\.chenzhongtech)\.com\/[A-Za-z\d._?%&+\-=\/#]*/g.exec(link)[0];
|
let msg =
|
||||||
|
/(?:https?:\/\/)?(www|v)\.(kuaishou|m\.chenzhongtech)\.com\/[A-Za-z\d._?%&+\-=\/#]*/g.exec(
|
||||||
|
link
|
||||||
|
)[0];
|
||||||
// 跳转短号
|
// 跳转短号
|
||||||
if (msg.includes("v.kuaishou")) {
|
if (msg.includes('v.kuaishou')) {
|
||||||
msg = await this.fetchUrl(msg, true);
|
msg = await this.fetchUrl(msg, true);
|
||||||
}
|
}
|
||||||
let video_id;
|
let video_id;
|
||||||
if (msg.includes('/fw/photo/')) {
|
if (msg.includes('/fw/photo/')) {
|
||||||
video_id = msg.match(/\/fw\/photo\/([^/?]+)/)[1];
|
video_id = msg.match(/\/fw\/photo\/([^/?]+)/)[1];
|
||||||
} else if (msg.includes("short-video")) {
|
} else if (msg.includes('short-video')) {
|
||||||
video_id = msg.match(/short-video\/([^/?]+)/)[1];
|
video_id = msg.match(/short-video\/([^/?]+)/)[1];
|
||||||
} else {
|
} else {
|
||||||
throw Error("无法提取快手的信息,请重试或者换一个视频!");
|
throw Error('无法提取快手的信息,请重试或者换一个视频!');
|
||||||
}
|
}
|
||||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK, `https://www.kuaishou.com/short-video/${ video_id }`);
|
const reqLink = this.createReqLink(
|
||||||
|
GENERAL_REQ_LINK,
|
||||||
|
`https://www.kuaishou.com/short-video/${video_id}`
|
||||||
|
);
|
||||||
// 提取视频
|
// 提取视频
|
||||||
return {
|
return {
|
||||||
name: "快手",
|
name: '快手',
|
||||||
reqLink
|
reqLink,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,53 +72,53 @@ class GeneralLinkAdapter {
|
|||||||
// 3. https://m.ixigua.com/video/7270448082586698281
|
// 3. https://m.ixigua.com/video/7270448082586698281
|
||||||
let msg = /(?:https?:\/\/)?(www|v|m)\.ixigua\.com\/[A-Za-z\d._?%&+\-=\/#]*/g.exec(link)[0];
|
let msg = /(?:https?:\/\/)?(www|v|m)\.ixigua\.com\/[A-Za-z\d._?%&+\-=\/#]*/g.exec(link)[0];
|
||||||
// 跳转短号
|
// 跳转短号
|
||||||
if (msg.includes("v.ixigua")) {
|
if (msg.includes('v.ixigua')) {
|
||||||
msg = await this.fetchUrl(msg, true);
|
msg = await this.fetchUrl(msg, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = /ixigua\.com\/(\d+)/.exec(msg)[1] || /\/video\/(\d+)/.exec(msg)[1];
|
const id = /ixigua\.com\/(\d+)/.exec(msg)[1] || /\/video\/(\d+)/.exec(msg)[1];
|
||||||
const videoReq = `https://www.ixigua.com/${ id }`;
|
const videoReq = `https://www.ixigua.com/${id}`;
|
||||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK, videoReq);
|
const reqLink = this.createReqLink(GENERAL_REQ_LINK, videoReq);
|
||||||
return { name: "西瓜", reqLink };
|
return { name: '西瓜', reqLink };
|
||||||
}
|
}
|
||||||
|
|
||||||
async pipixia(link) {
|
async pipixia(link) {
|
||||||
const msg = /https:\/\/h5\.pipix\.com\/(s|item)\/[A-Za-z0-9]+/.exec(link)?.[0];
|
const msg = /https:\/\/h5\.pipix\.com\/(s|item)\/[A-Za-z0-9]+/.exec(link)?.[0];
|
||||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
|
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
|
||||||
return { name: "皮皮虾", reqLink };
|
return { name: '皮皮虾', reqLink };
|
||||||
}
|
}
|
||||||
|
|
||||||
async pipigx(link) {
|
async pipigx(link) {
|
||||||
const msg = /https:\/\/h5\.pipigx\.com\/pp\/post\/[A-Za-z0-9]+/.exec(link)?.[0];
|
const msg = /https:\/\/h5\.pipigx\.com\/pp\/post\/[A-Za-z0-9]+/.exec(link)?.[0];
|
||||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
|
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
|
||||||
return { name: "皮皮搞笑", reqLink };
|
return { name: '皮皮搞笑', reqLink };
|
||||||
}
|
}
|
||||||
|
|
||||||
async tieba(link) {
|
async tieba(link) {
|
||||||
const msg = /https:\/\/tieba\.baidu\.com\/p\/[A-Za-z0-9]+/.exec(link)?.[0];
|
const msg = /https:\/\/tieba\.baidu\.com\/p\/[A-Za-z0-9]+/.exec(link)?.[0];
|
||||||
// 这里必须使用{ ...GENERAL_REQ_LINK_2 }赋值,不然就是对象的引用赋值,会造成全局数据问题!
|
// 这里必须使用{ ...GENERAL_REQ_LINK_2 }赋值,不然就是对象的引用赋值,会造成全局数据问题!
|
||||||
// !!!这里加了 '\?' 是因为 API 问题
|
// !!!这里加了 '\?' 是因为 API 问题
|
||||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg + "\"?")
|
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg + '"?');
|
||||||
return { name: "贴吧", reqLink };
|
return { name: '贴吧', reqLink };
|
||||||
}
|
}
|
||||||
|
|
||||||
async qqSmallWorld(link) {
|
async qqSmallWorld(link) {
|
||||||
const msg = /https:\/\/s.xsj\.qq\.com\/[A-Za-z0-9]+/.exec(link)?.[0];
|
const msg = /https:\/\/s.xsj\.qq\.com\/[A-Za-z0-9]+/.exec(link)?.[0];
|
||||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
|
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
|
||||||
return { name: "QQ小世界", reqLink };
|
return { name: 'QQ小世界', reqLink };
|
||||||
}
|
}
|
||||||
|
|
||||||
async jike(link) {
|
async jike(link) {
|
||||||
// https://m.okjike.com/originalPosts/6583b4421f0812cca58402a6?s=ewoidSI6ICI1YTgzMTY4ZmRmNDA2MDAwMTE5N2MwZmQiCn0=
|
// https://m.okjike.com/originalPosts/6583b4421f0812cca58402a6?s=ewoidSI6ICI1YTgzMTY4ZmRmNDA2MDAwMTE5N2MwZmQiCn0=
|
||||||
const msg = /https:\/\/m.okjike.com\/originalPosts\/[A-Za-z0-9]+/.exec(link)?.[0];
|
const msg = /https:\/\/m.okjike.com\/originalPosts\/[A-Za-z0-9]+/.exec(link)?.[0];
|
||||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK_3, msg);
|
const reqLink = this.createReqLink(GENERAL_REQ_LINK_3, msg);
|
||||||
return { name: "即刻", reqLink };
|
return { name: '即刻', reqLink };
|
||||||
}
|
}
|
||||||
|
|
||||||
async douyinBackup(link) {
|
async douyinBackup(link) {
|
||||||
const msg = /(http:\/\/|https:\/\/)v.douyin.com\/[A-Za-z\d._?%&+\-=\/#]*/.exec(link)?.[0];
|
const msg = /(http:\/\/|https:\/\/)v.douyin.com\/[A-Za-z\d._?%&+\-=\/#]*/.exec(link)?.[0];
|
||||||
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
|
const reqLink = this.createReqLink(GENERAL_REQ_LINK, msg);
|
||||||
return { name: "抖音动图", reqLink };
|
return { name: '抖音动图', reqLink };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -125,7 +127,7 @@ class GeneralLinkAdapter {
|
|||||||
* @returns {Promise<*>}
|
* @returns {Promise<*>}
|
||||||
*/
|
*/
|
||||||
async init(link) {
|
async init(link) {
|
||||||
logger.mark("[R插件][通用解析]", link)
|
logger.mark('[R插件][通用解析]', link);
|
||||||
const handlers = new Map([
|
const handlers = new Map([
|
||||||
[/(kuaishou.com|chenzhongtech.com)/, this.ks.bind(this)],
|
[/(kuaishou.com|chenzhongtech.com)/, this.ks.bind(this)],
|
||||||
[/ixigua.com/, this.xigua.bind(this)],
|
[/ixigua.com/, this.xigua.bind(this)],
|
||||||
@ -154,21 +156,22 @@ class GeneralLinkAdapter {
|
|||||||
// 发送GET请求
|
// 发送GET请求
|
||||||
return fetch(adapter.reqLink.link, {
|
return fetch(adapter.reqLink.link, {
|
||||||
headers: {
|
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:
|
||||||
|
'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',
|
'Accept-Language': 'zh-CN,zh;q=0.9',
|
||||||
'Cache-Control': 'no-cache',
|
'Cache-Control': 'no-cache',
|
||||||
'Connection': 'keep-alive',
|
Connection: 'keep-alive',
|
||||||
'Pragma': 'no-cache',
|
Pragma: 'no-cache',
|
||||||
'Sec-Fetch-Dest': 'document',
|
'Sec-Fetch-Dest': 'document',
|
||||||
'Sec-Fetch-Mode': 'navigate',
|
'Sec-Fetch-Mode': 'navigate',
|
||||||
'Sec-Fetch-Site': 'none',
|
'Sec-Fetch-Site': 'none',
|
||||||
'Sec-Fetch-User': '?1',
|
'Sec-Fetch-User': '?1',
|
||||||
'Upgrade-Insecure-Requests': '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',
|
'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
|
timeout: 10000,
|
||||||
}).then(async resp => {
|
}).then(async (resp) => {
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (sign === 1) {
|
if (sign === 1) {
|
||||||
// @link GENERAL_REQ_LINK
|
// @link GENERAL_REQ_LINK
|
||||||
@ -176,25 +179,26 @@ class GeneralLinkAdapter {
|
|||||||
name: adapter.name,
|
name: adapter.name,
|
||||||
images: data.data?.imageUrl,
|
images: data.data?.imageUrl,
|
||||||
video: data.data?.url,
|
video: data.data?.url,
|
||||||
}
|
};
|
||||||
} else if (sign === 2) {
|
} else if (sign === 2) {
|
||||||
// @link GENERAL_REQ_LINK_2
|
// @link GENERAL_REQ_LINK_2
|
||||||
return {
|
return {
|
||||||
name: adapter.name,
|
name: adapter.name,
|
||||||
|
|
||||||
images: data.data?.images,
|
images: data.data?.images,
|
||||||
video: data.data?.videoUrl,
|
video: data.data?.videoUrl,
|
||||||
desc: data.data?.desc
|
desc: data.data?.desc,
|
||||||
}
|
};
|
||||||
} else if (sign === 3) {
|
} else if (sign === 3) {
|
||||||
console.log(data)
|
console.log(data);
|
||||||
return {
|
return {
|
||||||
name: adapter.name,
|
name: adapter.name,
|
||||||
images: data?.images.map(item => item.url),
|
images: data?.images.map((item) => item.url),
|
||||||
}
|
};
|
||||||
} else {
|
} else {
|
||||||
throw Error("[R插件][通用解析]错误Sign标识");
|
throw Error('[R插件][通用解析]错误Sign标识');
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -211,4 +215,4 @@ class GeneralLinkAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -31,23 +31,5 @@ export async function redisGetKey(key) {
|
|||||||
* await redisSetKey('myKey', { foo: 'bar' });
|
* await redisSetKey('myKey', { foo: 'bar' });
|
||||||
*/
|
*/
|
||||||
export async function redisSetKey(key, value = {}) {
|
export async function redisSetKey(key, value = {}) {
|
||||||
return redis.set(
|
return redis.set(key, JSON.stringify(value));
|
||||||
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}
|
* 将只有 text 类型的数组转换为原生的 {Bot.makeForwardMsg}
|
||||||
* @param e
|
* @param e
|
||||||
* @param textArray {string[]}
|
* @param textArray {string[]}
|
||||||
*/
|
*/
|
||||||
export function textArrayToMakeForward(e, textArray) {
|
export function textArrayToMakeForward(e, textArray) {
|
||||||
return textArray.map(item => {
|
return textArray.map((item) => {
|
||||||
return {
|
return {
|
||||||
message: { type: "text", text: item },
|
message: { type: 'text', text: item },
|
||||||
nickname: e.sender.card || e.user_id,
|
nickname: e.sender.card || e.user_id,
|
||||||
user_id: 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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user