feat: V1.6.0 油管解析重构 & 修复tiktok

1. 加入梯子检测,提升R插件的健壮性
2. 油管解析大重构,稳定性大大提高
3. 修复tiktok出现的问题
This commit is contained in:
zhiyu1998 2024-03-26 20:24:22 +08:00
parent 1791f82bf1
commit ad6b29cf82
5 changed files with 132 additions and 113 deletions

View File

@ -63,18 +63,6 @@ sudo apt-get install ffmpeg
# 其他linux参考群友推荐https://gitee.com/baihu433/ffmpeg # 其他linux参考群友推荐https://gitee.com/baihu433/ffmpeg
# Windows 参考https://www.jianshu.com/p/5015a477de3c # Windows 参考https://www.jianshu.com/p/5015a477de3c
```` ````
`油管解析`需要 `yt-dlp` 的依赖才能完成解析(三选一):
```shell
# 三选一
# ubuntu (国内 or 国外且安装了snap
snap install yt-dlp
# debian 海外
curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o ~/.local/bin/yt-dlp
chmod a+rx ~/.local/bin/yt-dlp
# debian 国内
curl -L https://ghproxy.net/https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o ~/.local/bin/yt-dlp
chmod a+rx ~/.local/bin/yt-dlp
```
4. 【可选】小程序解析适配了: 4. 【可选】小程序解析适配了:
* 喵崽:[Yoimiya / Miao-Yunzai](https://gitee.com/yoimiya-kokomi/Miao-Yunzai) * 喵崽:[Yoimiya / Miao-Yunzai](https://gitee.com/yoimiya-kokomi/Miao-Yunzai)
@ -186,11 +174,6 @@ git clone -b 1.5.1 https://gitee.com/kyrzy0416/rconsole-plugin.git
<img src="https://contrib.rocks/image?repo=zhiyu1998/rconsole-plugin&max=1000" /> <img src="https://contrib.rocks/image?repo=zhiyu1998/rconsole-plugin&max=1000" />
</a> </a>
🌸感谢以下框架的开源🌸:
油管解析参考了:
- [yt-dlp:A youtube-dl fork with additional features and fixes](https://github.com/yt-dlp/yt-dlp)
## ☕ 请我喝一杯瑞幸咖啡 ## ☕ 请我喝一杯瑞幸咖啡
如果你觉得插件能帮助到你增进好友关系,那么你可以在有条件的情况下[请我喝一杯瑞幸咖啡](https://afdian.net/a/zhiyu1998),这是我开源这个插件的最大动力! 如果你觉得插件能帮助到你增进好友关系,那么你可以在有条件的情况下[请我喝一杯瑞幸咖啡](https://afdian.net/a/zhiyu1998),这是我开源这个插件的最大动力!
感谢以下朋友的支持!(排名不分多少) 感谢以下朋友的支持!(排名不分多少)

View File

@ -31,9 +31,9 @@ import {
containsChinese, containsChinese,
downloadImg, downloadImg,
downloadMp3, downloadMp3,
formatBiliInfo, formatBiliInfo, formatSeconds,
getIdVideo, getIdVideo,
secondsToTime, truncateString secondsToTime, testProxy, truncateString
} from "../utils/common.js"; } from "../utils/common.js";
import config from "../model/index.js"; import config from "../model/index.js";
import Translate from "../utils/trans-strategy.js"; import Translate from "../utils/trans-strategy.js";
@ -284,25 +284,46 @@ export class tools extends plugin {
async tiktok(e) { async tiktok(e) {
// 判断海外 // 判断海外
const isOversea = await this.isOverseasServer(); const isOversea = await this.isOverseasServer();
// 如果不是海外用户且没有梯子直接返回
if (!isOversea && await testProxy()) {
e.reply("检测到没有梯子无法解析TikTok");
return false;
}
// 处理链接 // 处理链接
let url = await processTikTokUrl(e.msg.trim(), isOversea); let url = await processTikTokUrl(e.msg.trim(), isOversea);
// 处理ID // 处理ID
let tiktokVideoId = await getIdVideo(url); let tiktokVideoId = await getIdVideo(url);
tiktokVideoId = tiktokVideoId.replace(/\//g, ""); tiktokVideoId = tiktokVideoId.replace(/\//g, "");
// API链接
const API_URL = TIKTOK_INFO.replace("{}", tiktokVideoId); const config = {
await fetch(API_URL, {
headers: { headers: {
"User-Agent": "User-Agent":
"Mozilla/5.0 (Linux; Android 5.0; SM-G900P Build/LRX21T) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Mobile Safari/537.36", "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36",
"Content-Type": "application/json",
"Accept-Encoding": "gzip,deflate,compress",
}, },
// redirect: "follow", // redirect: "follow",
follow: 10, follow: 10,
timeout: 10000, timeout: 10000,
agent: isOversea ? '' : new HttpProxyAgent(this.myProxy), }
// 如果不是海外,则使用代理
if (!isOversea) {
config.httpsAgent = tunnel.httpsOverHttp({
proxy: new HttpProxyAgent(this.myProxy),
});
}
const params = new URLSearchParams({
"iid": "7318518857994389254",
"device_id": "7318517321748022790",
"channel": "googleplay",
"app_name": "musical_ly",
"version_code": "300904",
"device_platform": "android",
"device_type": "ASUS_Z01QD",
"os_version": "9",
"aweme_id": tiktokVideoId
}) })
console.log(`${TIKTOK_INFO}?${params.toString()}`)
await fetch(`${TIKTOK_INFO}?${params.toString()}`, config)
.then(async resp => { .then(async resp => {
const respJson = await resp.json(); const respJson = await resp.json();
const data = respJson.aweme_list[0]; const data = respJson.aweme_list[0];
@ -584,13 +605,21 @@ export class tools extends plugin {
// 使用现有api解析小蓝鸟 // 使用现有api解析小蓝鸟
async twitter_x(e) { async twitter_x(e) {
// 判断海外
const isOversea = await this.isOverseasServer();
// 如果不是海外用户且没有梯子直接返回
if (!isOversea && await testProxy()) {
e.reply("检测到没有梯子无法解析TikTok");
return false;
}
// 配置参数及解析 // 配置参数及解析
const reg = /https?:\/\/x.com\/[0-9-a-zA-Z_]{1,20}\/status\/([0-9]*)/; const reg = /https?:\/\/x.com\/[0-9-a-zA-Z_]{1,20}\/status\/([0-9]*)/;
const twitterUrl = reg.exec(e.msg)[0]; const twitterUrl = reg.exec(e.msg)[0];
// 提取视频 // 提取视频
const videoUrl = GENERAL_REQ_LINK.link.replace("{}", twitterUrl); const videoUrl = GENERAL_REQ_LINK.link.replace("{}", twitterUrl);
e.reply("识别:小蓝鸟"); e.reply("识别:小蓝鸟");
axios.get(videoUrl, { const config = {
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',
@ -606,7 +635,17 @@ export class tools extends plugin {
}, },
timeout: 10000 // 设置超时时间 timeout: 10000 // 设置超时时间
}).then(resp => { }
// 如果不是海外,则使用代理
if (!isOversea) {
config.httpsAgent = tunnel.httpsOverHttp({
proxy: {
host: this.proxyAddr,
port: this.proxyPort
},
});
}
axios.get(videoUrl, config).then(resp => {
const url = resp.data.data?.url; const url = resp.data.data?.url;
if (url && (url.endsWith(".jpg") || url.endsWith(".png"))) { if (url && (url.endsWith(".jpg") || url.endsWith(".png"))) {
e.reply(segment.image(url)); e.reply(segment.image(url));
@ -1027,94 +1066,55 @@ export class tools extends plugin {
async y2b(e) { async y2b(e) {
const urlRex = /(?:https?:\/\/)?(www\.)?youtube\.com\/[A-Za-z\d._?%&+\-=\/#]*/g; const urlRex = /(?:https?:\/\/)?(www\.)?youtube\.com\/[A-Za-z\d._?%&+\-=\/#]*/g;
let url = urlRex.exec(e.msg)[0]; let url = urlRex.exec(e.msg)[0];
// 获取url查询参数 // 判断海外
const query = querystring.parse(url.split("?")[1]); const isOversea = await this.isOverseasServer();
let p = query?.p || '0'; // 如果不是海外用户且没有梯子直接返回
let v = query?.v || url.match(/shorts\/([A-Za-z0-9_-]+)/)[1]; if (!isOversea && await testProxy()) {
// 判断是否是海外服务器默认为false e.reply("检测到没有梯子无法解析TikTok");
const isProxy = !(await this.isOverseasServer()); return false;
}
let audios = [], videos = [];
let bestAudio = {}, bestVideo = {};
let rs = { title: '', thumbnail: '', formats: [] };
try { try {
let cmd = `yt-dlp --print-json --skip-download ${ this.y2bCk !== undefined ? `--cookies ${ this.y2bCk }` : '' } '${ url }' ${ isProxy ? `--proxy ${ this.proxyAddr }:${ this.proxyPort }` : '' } 2> /dev/null` // Perform the HTTP GET request
logger.mark('解析视频, 命令:', cmd); const formData = {
rs = child_process.execSync(cmd).toString(); "link": url,
try { "from": "ytbsaver"
rs = JSON.parse(rs);
} catch (error) {
let cmd = `yt-dlp --print-json --skip-download ${ this.y2bCk !== undefined ? `--cookies ${ this.y2bCk }` : '' } '${ url }?p=1' ${ isProxy ? `--proxy ${ this.proxyAddr }:${ this.proxyPort }` : '' } 2> /dev/null`;
logger.mark('尝试分P, 命令:', cmd);
rs = child_process.execSync(cmd).toString();
rs = JSON.parse(rs);
p = '1';
// url = `${msg.url}?p=1`;
}
if (!containsChinese(rs.title)) {
// 启用翻译引擎翻译不是中文的标题
const transedTitle = await this.translateEngine.translate(rs.title, '中');
// const transedDescription = await this.translateEngine.translate(rs.description, '中');
e.reply(`识别:油管,
${ rs.title.trim() }\n
${ DIVIDING_LINE.replace("{}", "R插件翻译引擎服务") }\n
${ transedTitle }\n
${ rs.description }
`);
} else {
e.reply(`识别:油管,${ rs.title }`);
}
} catch (error) {
logger.error(error.toString());
e.reply("解析失败")
return;
} }
const params = new URLSearchParams();
Object.keys(formData).forEach(key => params.append(key, formData[key]));
// 格式化 const config = {
rs.formats.forEach(it => { headers: {
let length = (it.filesize_approx ? '≈' : '') + ((it.filesize || it.filesize_approx || 0) / 1024 / 1024).toFixed(2); "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36",
if (it.audio_ext != 'none') { "origin": "https://www.ytbsaver.com"
audios.push(getAudio(it.format_id, it.ext, (it.abr || 0).toFixed(0), it.format_note || it.format || '', length)); },
} else if (it.video_ext != 'none') {
videos.push(getVideo(it.format_id, it.ext, it.resolution, it.height, (it.vbr || 0).toFixed(0), it.format_note || it.format || '', length));
} }
// 如果不是海外,则使用代理
if (!isOversea) {
config.httpsAgent = tunnel.httpsOverHttp({
proxy: {
host: this.proxyAddr,
port: this.proxyPort
},
}); });
// 寻找最佳的分辨率
// bestAudio = Array.from(audios).sort((a, b) => a.rate - b.rate)[audios.length - 1];
// bestVideo = Array.from(videos).sort((a, b) => a.rate - b.rate)[videos.length - 1];
// 较为有性能的分辨率
bestVideo = Array.from(videos).find(item => item.scale.includes("720") || item.scale.includes("360"));
bestAudio = Array.from(audios).find(item => item.format === 'm4a');
// logger.mark({
// bestVideo,
// bestAudio
// })
// 格式化yt-dlp的请求
const format = `${ bestVideo.id }x${ bestAudio.id }`
// 下载地址格式化
const path = `${ v }${ p ? `/p${ p }` : '' }`;
const fullpath = `${ this.getCurDownloadPath(e) }/${ path }`;
// 创建下载文件夹
await mkdirIfNotExists(fullpath);
// yt-dlp下载
let cmd = //`cd '${__dirname}' && (cd tmp > /dev/null || (mkdir tmp && cd tmp)) &&` +
`yt-dlp ${ this.y2bCk !== undefined ? `--cookies ${ this.y2bCk }` : '' } ${ url } -f ${ format.replace('x', '+') } ` +
`-o '${ fullpath }/${ v }.%(ext)s' ${ isProxy ? `--proxy ${ this.proxyAddr }:${ this.proxyPort }` : '' } -k --write-info-json`;
logger.mark(cmd)
try {
await child_process.execSync(cmd);
e.reply(segment.video(`${ fullpath }/${ v }.mp4`))
// 清理文件
await deleteFolderRecursive(`${ fullpath.split('\/').slice(0, -2).join('/') }`);
} catch (error) {
logger.error(error.toString());
e.reply("y2b下载失败");
return;
} }
const response = await axios.post("https://api.ytbvideoly.com/api/thirdvideo/parse", params.toString(), config);
const {title, /*thumbnail,*/ duration, formats} = response.data.data;
e.reply(`识别:油管,${title}\n时长:${formatSeconds(duration)}`);
if (formats.length > 0) {
// 大概率是720p
const videoUrl = formats?.[formats.length - 1].url;
this.downloadVideo(videoUrl).then(path => {
e.reply(segment.video(path + "/temp.mp4"));
});
}
} catch (error) {
console.error(error);
throw error; // Rethrow the error so it can be handled by the caller
}
return true;
} }
// 米游社 // 米游社

View File

@ -1,5 +1,5 @@
- { - {
version: 1.5.13, version: 1.6.0,
data: data:
[ [
新增<span class="cmd">微视解析</span>功能, 新增<span class="cmd">微视解析</span>功能,

View File

@ -63,7 +63,7 @@ export const DY_INFO = "https://www.douyin.com/aweme/v1/web/aweme/detail/?device
* Tiktok API * Tiktok API
* @type {string} * @type {string}
*/ */
export const TIKTOK_INFO = "https://api16-normal-c-useast1a.tiktokv.com/aweme/v1/feed/?aweme_id={}" export const TIKTOK_INFO = "https://api22-normal-c-alisg.tiktokv.com/aweme/v1/feed/"
/** /**
* X API * X API

View File

@ -1,6 +1,7 @@
import schedule from "node-schedule"; import schedule from "node-schedule";
import common from "../../../lib/common/common.js"; import common from "../../../lib/common/common.js";
import axios from "axios"; import axios from "axios";
import tunnel from "tunnel";
import fs from "node:fs"; import fs from "node:fs";
import fetch from "node-fetch"; import fetch from "node-fetch";
import { mkdirIfNotExists } from "./file.js"; import { mkdirIfNotExists } from "./file.js";
@ -299,3 +300,38 @@ export function truncateString(inputString, maxLength = 50) {
return truncatedString; return truncatedString;
} }
} }
/**
* 测试当前是否存在🪜
* @returns {Promise<Boolean>}
*/
export async function testProxy() {
// 配置代理服务器
const proxyOptions = {
host: '127.0.0.1',
port: 7890,
// 如果你的代理服务器需要认证
// auth: 'username:password', // 取消注释并提供实际的用户名和密码
};
// 创建一个代理隧道
const httpsAgent = tunnel.httpsOverHttp({
proxy: proxyOptions
});
try {
// 通过代理服务器发起请求
await axios.get('https://google.com.hk', { httpsAgent });
logger.mark('[R插件][梯子测试模块] 检测到梯子');
return true;
} catch (error) {
logger.error('[R插件][梯子测试模块] 检测不到梯子');
return false;
}
}
export function formatSeconds(seconds) {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}${remainingSeconds}`;
}