🐞 fix: V1.3.3 修复bili音乐逻辑 (现在即可使用命令:bili音乐+链接,下载音乐啦!)

1. 将m4s发送转换为mp3发送格式
2. 提升了bili解析的扩展性
3. 整理了path获取的方式
This commit is contained in:
zhiyu 2024-02-08 00:23:47 +08:00
parent 9e44ced5f5
commit 2f794fd80b
5 changed files with 246 additions and 134 deletions

View File

@ -6,35 +6,54 @@ import { Buffer } from 'node:buffer';
import axios from "axios";
import _ from "lodash";
import tunnel from "tunnel";
import HttpProxyAgent, { HttpsProxyAgent } from "https-proxy-agent";
import { mkdirIfNotExists, checkAndRemoveFile, deleteFolderRecursive } from "../utils/file.js";
import { downloadBFile, getAudioUrl, getDownloadUrl, mergeFileToMp4 } from "../utils/bilibili.js";
import { parseUrl, parseM3u8, downloadM3u8Videos, mergeAcFileToMp4 } from "../utils/acfun.js";
import HttpProxyAgent from "https-proxy-agent";
import { checkAndRemoveFile, deleteFolderRecursive, mkdirIfNotExists } from "../utils/file.js";
import {
downloadBFile,
getBiliAudio,
getDownloadUrl,
getDynamic,
getVideoInfo,
m4sToMp3,
mergeFileToMp4
} from "../utils/bilibili.js";
import { downloadM3u8Videos, mergeAcFileToMp4, parseM3u8, parseUrl } from "../utils/acfun.js";
import {
transMap,
douyinTypeMap,
DIVIDING_LINE,
XHS_NO_WATERMARK_HEADER,
douyinTypeMap,
REDIS_YUNZAI_ISOVERSEA,
transMap,
TWITTER_BEARER_TOKEN,
XHS_NO_WATERMARK_HEADER,
} from "../constants/constant.js";
import { containsChinese, formatBiliInfo, getIdVideo, secondsToTime } from "../utils/common.js";
import config from "../model/index.js";
import Translate from "../utils/trans-strategy.js";
import * as xBogus from "../utils/x-bogus.cjs";
import { getVideoInfo, getDynamic } from "../utils/biliInfo.js";
import { getBodianAudio, getBodianMv, getBodianMusicInfo } from "../utils/bodian.js";
import { getBodianAudio, getBodianMusicInfo, getBodianMv } from "../utils/bodian.js";
import { av2BV } from "../utils/bilibili-bv-av-convert.js";
import querystring from "querystring";
import TokenBucket from "../utils/token-bucket.js";
import { getWbi } from "../utils/biliWbi.js";
import { BILI_SUMMARY, DY_INFO, TIKTOK_INFO, TWITTER_TWEET_INFO, XHS_REQ_LINK } from "../constants/tools.js";
import { XHS_VIDEO } from "../constants/tools.js";
import { BILI_SUMMARY, DY_INFO, TIKTOK_INFO, TWITTER_TWEET_INFO, XHS_REQ_LINK, XHS_VIDEO } from "../constants/tools.js";
import child_process from 'node:child_process'
import { getAudio, getVideo } from "../utils/y2b.js";
import { processTikTokUrl } from "../utils/tiktok.js";
export class tools extends plugin {
/**
* 构造安全的命令
* @type {{existsPromptKey: string, existsTransKey: string}}
*/
static Constants = {
existsTransKey: Object.keys(transMap).join("|"),
};
/**
* 构造令牌桶防止解析致使服务器宕机默认限制5s调用一次
* @type {TokenBucket}
*/
static #tokenBucket = new TokenBucket(1, 1, 5);
constructor() {
super({
name: "R插件工具和学习类",
@ -197,9 +216,7 @@ export class tools extends plugin {
"http",
"https",
);
const path = `${ this.defaultPath }${
this.e.group_id || this.e.user_id
}/temp.mp4`;
const path = `${ this.getCurDownloadPath(e) }/temp.mp4`;
await this.downloadVideo(resUrl).then(() => {
e.reply(segment.video(path));
});
@ -257,7 +274,7 @@ export class tools extends plugin {
this.downloadVideo(data.video.play_addr.url_list[0], !isOversea).then(video => {
e.reply(
segment.video(
`${ this.defaultPath }${ this.e.group_id || this.e.user_id }/temp.mp4`,
`${ this.getCurDownloadPath(e) }/temp.mp4`,
),
);
});
@ -296,7 +313,7 @@ export class tools extends plugin {
}
// 只提取音乐处理
if (e.msg !== undefined && e.msg.includes("bili音乐")) {
await this.biliMusic(url, e);
await this.biliMusic(e, url);
return true;
}
// 动态处理
@ -346,7 +363,7 @@ export class tools extends plugin {
}
// 创建文件,如果不存在
const path = `${ this.defaultPath }${ this.e.group_id || this.e.user_id }/`;
const path = `${ this.getCurDownloadPath(e) }/`;
await mkdirIfNotExists(path);
// 下载文件
getDownloadUrl(url)
@ -367,9 +384,13 @@ export class tools extends plugin {
return true;
}
async biliMusic(url, e) {
const { audioUrl } = await getAudioUrl(url);
e.reply(segment.record(audioUrl))
// 下载哔哩哔哩音乐
async biliMusic(e, url) {
const videoId = /video\/[^\?\/ ]+/.exec(url)[0].split("/")[1];
getBiliAudio(videoId, "").then(async audioUrl => {
const path = this.getCurDownloadPath(e);
e.reply(segment.record(await m4sToMp3(audioUrl, path)));
})
return true
}
@ -399,6 +420,8 @@ export class tools extends plugin {
return url;
}
// 小蓝鸟解析:停止更新
/**
* 哔哩哔哩总结
* @author zhiyu1998
@ -492,7 +515,6 @@ export class tools extends plugin {
return true;
}
// 小蓝鸟解析:停止更新
// 例子https://twitter.com/chonkyanimalx/status/1595834168000204800
async twitter(e) {
// 配置参数及解析
@ -512,14 +534,14 @@ export class tools extends plugin {
await fetch(TWITTER_TWEET_INFO.replace("{}", id), {
headers: {
"User-Agent": "v2TweetLookupJS",
"authorization": `Bearer ${Buffer.from(TWITTER_BEARER_TOKEN, "base64").toString()}`
"authorization": `Bearer ${ Buffer.from(TWITTER_BEARER_TOKEN, "base64").toString() }`
},
...params,
agent: !isOversea ? '' : new HttpProxyAgent(this.myProxy),
}).then(async resp => {
logger.info(resp)
e.reply(`识别:小蓝鸟学习版,${ resp.data.text }`);
const downloadPath = `${ this.defaultPath }${ this.e.group_id || this.e.user_id }`;
const downloadPath = `${ this.getCurDownloadPath(e) }`;
// 创建文件夹(如果没有过这个群)
if (!fs.existsSync(downloadPath)) {
mkdirsSync(downloadPath);
@ -569,7 +591,7 @@ export class tools extends plugin {
// acfun解析
async acfun(e) {
const path = `${ this.defaultPath }${ this.e.group_id || this.e.user_id }/temp/`;
const path = `${ this.getCurDownloadPath(e) }/temp/`;
await mkdirIfNotExists(path);
let inputMsg = e.msg;
@ -619,9 +641,9 @@ export class tools extends plugin {
} else {
id = /explore\/(\w+)/.exec(msgUrl)?.[1] || /discovery\/item\/(\w+)/.exec(msgUrl)?.[1];
}
const downloadPath = `${ this.defaultPath }${ this.e.group_id || this.e.user_id }`;
const downloadPath = `${ this.getCurDownloadPath(e) }`;
// 获取信息
fetch(`${XHS_REQ_LINK}${ id }`, {
fetch(`${ XHS_REQ_LINK }${ id }`, {
headers: XHS_NO_WATERMARK_HEADER,
}).then(async resp => {
const xhsHtml = await resp.text();
@ -641,7 +663,7 @@ export class tools extends plugin {
this.downloadVideo(xhsVideoUrl).then(path => {
if (path === undefined) {
// 创建文件,如果不存在
path = `${ this.defaultPath }${ this.e.group_id || this.e.user_id }/`;
path = `${ this.getCurDownloadPath(e) }/`;
}
e.reply(segment.video(path + "/temp.mp4"));
});
@ -730,7 +752,7 @@ export class tools extends plugin {
const API = `https://imginn.com/${ suffix }`;
// logger.info(API);
let imgPromise = [];
const downloadPath = `${ this.defaultPath }${ this.e.group_id || this.e.user_id }`;
const downloadPath = `${ this.getCurDownloadPath(e) }`;
// 判断是否是海外服务器
const isOversea = await this.isOverseasServer();
// 简单封装图片下载
@ -813,7 +835,7 @@ export class tools extends plugin {
segment.image(albumPic120),
]);
if (e.msg.includes("musicId")) {
const path = `${ this.defaultPath }${ this.e.group_id || this.e.user_id }`;
const path = `${ this.getCurDownloadPath(e) }`;
await getBodianAudio(id, path).then(_ => {
Bot.acquireGfs(e.group_id).upload(
fs.readFileSync(path + "/temp.mp3"),
@ -968,12 +990,12 @@ export class tools extends plugin {
const format = `${ bestVideo.id }x${ bestAudio.id }`
// 下载地址格式化
const path = `${ v }${ p ? `/p${ p }` : '' }`;
const fullpath = `${ this.defaultPath }${ this.e.group_id || this.e.user_id }/${ path }`;
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', '+') } ` +
`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 {
@ -1099,6 +1121,15 @@ export class tools extends plugin {
}
}
/**
* 获取当前发送人/群的下载路径
* @param e Yunzai 机器人事件
* @returns {string}
*/
getCurDownloadPath(e) {
return `${ this.defaultPath }${ e.group_id || e.user_id }`
}
/**
* 提取视频下载位置
* @returns {{groupPath: string, target: string}}
@ -1208,18 +1239,4 @@ export class tools extends plugin {
logger.warn(`解析被限制使用`);
}
}
/**
* 构造安全的命令
* @type {{existsPromptKey: string, existsTransKey: string}}
*/
static Constants = {
existsTransKey: Object.keys(transMap).join("|"),
};
/**
* 构造令牌桶防止解析致使服务器宕机默认限制5s调用一次
* @type {TokenBucket}
*/
static #tokenBucket = new TokenBucket(1, 1, 5);
}

View File

@ -1,5 +1,5 @@
- {
version: 1.3.2,
version: 1.3.3,
data:
[
新增<span class="cmd">油管解析</span>功能,

View File

@ -5,6 +5,27 @@
*/
export const BILI_SUMMARY = "https://api.bilibili.com/x/web-interface/view/conclusion/get"
/**
* 视频流URL
* https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/videostream_url.md
* @type {string}
*/
export const BILI_PLAY_STREAM = "https://api.bilibili.com/x/player/playurl?cid={cid}&bvid={bvid}&qn=64&fnval=16"
/**
* 动态信息
* https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/dynamic/content.md
* @type {string}
*/
export const BILI_DYNAMIC = "https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/get_dynamic_detail?dynamic_id={}"
/**
* BVID -> CID
* https://github.com/SocialSisterYi/bilibili-API-collect/blob/33bde6f6afcac2ff8c6f7069f08ce84065a6cff6/docs/video/info.md?plain=1#L4352
* @type {string}
*/
export const BILI_BVID_TO_CID = "https://api.bilibili.com/x/player/pagelist?bvid={bvid}&jsonp=jsonp"
/**
* 视频基本信息API
* https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/info.md

View File

@ -1,57 +0,0 @@
import fetch from "node-fetch";
import axios from "axios";
import { BILI_VIDEO_INFO } from "../constants/tools.js";
async function getVideoInfo(url) {
// const baseVideoInfo = "http://api.bilibili.com/x/web-interface/view";
const videoId = /video\/[^\?\/ ]+/.exec(url)[0].split("/")[1];
// 获取视频信息,然后发送
return fetch(`${BILI_VIDEO_INFO}?bvid=${videoId}`)
.then(async resp => {
const respJson = await resp.json();
const respData = respJson.data;
return {
title: respData.title,
pic: respData.pic,
desc: respData.desc,
duration: respData.duration,
dynamic: respJson.data.dynamic,
stat: respData.stat,
bvid: respData.bvid,
aid: respData.aid,
cid: respData.pages?.[0].cid,
owner: respData.owner,
pages: respData?.pages,
};
});
}
async function getDynamic(dynamicId) {
const dynamicApi = `https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/get_dynamic_detail?dynamic_id=${dynamicId}`
return axios.get(dynamicApi, {
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36',
'referer': 'https://www.bilibili.com',
}
}).then(resp => {
const dynamicData = resp.data.data.card
const card = JSON.parse(dynamicData.card)
const dynamicOrigin = card.item
const dynamicDesc = dynamicOrigin.description
const pictures = dynamicOrigin.pictures
let dynamicSrc = []
for (let pic of pictures) {
const img_src = pic.img_src
dynamicSrc.push(img_src)
}
// console.log(dynamic_src)
return {
dynamicSrc,
dynamicDesc
}
})
}
export { getVideoInfo, getDynamic };

View File

@ -2,8 +2,17 @@ import fs from "node:fs";
import axios from 'axios'
import child_process from 'node:child_process'
import util from "util";
import { BILI_BVID_TO_CID, BILI_DYNAMIC, BILI_PLAY_STREAM, BILI_VIDEO_INFO } from "../constants/tools.js";
import { mkdirIfNotExists } from "./file.js";
async function downloadBFile (url, fullFileName, progressCallback) {
/**
* 下载单个bili文件
* @param url
* @param fullFileName
* @param progressCallback
* @returns {Promise<any>}
*/
export async function downloadBFile(url, fullFileName, progressCallback) {
return axios
.get(url, {
responseType: 'stream',
@ -35,7 +44,12 @@ async function downloadBFile (url, fullFileName, progressCallback) {
});
}
async function getDownloadUrl (url) {
/**
* 获取下载链接
* @param url
* @returns {Promise<any>}
*/
export async function getDownloadUrl(url) {
return axios
.get(url, {
headers: {
@ -65,34 +79,15 @@ async function getDownloadUrl (url) {
});
}
async function getAudioUrl (url) {
return axios
.get(url, {
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36',
referer: 'https://www.bilibili.com',
},
})
.then(({ data }) => {
const info = JSON.parse(
data.match(/<script>window\.__playinfo__=({.*})<\/script><script>/)?.[1],
);
// 获取音频
const audioUrl =
info?.data?.dash?.audio?.[0]?.baseUrl ?? info?.data?.dash?.audio?.[0]?.backupUrl?.[0];
const title = data.match(/title="(.*?)"/)?.[1]?.replaceAll?.(/\\|\/|:|\*|\?|"|<|>|\|/g, '');
if (audioUrl) {
return { audioUrl, title };
}
return Promise.reject('获取下载地址失败');
});
}
async function mergeFileToMp4 (vFullFileName, aFullFileName, outputFileName, shouldDelete = true) {
/**
* 合并视频和音频
* @param vFullFileName
* @param aFullFileName
* @param outputFileName
* @param shouldDelete
* @returns {Promise<{outputFileName}>}
*/
export async function mergeFileToMp4(vFullFileName, aFullFileName, outputFileName, shouldDelete = true) {
// 判断当前环境
let env;
if (process.platform === "win32") {
@ -122,4 +117,140 @@ async function mergeFileToMp4 (vFullFileName, aFullFileName, outputFileName, sho
}
}
export { downloadBFile, getDownloadUrl, getAudioUrl, mergeFileToMp4 }
/**
* 下载m4s文件通过ffmpeg转换成mp3
* @param m4sUrl
* @returns {Promise<void>}
*/
export async function m4sToMp3(m4sUrl, path) {
return axios
.get(m4sUrl, {
responseType: 'stream',
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36',
referer: 'https://www.bilibili.com',
},
}).then(async res => {
// 如果没有目录就创建一个
await mkdirIfNotExists(path)
// 补充保存文件名
path += "/temp.m4s";
if (fs.existsSync(path)) {
fs.unlinkSync(path);
}
// 开始下载
const fileStream = fs.createWriteStream(path);
res.data.pipe(fileStream);
// 下载完成
return new Promise((resolve, reject) => {
fileStream.on("finish", () => {
fileStream.close(() => {
const transformCmd = `ffmpeg -i ${ path } ${ path.replace(".m4s", ".mp3") } -y -loglevel quiet`;
child_process.execSync(transformCmd)
logger.mark("bili: mp3下载完成")
resolve(path);
});
});
fileStream.on("error", err => {
fs.unlink(path, () => {
reject(err);
});
});
});
});
}
/**
* 哔哩哔哩音乐下载
* @param bvid BVID
* @param cid 选项CID
* @returns {Promise<any>}
*/
export async function getBiliAudio(bvid, cid) {
// 转换cid
if (!cid)
cid = await fetchCID(bvid).catch((err) => console.log(err))
// 返回一个fetch的promise
return (new Promise((resolve, reject) => {
fetch(BILI_PLAY_STREAM.replace("{bvid}", bvid).replace("{cid}", cid))
.then(res => res.json())
.then(json => resolve(json.data.dash.audio[0].baseUrl));
}))
}
/**
* bvid转换成cid
* @param bvid
* @returns {Promise<*>}
*/
export const fetchCID = async (bvid) => {
//console.log('Data.js Calling fetchCID:' + URL_BVID_TO_CID.replace("{bvid}", bvid))
const res = await fetch(BILI_BVID_TO_CID.replace("{bvid}", bvid))
const json = await res.json()
const cid = json.data[0].cid
return cid
}
/**
* 获取视频信息
* @param url
* @returns {Promise<{duration: *, owner: *, bvid: *, stat: *, pages: *, dynamic: *, pic: *, title: *, aid: *, desc: *, cid: *}>}
*/
export async function getVideoInfo(url) {
// const baseVideoInfo = "http://api.bilibili.com/x/web-interface/view";
const videoId = /video\/[^\?\/ ]+/.exec(url)[0].split("/")[1];
// 获取视频信息,然后发送
return fetch(`${ BILI_VIDEO_INFO }?bvid=${ videoId }`)
.then(async resp => {
const respJson = await resp.json();
const respData = respJson.data;
return {
title: respData.title,
pic: respData.pic,
desc: respData.desc,
duration: respData.duration,
dynamic: respJson.data.dynamic,
stat: respData.stat,
bvid: respData.bvid,
aid: respData.aid,
cid: respData.pages?.[0].cid,
owner: respData.owner,
pages: respData?.pages,
};
});
}
/**
* 获取动态
* @param dynamicId
* @returns {Promise<any>}
*/
export async function getDynamic(dynamicId) {
const dynamicApi = BILI_DYNAMIC.replace("{}", dynamicId);
return axios.get(dynamicApi, {
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36',
'referer': 'https://www.bilibili.com',
}
}).then(resp => {
const dynamicData = resp.data.data.card
const card = JSON.parse(dynamicData.card)
const dynamicOrigin = card.item
const dynamicDesc = dynamicOrigin.description
const pictures = dynamicOrigin.pictures
let dynamicSrc = []
for (let pic of pictures) {
const img_src = pic.img_src
dynamicSrc.push(img_src)
}
// console.log(dynamic_src)
return {
dynamicSrc,
dynamicDesc
}
})
}