Compare commits

..

No commits in common. "3e01d94268535720833b619d791a96c93ab4eaad" and "64d2022d0e22818e21a23b55c227f48cb24fae4b" have entirely different histories.

5 changed files with 0 additions and 489 deletions

View File

@ -6,7 +6,6 @@ import fetch from 'node-fetch';
import fs from 'node:fs'; import fs from 'node:fs';
import PQueue from 'p-queue'; import PQueue from 'p-queue';
import querystring from 'querystring'; import querystring from 'querystring';
import { genVerifyFp } from '../utils/tiktok.js';
import { import {
BILI_CDN_SELECT_LIST, BILI_CDN_SELECT_LIST,
BILI_DEFAULT_INTRO_LEN_LIMIT, BILI_DEFAULT_INTRO_LEN_LIMIT,
@ -26,8 +25,6 @@ import {
BILI_SSID_INFO, BILI_SSID_INFO,
BILI_STREAM_FLV, BILI_STREAM_FLV,
BILI_STREAM_INFO, BILI_STREAM_INFO,
DY_LIVE_INFO,
DY_LIVE_INFO_2,
BILI_SUMMARY, BILI_SUMMARY,
MIYOUSHE_ARTICLE, MIYOUSHE_ARTICLE,
} from '../constants/tools.js'; } from '../constants/tools.js';
@ -48,7 +45,6 @@ import { getWbi } from '../utils/biliWbi.js';
import { import {
checkToolInCurEnv, checkToolInCurEnv,
formatBiliInfo, formatBiliInfo,
retryAxiosReq,
secondsToTime, secondsToTime,
truncateString, truncateString,
urlTransformShortLink, urlTransformShortLink,
@ -59,9 +55,6 @@ import { getDS } from '../utils/mihoyo.js';
import { redisExistKey, redisGetKey, redisSetKey } from '../utils/redis-util.js'; import { redisExistKey, redisGetKey, redisSetKey } from '../utils/redis-util.js';
import { textArrayToMakeForward } from '../utils/yunzai-util.js'; import { textArrayToMakeForward } from '../utils/yunzai-util.js';
import GeneralLinkAdapter from '../utils/general-link-adapter.js'; import GeneralLinkAdapter from '../utils/general-link-adapter.js';
import aBogus from '../utils/a-bogus.cjs';
import * as DY_INFO from 'es-toolkit/compat';
import * as DY_TOUTIAO_INFO from 'es-toolkit/compat';
export class RCtools extends plugin { export class RCtools extends plugin {
constructor() { constructor() {
@ -83,10 +76,6 @@ export class RCtools extends plugin {
reg: '(miyoushe.com)', reg: '(miyoushe.com)',
fnc: 'miyoushe', fnc: 'miyoushe',
}, },
{
reg: '(v.douyin.com|live.douyin.com)',
fnc: 'douyin',
},
], ],
}); });
// 配置文件 // 配置文件
@ -172,409 +161,6 @@ export class RCtools extends plugin {
this.nickName = '真寻'; this.nickName = '真寻';
} }
async douyin(e) {
// 切面判断是否需要解析
if (!(await this.isEnableResolve(RESOLVE_CONTROLLER_NAME_ENUM.douyin))) {
logger.info(`[R插件][全局解析控制] ${RESOLVE_CONTROLLER_NAME_ENUM.douyin} 已拦截`);
return true;
}
const urlRex = /(http:\/\/|https:\/\/)(v|live).douyin.com\/[A-Za-z\d._?%&+\-=\/#]*/;
// 检测无效链接例如v.douyin.com
if (!urlRex.test(e.msg)) {
e.reply(`看上去这不像一个正经的抖音链接呢,${this.nickName}就不帮你解析咯..`);
return;
}
// 获取链接
let douUrl = urlRex.exec(e.msg.trim())[0];
let ttwid = '';
if (douUrl.includes('v.douyin.com')) {
const { location, ttwidValue } = await this.douyinRequest(douUrl);
ttwid = ttwidValue;
douUrl = location;
}
// TODO 如果有新的好解决方案可以删除如果遇到https://www.iesdouyin.com/share/slides这类动图暂时交付给其他API解析感谢群u:"Error: Cannot find id"提供的服务器
if (douUrl.includes('share/slides')) {
const detailIdMatch = douUrl.match(/\/slides\/(\d+)/);
const detailId = detailIdMatch[1];
const apiUrl = 'http://tk.xigua.wiki:5555/douyin/detail';
const postData = {
cookie: '',
proxy: '',
source: false,
detail_id: detailId,
};
// 用于存储下载的文件路径
const downloadedFilePaths = [];
try {
const apiResponse = await axios.post(apiUrl, postData, {
headers: {
'Content-Type': 'application/json',
accept: 'application/json',
},
timeout: 15000,
});
if (apiResponse.status !== 200 || !apiResponse.data || !apiResponse.data.data) {
logger.error(
`[R插件][抖音解析] API返回异常状态码或数据结构错误: ${apiResponse.status}, ${JSON.stringify(apiResponse.data)}`
);
e.reply(
'解析抖音动图失败了,可能是小猫踹翻了路由器,或者是小老鼠咬断了网线,总之等等再试试看..'
);
return true;
}
const apiData = apiResponse.data.data;
const downloads = apiData.downloads;
const desc = apiData.desc || '无简介';
const authorNickname = apiData.nickname || '未知作者';
const replyMessages = [];
replyMessages.push(
`${this.nickName}识别:抖音动图 \n 作者是:${authorNickname}\n 简介:${desc}`
);
const messageSegments = [];
const downloadPath = this.getCurDownloadPath(e);
await mkdirIfNotExists(downloadPath);
await e.reply(replyMessages.join('\n'));
for (const [index, downloadUrl] of downloads.entries()) {
let filePath;
let fileName;
try {
if (downloadUrl.includes('.mp4') || downloadUrl.includes('video_id')) {
fileName = `temp${index > 0 ? index : ''}.mp4`;
filePath = `${downloadPath}/${fileName}`;
logger.info(`[R插件][抖音动图] 下载视频: ${downloadUrl}`);
const response = await axios({
method: 'get',
url: downloadUrl,
responseType: 'stream',
});
const writer = fs.createWriteStream(filePath);
response.data.pipe(writer);
await new Promise((resolve, reject) => {
writer.on('finish', resolve);
writer.on('error', reject);
});
logger.info(`[R插件][抖音动图] 视频下载完成: ${filePath}`);
messageSegments.push({
message: segment.video(filePath),
nickname: e.sender.card || e.user_id,
user_id: e.user_id,
});
downloadedFilePaths.push(filePath);
} else {
fileName = `temp${index > 0 ? index : ''}.png`;
filePath = `${downloadPath}/${fileName}`;
logger.info(`[R插件][抖音动图] 下载图片: ${downloadUrl}`);
const response = await axios({
method: 'get',
url: downloadUrl,
responseType: 'stream',
});
const writer = fs.createWriteStream(filePath);
response.data.pipe(writer);
await new Promise((resolve, reject) => {
writer.on('finish', resolve);
writer.on('error', reject);
});
logger.info(`[R插件][抖音动图] 图片下载完成: ${filePath}`);
messageSegments.push({
message: segment.image(filePath),
nickname: e.sender.card || e.user_id,
user_id: e.user_id,
});
downloadedFilePaths.push(filePath);
}
} catch (downloadError) {
logger.error(
`[R插件][抖音动图] 下载文件失败: ${downloadUrl}, 错误: ${downloadError.message}`
);
messageSegments.push({
message: { type: 'text', text: `下载文件失败: ${downloadUrl}` },
nickname: e.sender.card || e.user_id,
user_id: e.user_id,
});
}
}
if (messageSegments.length > 0) {
const forwardMsg = await Bot.makeForwardMsg(messageSegments);
await e.reply(forwardMsg);
// 删除文件
for (const filePath of downloadedFilePaths) {
await checkAndRemoveFile(filePath);
}
}
const headers = {
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
'User-Agent': COMMON_USER_AGENT,
Referer: 'https://www.douyin.com/',
cookie: this.douyinCookie,
};
await this.douyinComment(e, detailId, headers);
} catch (error) {
logger.error(`[R插件][抖音动图] 调用API或处理下载时发生错误: ${error.message}`);
}
return true;
}
// 获取 ID
const douId =
/note\/(\d+)/g.exec(douUrl)?.[1] ||
/video\/(\d+)/g.exec(douUrl)?.[1] ||
/live.douyin.com\/(\d+)/.exec(douUrl)?.[1] ||
/live\/(\d+)/.exec(douUrl)?.[1] ||
/webcast.amemv.com\/douyin\/webcast\/reflow\/(\d+)/.exec(douUrl)?.[1];
// 当前版本需要填入cookie
if (_.isEmpty(this.douyinCookie) || _.isEmpty(douId)) {
e.reply(`${this.nickName}的主人还没有给我配置cookie,没办法帮你解析抖音啦`);
return;
}
// 以下是更新了很多次的抖音API历史且用且珍惜
// const url = `https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids=${ douId }`;
// const url = `https://www.iesdouyin.com/aweme/v1/web/aweme/detail/?aweme_id=${ douId }&aid=1128&version_name=23.5.0&device_platform=android&os_version=2333`;
// 感谢 Evil0ctalhttps://github.com/Evil0ctal提供的header 和 B1gM8chttps://github.com/B1gM8c的逆向算法X-Bogus
const headers = {
'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
'User-Agent': COMMON_USER_AGENT,
Referer: 'https://www.douyin.com/',
cookie: this.douyinCookie,
};
let dyApi;
if (douUrl.includes('live.douyin.com')) {
// 第一类直播类型
dyApi = DY_LIVE_INFO.replaceAll('{}', douId);
} else if (douUrl.includes('webcast.amemv.com')) {
// 第二类直播类型,这里必须使用客户端的 fetch 请求
dyApi =
DY_LIVE_INFO_2.replace('{}', douId) + `&verifyFp=${genVerifyFp()}` + `&msToken=${ttwid}`;
const webcastResp = await fetch(dyApi);
const webcastData = await webcastResp.json();
const item = webcastData.data.room;
const { title, cover, user_count, stream_url } = item;
const dySendContent = `${this.nickName}发现了一个抖音直播,${title}`;
e.reply([
segment.image(cover?.url_list?.[0]),
dySendContent,
`\n🏄共有${user_count}人正在观看`,
]);
// 下载10s的直播流
await this.sendStreamSegment(
e,
stream_url?.flv_pull_url?.HD1 ||
stream_url?.flv_pull_url?.FULL_HD1 ||
stream_url?.flv_pull_url?.SD1 ||
stream_url?.flv_pull_url?.SD2
);
return;
} else {
// 普通类型
dyApi = DY_INFO.replace('{}', douId);
}
// a-bogus参数
const abParam = aBogus.generate_a_bogus(
new URLSearchParams(new URL(dyApi).search).toString(),
headers['User-Agent']
);
// const param = resp.data.result[0].paramsencode;
const resDyApi = `${dyApi}&a_bogus=${abParam}`;
headers['Referer'] = `https://www.douyin.com/`;
// 定义一个dy请求
const dyResponse = () =>
axios.get(resDyApi, {
headers,
});
// 如果失败进行3次重试
try {
const data = await retryAxiosReq(dyResponse);
// saveJsonToFile(data);
// 直播数据逻辑
if (douUrl.includes('live')) {
const item = await data.data.data?.[0];
const { title, cover, user_count_str, stream_url } = item;
const dySendContent = `${this.nickName}发现了一个抖音直播,${title}`;
e.reply([
segment.image(cover?.url_list?.[0]),
dySendContent,
`\n共有${user_count_str}人正在观看`,
]);
// 下载10s的直播流
await this.sendStreamSegment(
e,
stream_url?.flv_pull_url?.HD1 ||
stream_url?.flv_pull_url?.FULL_HD1 ||
stream_url?.flv_pull_url?.SD1 ||
stream_url?.flv_pull_url?.SD2
);
return;
}
const item = await data.aweme_detail;
// await saveJsonToFile(item);
// 如果为null则退出
if (item == null) {
e.reply(`${this.nickName}无法识别当前抖音内容了..换一个试试`);
return;
}
const urlTypeCode = item.aweme_type;
const urlType = douyinTypeMap[urlTypeCode];
// 核心内容
if (urlType === 'video') {
// logger.info(item.video);
// 多位面选择play_addr、play_addr_265、play_addr_h264
const {
play_addr: { uri: videoAddrURI },
duration,
cover,
} = item.video;
// 进行时间判断,如果超过时间阈值就不发送
const dyDuration = Math.trunc(duration / 1000);
const durationThreshold = this.biliDuration;
// 一些共同发送内容
let dySendContent = `${this.nickName}猜这是一个抖音视频\n${item.author.nickname}\n简介:${item.desc}`;
if (dyDuration >= durationThreshold) {
// 超过阈值,不发送的情况
// 封面
const dyCover = cover.url_list?.pop();
// logger.info(cover.url_list);
dySendContent += `\n
${DIVIDING_LINE.replace('{}', '这视频真带派')}\n当前视频时长约${(dyDuration / 60).toFixed(2).replace(/\.00$/, '')} 分钟\n大于${this.nickName}管理员设置的最大时长 ${(durationThreshold / 60).toFixed(2).replace(/\.00$/, '')} 分钟,还是到抖音里面看吧..`;
e.reply([segment.image(dyCover), dySendContent]);
// 如果开启评论的就调用
await this.douyinComment(e, douId, headers);
return;
}
e.reply(`${dySendContent}`);
// 分辨率判断是否压缩
const resolution = this.douyinCompression ? '720p' : '1080p';
// 使用今日头条 CDN 进一步加快解析速度
const resUrl = DY_TOUTIAO_INFO.replace('1080p', resolution).replace('{}', videoAddrURI);
// ⚠️ 暂时废弃代码
/*if (this.douyinCompression) {
// H.265压缩率更高、流量省一半. 相对于H.264
// 265 和 264 随机均衡负载
const videoAddrList = Math.random() > 0.5 ? play_addr_265.url_list : play_addr_h264.url_list;
resUrl = videoAddrList[videoAddrList.length - 1] || videoAddrList[0];
} else {
// 原始格式ps. videoAddrList这里[0]、[1]是 http[最后一个]是 https
const videoAddrList = play_addr.url_list;
resUrl = videoAddrList[videoAddrList.length - 1] || videoAddrList[0];
}*/
// logger.info(resUrl);
const path = `${this.getCurDownloadPath(e)}/temp.mp4`;
// 加入队列
await this.downloadVideo(resUrl).then(() => {
this.sendVideoToUpload(e, path);
});
} else if (urlType === 'image') {
// 发送描述
e.reply(`${this.nickName}识别: 抖音, ${item.desc}`);
// 无水印图片列表
let no_watermark_image_list = [];
// 有水印图片列表
// let watermark_image_list = [];
for (let i of item.images) {
// 无水印图片列表
no_watermark_image_list.push({
message: segment.image(i.url_list[0]),
nickname: this.e.sender.card || this.e.user_id,
user_id: this.e.user_id,
});
// 有水印图片列表
// watermark_image_list.push(i.download_url_list[0]);
// e.reply(segment.image(i.url_list[0]));
}
// console.log(no_watermark_image_list)
await e.reply(await Bot.makeForwardMsg(no_watermark_image_list));
}
// 如果开启评论的就调用
await this.douyinComment(e, douId, headers);
} catch (err) {
logger.error(err);
logger.mark(
`Cookie 过期或者 Cookie 没有填写,请参考\n${HELP_DOC}\n尝试无效后可以到官方QQ群[575663150]提出 bug 等待解决`
);
}
return true;
}
/**
* douyin 请求参数
* @param url
* @returns {Promise<string>}
*/
async douyinRequest(url) {
const params = {
headers: {
'User-Agent': COMMON_USER_AGENT,
},
timeout: 10000,
};
try {
const resp = await axios.get(url, params);
const location = resp.request.res.responseUrl;
const setCookieHeaders = resp.headers['set-cookie'];
let ttwidValue;
if (setCookieHeaders) {
setCookieHeaders.forEach((cookie) => {
// 使用正则表达式提取 ttwid 的值
const ttwidMatch = cookie.match(/ttwid=([^;]+)/);
if (ttwidMatch) {
ttwidValue = ttwidMatch[1];
}
});
}
return new Promise((resolve, reject) => {
if (location != null) {
return resolve({
location: location,
ttwidValue: ttwidValue,
});
} else {
return reject('获取失败');
}
});
} catch (error) {
logger.error(error);
throw error;
}
}
/**
* 获取 DY 评论
* @param e
* @param douId
* @param headers
*/
async douyinComment(e, douId, headers) {
if (!this.douyinComments) {
return;
}
const dyCommentUrl = DY_COMMENT.replace('{}', douId);
const abParam = aBogus.generate_a_bogus(
new URLSearchParams(new URL(dyCommentUrl).search).toString(),
headers['User-Agent']
);
const commentsResp = await axios.get(`${dyCommentUrl}&a_bogus=${abParam}`, {
headers,
});
// logger.info(headers)
// saveJsonToFile(commentsResp.data, "data.json", _);
const comments = commentsResp.data.comments;
const replyComments = comments.map((item) => {
return {
message: item.text,
nickname: this.e.sender.card || this.e.user_id,
user_id: this.e.user_id,
};
});
e.reply(await Bot.makeForwardMsg(replyComments));
}
/** /**
* 下载直播片段 * 下载直播片段
* @param e * @param e

View File

@ -13,20 +13,6 @@ export const BILI_SUMMARY = 'https://api.bilibili.com/x/web-interface/view/concl
export const BILI_PLAY_STREAM = export const BILI_PLAY_STREAM =
'https://api.bilibili.com/x/player/wbi/playurl?cid={cid}&bvid={bvid}&qn={qn}&fnval=16'; 'https://api.bilibili.com/x/player/wbi/playurl?cid={cid}&bvid={bvid}&qn={qn}&fnval=16';
/**
* DY 直播信息
* @type {string}
*/
export const DY_LIVE_INFO =
'https://live.douyin.com/webcast/room/web/enter/?device_platform=webapp&aid=6383&channel=channel_pc_web&pc_client_type=1&version_code=190500&version_name=19.5.0&cookie_enabled=true&screen_width=1920&screen_height=1080&browser_language=zh-CN&browser_platform=Win32&browser_name=Firefox&browser_version=124.0&browser_online=true&engine_name=Gecko&engine_version=122.0.0.0&os_name=Windows&os_version=10&cpu_core_num=12&device_memory=8&platform=PC&web_rid={}&room_id_str={}';
/**
* DY 直播信息 二类型
* @type {string}
*/
export const DY_LIVE_INFO_2 =
'https://webcast.amemv.com/webcast/room/reflow/info/?type_id=0&live_id=1&sec_user_id=&version_code=99.99.99&app_id=1128&room_id={}';
/** /**
* 动态信息 * 动态信息
* https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/dynamic/get_dynamic_detail.md * https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/dynamic/get_dynamic_detail.md

View File

@ -23,31 +23,6 @@ export function formatBiliInfo(data) {
.join(' | '); .join(' | ');
} }
/**
* 重试 axios 请求
* @param requestFunction
* @param retries
* @param delay
* @returns {*}
*/
export async function retryAxiosReq(requestFunction, retries = 3, delay = 1000) {
try {
const response = await requestFunction();
if (!response.data) {
throw new Error('请求空数据');
}
return response.data;
} catch (error) {
if (retries > 0) {
logger.mark(`[R插件][重试模块]重试中... (${3 - retries + 1}/3) 次`);
await new Promise((resolve) => setTimeout(resolve, delay));
return retryAxiosReq(requestFunction, retries - 1, delay);
} else {
throw error;
}
}
}
/** /**
* 数字转换成具体时间 * 数字转换成具体时间
* @param seconds * @param seconds

View File

@ -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("");
}