diff --git a/apps/tools.js b/apps/tools.js index 2503bf9..b624a64 100644 --- a/apps/tools.js +++ b/apps/tools.js @@ -36,7 +36,7 @@ import { BILI_SUMMARY, DY_COMMENT, DY_INFO, - DY_LIVE_INFO, + DY_LIVE_INFO, DY_LIVE_INFO_2, DY_TOUTIAO_INFO, GENERAL_REQ_LINK, HIBI_API_SERVICE, @@ -77,7 +77,7 @@ import { downloadImg, estimateReadingTime, formatBiliInfo, - retryAxiosReq, + retryAxiosReq, saveJsonToFile, secondsToTime, testProxy, truncateString, @@ -99,6 +99,7 @@ import { getDS } from "../utils/mihoyo.js"; import { OpenaiBuilder } from "../utils/openai-builder.js"; import { redisExistKey, redisGetKey, redisSetKey } from "../utils/redis-util.js"; import { saveTDL, startTDL } from "../utils/tdl-util.js"; +import { genVerifyFp } from "../utils/tiktok.js"; import Translate from "../utils/trans-strategy.js"; import { mid2id } from "../utils/weibo.js"; import { ytDlpGetTilt, ytDlpHelper } from "../utils/yt-dlp-util.js"; @@ -232,6 +233,8 @@ export class tools extends plugin { this.myProxy = `http://${ this.proxyAddr }:${ this.proxyPort }`; // 加载识别前缀 this.identifyPrefix = this.toolsConfig.identifyPrefix; + // 加载直播录制时长 + this.streamDuration = this.toolsConfig.streamDuration; // 加载哔哩哔哩配置 this.biliSessData = this.toolsConfig.biliSessData; // 加载哔哩哔哩的限制时长 @@ -317,14 +320,18 @@ export class tools extends plugin { } // 获取链接 let douUrl = urlRex.exec(e.msg.trim())[0]; + let ttwid = ''; if (douUrl.includes("v.douyin.com")) { - douUrl = await this.douyinRequest(douUrl) + const { location, ttwidValue } = await this.douyinRequest(douUrl); + ttwid = ttwidValue; + douUrl = location } // 获取 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]; + /live\/(\d+)/.exec(douUrl)?.[1] || + /webcast.amemv.com\/douyin\/webcast\/reflow\/(\d+)/.exec(douUrl)?.[1]; // 当前版本需要填入cookie if (_.isEmpty(this.douyinCookie) || _.isEmpty(douId)) { e.reply(`检测到没有Cookie 或者 这是一个无效链接,无法解析抖音${ HELP_DOC }`); @@ -340,7 +347,28 @@ export class tools extends plugin { Referer: "https://www.douyin.com/", cookie: this.douyinCookie, }; - const dyApi = douUrl.includes("live") ? DY_LIVE_INFO.replaceAll("{}", douId) : DY_INFO.replace("{}", douId); + 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; + logger.info(item); + const { title, cover, user_count, stream_url } = item; + const dySendContent = `${ this.identifyPrefix }识别:抖音直播,${ 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); + } + logger.info(dyApi); // a-bogus参数 const abParam = aBogus.generate_a_bogus( new URLSearchParams(new URL(dyApi).search).toString(), @@ -355,7 +383,7 @@ export class tools extends plugin { }); // 如果失败进行3次重试 try { - const data = await retryAxiosReq(dyResponse) + const data = await retryAxiosReq(dyResponse); // saveJsonToFile(data); // 直播数据逻辑 if (douUrl.includes("live")) { @@ -364,7 +392,7 @@ export class tools extends plugin { const dySendContent = `${ this.identifyPrefix }识别:抖音直播,${ 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); + 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; @@ -458,7 +486,7 @@ export class tools extends plugin { * @param stream_url * @param second */ - async sendStreamSegment(e, stream_url, second = 10) { + async sendStreamSegment(e, stream_url, second = this.streamDuration) { const outputFilePath = `${ this.getCurDownloadPath(e) }/stream_10s.flv`; await checkAndRemoveFile(outputFilePath); const file = fs.createWriteStream(outputFilePath); @@ -471,13 +499,13 @@ export class tools extends plugin { // 设置 10 秒后停止下载 setTimeout(() => { - logger.info('[R插件][发送直播流] 直播下载10秒钟到,停止下载!'); + logger.info(`[R插件][发送直播流] 直播下载 ${this.streamDuration} 秒钟到,停止下载!`); response.data.destroy(); // 销毁流 e.reply(segment.video(outputFilePath)); file.close(); // 关闭文件流 }, second * 1000); // 10秒 = 10000毫秒 }).catch(error => { - console.error('下载失败:', error.message); + logger.error(`下载失败:${ error.message }`); fs.unlink(outputFilePath, () => { }); // 下载失败时删除文件 }); @@ -2160,11 +2188,28 @@ export class tools extends plugin { timeout: 10000, }; try { - const resp = await axios.head(url, params); + 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); + return resolve({ + location: location, + ttwidValue: ttwidValue + }); } else { return reject("获取失败"); } diff --git a/config/tools.yaml b/config/tools.yaml index 9891ba7..cc46cfe 100644 --- a/config/tools.yaml +++ b/config/tools.yaml @@ -6,6 +6,8 @@ identifyPrefix: '' # 识别前缀,比如你识别哔哩哔哩,那么就有 deeplApiUrls: 'http://www.gptspt.cn/translate,http://gptspt.top/translate,http://8.134.135.4:1188/translate,http://120.76.141.173:1188/translate,http://bit.x7ys.com:1188/translate,http://deeplxapi.x7ys.com:1188/translate' +streamDuration: 10 # 视频最大时长(单位秒) + biliSessData: '' # 哔哩哔哩的SESSDATA biliIntroLenLimit: 50 # 哔哩哔哩简介长度限制,填 0 或者 -1 可以不做任何限制,显示完整简介 biliDuration: 480 # 哔哩哔哩限制的最大视频时长(默认8分钟),单位:秒 diff --git a/config/version.yaml b/config/version.yaml index f8e4610..db6d4b3 100644 --- a/config/version.yaml +++ b/config/version.yaml @@ -1,11 +1,10 @@ - { - version: 1.9.0, + version: 1.9.1, data: [ - 新增rso搜索功能, + 新增直播切片功能, 优化队列下载和GPT功能, 新增哔哩哔哩下载分辨率设置功能, - 新增自定义识别功能, 支持锅巴插件,方便查看和修改配置, 输入#R帮助获取插件帮助, 输入#R更新更新插件, diff --git a/constants/tools.js b/constants/tools.js index 91035a4..2f41a7e 100644 --- a/constants/tools.js +++ b/constants/tools.js @@ -127,6 +127,12 @@ export const DY_TOUTIAO_INFO = "https://aweme.snssdk.com/aweme/v1/play/?video_id */ 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={}"; + /** * X API * @type {string} diff --git a/guoba.support.js b/guoba.support.js index 0ef076c..d87e661 100644 --- a/guoba.support.js +++ b/guoba.support.js @@ -69,6 +69,17 @@ export function supportGuoba() { placeholder: "请输入DeeplX API地址集合", }, }, + { + field: "tools.streamDuration", + label: "解析直播时长", + bottomHelpMessage: + "解析直播(目前涉及哔哩哔哩、抖音)时长,单位:秒(默认:10秒),建议时间为10~60,不然也没人看", + component: "InputNumber", + required: false, + componentProps: { + placeholder: "请输入最大解析直播时长", + }, + }, { field: "tools.defaultPath", label: "视频暂存位置", diff --git a/utils/tiktok.js b/utils/tiktok.js new file mode 100644 index 0000000..7f86e51 --- /dev/null +++ b/utils/tiktok.js @@ -0,0 +1,36 @@ +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(""); +} +