diff --git a/apps/query.js b/apps/query.js index ce6993a..4049d40 100644 --- a/apps/query.js +++ b/apps/query.js @@ -19,6 +19,10 @@ export class query extends plugin { reg: '^#*医药查询 (.*)$', fnc: 'doctor' }, + { + reg: '^#*评分 (.*)', + fnc: 'videoScore' + }, { reg: '^#(cat)$', fnc: 'cat' @@ -77,6 +81,41 @@ export class query extends plugin { return !!this.reply(await Bot.makeForwardMsg(msg)) } + async videoScore(e) { + let keyword = e.msg.split(' ')[1] + const api = `https://movie.douban.com/j/subject_suggest?q=${encodeURI(keyword)}`; + + let movieId = 30433417; + fetch(api, { + Headers: { + "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", + "Content-Type": "application/json", + } + }).then(resp => resp.json()).then(resp => { + if (resp.length === 0 || resp === "") { + e.reply("没找到!"); + return true; + } + movieId = resp[0].id; + const doubanApi = `https://movie.querydata.org/api?id=${movieId}`; + fetch(doubanApi, { + Headers: { + "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", + "Content-Type": "application/json", + } + }).then(resp => resp.json()).then(resp => { + if (resp.length === 0 || resp === "") { + e.reply("没找到!"); + return true; + } + e.reply(`识别:${resp.data[0].name}\n烂番茄评分:${resp.imdbRating}\n豆瓣评分:${resp.doubanRating}\n评分:${resp.imdbRating}`); + }) + }) + return true; + } + async cat (e) { const numb = this.catConfig.count let images = [] @@ -196,13 +235,27 @@ export class query extends plugin { } async buyerShow (e) { - const urls = ['https://api.vvhan.com/api/tao', 'http://3650000.xyz/api/?type=img'] - const randomIndex = Math.floor(Math.random() * urls.length); - const randomElement = urls.splice(randomIndex, 1)[0]; - await fetch(randomElement).then(resp => { - e.reply(segment.image(resp.url)) + // http://3650000.xyz/api/?type=img + // https://api.vvhan.com/api/tao + // https://api.uomg.com/api/rand.img3?format=json + // const randomIndex = Math.floor(Math.random() * urls.length); + // const randomElement = urls.splice(randomIndex, 1)[0]; + const p1 = new Promise((resolve, reject) => { + fetch("https://api.vvhan.com/api/tao").then(resp => { + return resolve(resp.url) + }).catch(err => reject(err)) }) - return true + const p2 = new Promise((resolve, reject) => { + fetch("https://api.uomg.com/api/rand.img3?format=json").then(resp => resp.json()).then(resp => { + return resolve(resp.imgurl) + }).catch(err => reject(err)) + }) + Promise.all([p1, p2]).then(res => { + res.forEach(item => { + e.reply(segment.image(item)) + }) + }) + return true; } // 删除标签 diff --git a/apps/tools.js b/apps/tools.js index 3e5aa4e..9869ae1 100644 --- a/apps/tools.js +++ b/apps/tools.js @@ -11,7 +11,8 @@ import { TwitterApi } from 'twitter-api-v2' import HttpProxyAgent from 'https-proxy-agent' import { mkdirsSync } from '../utils/file.js' import { downloadBFile, getDownloadUrl, mergeFileToMp4 } from '../utils/bilibili.js' -import { get, remove, add } from "../utils/redisu.js"; +import { parseUrl, parseM3u8, downloadM3u8Videos, mergeAcFileToMp4 } from '../utils/acfun.js' +// import { get, remove, add } from "../utils/redisu.js"; const transMap = { "中": "zh", "日": "jp", "文": "wyw", "英": "en" } @@ -46,6 +47,14 @@ export class tools extends plugin { { reg: "(.*)(twitter.com)", fnc: "twitter", + }, + { + reg: "https:\/\/(m.)?v.qq.com\/(.*)", + fnc: "tx" + }, + { + reg: "(.*)(acfun.cn)", + fnc: "acfun" } ], }); @@ -199,24 +208,24 @@ export class tools extends plugin { async wiki (e) { const key = e.msg.replace(/#|百科|wiki/g, "").trim(); const url = `https://xiaoapi.cn/API/bk.php?m=json&type=bd&msg=${ encodeURI(key) }` - const url2 = 'https://api.jikipedia.com/go/auto_complete' + // const url2 = 'https://api.jikipedia.com/go/auto_complete' Promise.all([ - axios.post(url2, { - headers: { - "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", - "Content-Type": "application/json", - }, - timeout: 10000, - "phrase": key, - }) - .then(resp => { - const data = resp.data.data - if (_.isEmpty(data)) { - return data; - } - return data[0].entities[0]; - }), + // axios.post(url2, { + // headers: { + // "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", + // "Content-Type": "application/json", + // }, + // timeout: 10000, + // "phrase": key, + // }) + // .then(resp => { + // const data = resp.data.data + // if (_.isEmpty(data)) { + // return data; + // } + // return data[0].entities[0]; + // }), axios.get(url, { headers: { "User-Agent": @@ -229,13 +238,13 @@ export class tools extends plugin { }) ]) .then(res => { - const data = res[1] - const data2 = res[0] + const data = res[0] + // const data2 = res[0] const template = ` 解释:${ _.get(data, 'msg') }\n 详情:${ _.get(data, 'more') }\n - 小鸡解释:${ _.get(data2, 'content') } `; + // 小鸡解释:${ _.get(data2, 'content') } e.reply(template) }) return true @@ -286,6 +295,27 @@ export class tools extends plugin { return true; } + // 视频解析 + async tx( e ) { + const url = e.msg + const data = await ( await fetch( `https://xian.txma.cn/API/jx_txjx.php?url=${url}` ) ) + .json() + const k = data.url + const name = data.title + if( k && name ) { + e.reply( name + '\n' + k ) + let forward = await this.makeForwardMsg( url ) + e.reply( forward ) + return true + } else { + e.reply( '解析腾讯视频失败~\n去浏览器使用拼接接口吧...' ) + let forward = await this.makeForwardMsg( url ) + e.reply( forward ) + return true + } + } + + // 请求参数 async douyinRequest (url) { const params = { @@ -361,6 +391,32 @@ export class tools extends plugin { return (idVideo.length > 19) ? idVideo.substring(0, idVideo.indexOf("?")) : idVideo; } + // acfun解析 + async acfun(e) { + const path = `${ this.defaultPath }${ this.e.group_id || this.e.user_id }/temp/` + if (!fs.existsSync(path)) { + mkdirsSync(path); + } + + let inputMsg = e.msg; + // 适配手机分享:https://m.acfun.cn/v/?ac=32838812&sid=d2b0991bd6ad9c09 + if (inputMsg.includes("m.acfun.cn")) { + inputMsg = `https://www.acfun.cn/v/ac${/ac=([^&?]*)/.exec(inputMsg)[1]}` + } + + parseUrl(inputMsg).then(res => { + e.reply(`识别:猴山,${res.videoName}`) + parseM3u8(res.urlM3u8s[res.urlM3u8s.length-1]).then(res2 => { + downloadM3u8Videos(res2.m3u8FullUrls, path).then(_ => { + mergeAcFileToMp4( res2.tsNames, path, `${path}out.mp4`).then(_ => { + e.reply(segment.video(`${path}out.mp4`)) + }) + }) + }) + }) + return true; + } + // 工具:下载哔哩哔哩 async downBili (title, videoUrl, audioUrl) { return Promise.all([ diff --git a/config/help.yaml b/config/help.yaml index 6760ba1..de0daf7 100644 --- a/config/help.yaml +++ b/config/help.yaml @@ -15,6 +15,9 @@ - icon: android title: "#安卓软件推荐" desc: 推荐安卓软件 + - icon: buyer + title: "#买家秀" + desc: 淘宝买家秀 - group: 神秘功能合集 list: - icon: pic1 @@ -46,6 +49,9 @@ - icon: bilibili title: "bilibili/b23" desc: 哔哩哔哩分享实时下载 + - icon: 推特 + title: "bilibili/b23" + desc: 推特学习版分享实时下载 - group: 其他指令[实验] list: - icon: update diff --git a/resources/img/icon/buyer.png b/resources/img/icon/buyer.png new file mode 100644 index 0000000..528b5e9 Binary files /dev/null and b/resources/img/icon/buyer.png differ diff --git a/resources/img/icon/推特.png b/resources/img/icon/推特.png new file mode 100644 index 0000000..9becaae Binary files /dev/null and b/resources/img/icon/推特.png differ diff --git a/utils/acfun.js b/utils/acfun.js new file mode 100644 index 0000000..ab129c5 --- /dev/null +++ b/utils/acfun.js @@ -0,0 +1,164 @@ +import axios from 'axios' +import fs from 'node:fs' +import path from 'path' +import child_process from 'node:child_process' + +/** + * 去除JSON的一些转义 \\" -> \" ->" + * @param str + */ +function escapeSpecialChars(str) { + return str.replace(/\\\\"/g, '\\"').replace(/\\"/g, '"'); +} + +const parseVideoName = (videoInfo) => { + const strAc号 = "ac" + (videoInfo?.dougaId || ""); + const str标题 = videoInfo?.title; + const str作者 = videoInfo?.user.name; + const str上传时间 = videoInfo?.createTime; + const str描述 = videoInfo?.description; + + const raw = [strAc号, str标题, str作者, str上传时间, str描述] + .map((d) => d || "") + .join("_") + .slice(0, 100); + + return raw; +}; + +const parseVideoNameFixed = (videoInfo) => { + const f = parseVideoName(videoInfo); + const t = f.replaceAll(" ", "-"); + return t; +}; + +async function parseUrl(videoUrlAddress) { + // eg https://www.acfun.cn/v/ac4621380?quickViewId=videoInfo_new&ajaxpipe=1 + const urlSuffix = "?quickViewId=videoInfo_new&ajaxpipe=1"; + const url = videoUrlAddress + urlSuffix; + + const raw = await axios.get(url).then(resp => { + return resp.data + }); + // Split + const strsRemoveHeader = raw.split("window.pageInfo = window.videoInfo ="); + const strsRemoveTail = strsRemoveHeader[1].split(""); + const strJson = strsRemoveTail[0]; + + const strJsonEscaped = escapeSpecialChars(strJson); + /** Object videoInfo */ + const videoInfo = JSON.parse(strJsonEscaped); + + const videoName = parseVideoNameFixed(videoInfo); + + const ksPlayJson = videoInfo.currentVideoInfo.ksPlayJson; + /** Object ksPlay */ + const ksPlay = JSON.parse(ksPlayJson); + + const representations = ksPlay.adaptationSet[0].representation; + const urlM3u8s = representations.map((d) => d.url); + + return { urlM3u8s, videoName }; +} + +async function parseM3u8(m3u8Url) { + const m3u8File = await axios.get(m3u8Url).then(resp => resp.data); + + /** 分离ts文件链接 */ + const rawPieces = m3u8File.split(/\n#EXTINF:.{8},\n/); + /** 过滤头部 */ + const m3u8RelativeLinks = rawPieces.slice(1); + /** 修改尾部 去掉尾部多余的结束符 */ + const patchedTail = + m3u8RelativeLinks[m3u8RelativeLinks.length - 1].split("\n")[0]; + m3u8RelativeLinks[m3u8RelativeLinks.length - 1] = patchedTail; + + /** 完整链接,直接加m3u8Url的通用前缀 */ + const m3u8Prefix = m3u8Url.split("/").slice(0, -1).join("/"); + const m3u8FullUrls = m3u8RelativeLinks.map((d) => m3u8Prefix + "/" + d); + /** aria2c下载的文件名,就是取url最后一段,去掉末尾url参数(?之后是url参数) */ + const tsNames = m3u8RelativeLinks.map((d) => d.split("?")[0]); + /** 文件夹名,去掉文件名末尾分片号 */ + let outputFolderName = tsNames[0].slice(0, -9); + /** 输出最后合并的文件名,加个通用mp4后缀 */ + const outputFileName = outputFolderName + ".mp4"; + + return { + m3u8FullUrls, + tsNames, + outputFolderName, + outputFileName, + }; +} + +// 下载m3u8 +async function downloadM3u8Videos( + m3u8FullUrls, + outputFolderName +) { + /** 新建下载文件夹 在当前运行目录下 */ + const outPath = outputFolderName; + + /** 批下载 */ + const strDownloadParamFiles = m3u8FullUrls + .map(async (d, i) => { + + return new Promise((resolve, reject) => { + const writer = fs.createWriteStream(outPath + `${i}.ts`); + axios.get(d, { + headers: { + "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", + }, + responseType: "stream", + }).then(dres => { + dres.data.pipe(writer); + writer.on("finish", () => resolve(true)); + writer.on("error", () => reject); + }); + }) + + }) + /** 写入下载链接列表文件 */ + // fs.writeFileSync(path.resolve(outPath, "urls.txt"), str下载参数文件); + return Promise.all(strDownloadParamFiles); +} + + +function mergeAcFileToMp4(tsNames, FullFileName, outputFileName, shouldDelete = true) { + + /** 合并参数列表 格式file path */ + const concatStrs = tsNames.map( + (d, i) => `file ${path.resolve(FullFileName, i + ".ts").replace(/\\/g, "/")}` + ); + + const ffmpegList = path.resolve(FullFileName, 'file.txt'); + fs.writeFileSync(ffmpegList, concatStrs.join("\n")); + const outPath = path.resolve(outputFileName); + + let cmd = 'ffmpeg'; + const env = { + ...process.env, + PATH: '/usr/local/bin:' + child_process.execSync('echo $PATH').toString(), + }; + + return new Promise((resolve, reject) => { + child_process.exec( + `${ cmd } -y -f concat -safe 0 -i "${ ffmpegList }" -c copy "${ outPath }"`, + { env }, + err => { + if (shouldDelete) { + fs.unlink(FullFileName, f => f); + } + + if (err) { + reject(err); + } + + resolve({ outputFileName }); + }, + ); + }); +} + +export { parseUrl, parseM3u8, downloadM3u8Videos, mergeAcFileToMp4 } \ No newline at end of file diff --git a/utils/file.js b/utils/file.js index 606563f..9404719 100644 --- a/utils/file.js +++ b/utils/file.js @@ -27,4 +27,22 @@ function mkdirs (dirname, callback) { }); } -export { mkdirs, mkdirsSync } \ No newline at end of file +/** + * 删除文件夹下所有问价及将文件夹下所有文件清空 + * @param {*} path + */ +function emptyDir(path) { + const files = fs.readdirSync(path); + files.forEach(file => { + const filePath = `${path}/${file}`; + const stats = fs.statSync(filePath); + if (stats.isDirectory()) { + emptyDir(filePath); + } else { + fs.unlinkSync(filePath); + console.log(`删除${file}文件成功`); + } + }); +} + +export { mkdirs, mkdirsSync, emptyDir } \ No newline at end of file