feat: 添加对于网易云网盘操作的支持

This commit is contained in:
秋刀鱼 2024-11-12 11:26:30 +08:00
parent a6dc918727
commit d44bcd6502
8 changed files with 295 additions and 42 deletions

View File

@ -1,15 +1,17 @@
import axios from "axios";
import { formatTime } from '../utils/other.js'
import fs from "node:fs";
import { formatTime, toGBorTB } from '../utils/other.js'
import puppeteer from "../../../lib/puppeteer/puppeteer.js";
import PickSongList from "../model/pick-song.js";
import NeteaseMusicInfo from '../model/neteaseMusicInfo.js'
import { NETEASE_API_CN, NETEASE_SONG_DOWNLOAD, NETEASE_TEMP_API } from "../constants/tools.js";
import { COMMON_USER_AGENT, REDIS_YUNZAI_ISOVERSEA, REDIS_YUNZAI_SONGINFO } from "../constants/constant.js";
import { COMMON_USER_AGENT, REDIS_YUNZAI_ISOVERSEA, REDIS_YUNZAI_SONGINFO, REDIS_YUNZAI_CLOUDSONGLIST } from "../constants/constant.js";
import { downloadAudio } from "../utils/common.js";
import { redisExistKey, redisGetKey, redisSetKey } from "../utils/redis-util.js";
import { checkAndRemoveFile } from "../utils/file.js";
import { sendMusicCard } from "../utils/yunzai-util.js";
import config from "../model/config.js";
import FormData from 'form-data';
export class songRequest extends plugin {
constructor() {
@ -27,9 +29,29 @@ export class songRequest extends plugin {
fnc: "playSong"
},
{
reg: "^#?上传(.*)",
reg: "^#?上传$",
fnc: "upLoad"
},
{
reg: '^#?我的云盘$',
fnc: 'myCloud',
permission: 'master'
},
{
reg: '^#?云盘更新$',
fnc: 'songCloudUpdate',
permission: 'master'
},
{
reg: '^#?上传云盘$',
fnc: 'uploadCloud',
permission: 'master'
},
{
reg: '^#?清除云盘缓存$',
fnc: 'cleanCloudData',
permission: 'master'
}
]
});
this.toolsConfig = config.getConfig("tools");
@ -51,6 +73,8 @@ export class songRequest extends plugin {
this.songRequestMaxList = this.toolsConfig.songRequestMaxList
// 视频保存路径
this.defaultPath = this.toolsConfig.defaultPath;
// uid
this.uid = this.toolsConfig.neteaseUserId
}
async pickSong(e) {
@ -59,6 +83,7 @@ export class songRequest extends plugin {
logger.info('当前未开启网易云点歌')
return false
}
// 获取自定义API
const autoSelectNeteaseApi = await this.pickApi()
// 只在群里可以使用
let group_id = e.group.group_id
@ -68,17 +93,35 @@ export class songRequest extends plugin {
const saveId = songInfo.findIndex(item => item.group_id === e.group.group_id)
let musicDate = { 'group_id': group_id, data: [] }
// 获取搜索歌曲列表信息
let searchUrl = autoSelectNeteaseApi + '/search?keywords={}&limit=' + this.songRequestMaxList //搜索API
let detailUrl = autoSelectNeteaseApi + "/song/detail?ids={}" //歌曲详情API
if (e.msg.replace(/\s+/g, "").match(/点歌(.+)/)) {
const songKeyWord = e.msg.replace(/\s+/g, "").match(/点歌(.+)/)[1]
// 获取云盘歌单列表
const cloudSongList = await this.getCloudSong()
// 搜索云盘歌单并进行搜索
const matchedSongs = await cloudSongList.filter(({ songName, singerName }) =>
songName.includes(songKeyWord) || singerName.includes(songKeyWord)
);
// 计算列表数 计算偏移量
let songListCount = matchedSongs.length >= this.songRequestMaxList ? this.songRequestMaxList : matchedSongs.length
let searchCount = this.songRequestMaxList - songListCount
for (let i = 0; i < songListCount; i++) {
musicDate.data.push({
'id': matchedSongs[i].id,
'songName': matchedSongs[i].songName,
'singerName': matchedSongs[i].singerName,
'duration': matchedSongs[i].duration
});
}
let searchUrl = autoSelectNeteaseApi + '/search?keywords={}&limit=' + searchCount + '&offset=' + songListCount//搜索API
searchUrl = searchUrl.replace("{}", songKeyWord)
await axios.get(searchUrl, {
headers: {
"User-Agent": COMMON_USER_AGENT
},
}).then(async res => {
if (res.data.result.songs) {
if (res.data.result.songs || musicDate.data[0]) {
try {
for (const info of res.data.result.songs) {
musicDate.data.push({
'id': info.id,
@ -87,6 +130,9 @@ export class songRequest extends plugin {
'duration': formatTime(info.duration)
});
}
} catch (error) {
logger.info('并未获取云服务歌曲')
}
const ids = musicDate.data.map(item => item.id).join(',');
detailUrl = detailUrl.replace("{}", ids)
await axios.get(detailUrl, {
@ -103,6 +149,7 @@ export class songRequest extends plugin {
} else {
songInfo[saveId] = musicDate
}
logger.info('当前搜索列表---', songInfo)
await redisSetKey(REDIS_YUNZAI_SONGINFO, songInfo)
const data = await new PickSongList(e).getData(musicDate.data)
let img = await puppeteer.screenshot("pick-song", data);
@ -185,8 +232,46 @@ export class songRequest extends plugin {
}
}
// 获取云盘信息
async myCloud(e) {
const autoSelectNeteaseApi = await this.pickApi()
const cloudUrl = autoSelectNeteaseApi + '/user/cloud'
// 云盘数据API
await axios.get(cloudUrl, {
headers: {
"User-Agent": COMMON_USER_AGENT,
"Cookie": this.neteaseCookie
},
}).then(res => {
const cloudData = {
'songCount': res.data.count,
'useSize': toGBorTB(res.data.size),
'cloudSize': toGBorTB(res.data.maxSize)
}
e.reply(`云盘数据\n歌曲数量:${cloudData.songCount}\n云盘容量:${cloudData.cloudSize}\n已使用容量:${cloudData.useSize}`)
})
}
// 更新云盘
async songCloudUpdate(e) {
try {
await this.cleanCloudData()
await this.getCloudSong(e, true)
try {
await e?.reply('更新成功')
} catch (error) {
logger.error('trss又拉屎了')
}
await this.myCloud(e)
} catch (error) {
logger.error('更新云盘失败', error)
}
}
// 上传音频文件
async upLoad(e) {
let msg = await e.getReply();
let msg = await e?.getReply();
const musicUrlReg = /(http:|https:)\/\/music.163.com\/song\/media\/outer\/url\?id=(\d+)/;
const musicUrlReg2 = /(http:|https:)\/\/y.music.163.com\/m\/song\?(.*)&id=(\d+)/;
const musicUrlReg3 = /(http:|https:)\/\/music.163.com\/m\/song\/(\d+)/;
@ -198,17 +283,139 @@ export class songRequest extends plugin {
const title = msg.message[0].data.match(/"title":"([^"]+)"/)[1]
const desc = msg.message[0].data.match(/"desc":"([^"]+)"/)[1]
if (id === "") return
let path = this.getCurDownloadPath(e) + '/' + title + '-' + desc + '.' + 'flac'
let paths = [
this.getCurDownloadPath(e) + '/' + title + '-' + desc + '.flac',
this.getCurDownloadPath(e) + '/' + title + '-' + desc + '.mp3',
this.getCurDownloadPath(e) + '/' + title + '-' + desc + '.mp4'
];
for (let path of paths) {
try {
// 上传群文件
await this.uploadGroupFile(e, path);
// 删除文件
await checkAndRemoveFile(path);
} catch (error) {
logger.error(error)
logger.error(error);
}
}
}
// 上传云盘
async uploadCloud(e) {
const autoSelectNeteaseApi = await this.pickApi()
let msg = await e?.getReply();
const musicUrlReg = /(http:|https:)\/\/music.163.com\/song\/media\/outer\/url\?id=(\d+)/;
const musicUrlReg2 = /(http:|https:)\/\/y.music.163.com\/m\/song\?(.*)&id=(\d+)/;
const musicUrlReg3 = /(http:|https:)\/\/music.163.com\/m\/song\/(\d+)/;
const id =
musicUrlReg2.exec(msg.message[0].data)?.[3] ||
musicUrlReg.exec(msg.message[0].data)?.[2] ||
musicUrlReg3.exec(msg.message[0].data)?.[2] ||
/(?<!user)id=(\d+)/.exec(msg.message[0].data)[1] || "";
const title = msg.message[0].data.match(/"title":"([^"]+)"/)[1]
const desc = msg.message[0].data.match(/"desc":"([^"]+)"/)[1]
if (id === "") return
let paths = [
this.getCurDownloadPath(e) + '/' + title + '-' + desc + '.flac',
this.getCurDownloadPath(e) + '/' + title + '-' + desc + '.mp3',
this.getCurDownloadPath(e) + '/' + title + '-' + desc + '.mp4'
];
for (let path of paths) {
try {
await formData.append('songFile', fs.createReadStream(path))
} catch (error) {
logger.error(error);
}
}
let formData = new FormData()
const headers = {
...formData.getHeaders(),
'Cookie': this.neteaseCookie,
};
const updateUrl = autoSelectNeteaseApi + `/cloud?time=${Date.now()}`
let tryCount = 0
const tryUpload = () => {
axios({
method: 'post',
url: updateUrl,
headers: headers,
data: formData,
})
.then(async res => {
if (res.data.code == 200) {
let matchUrl = autoSelectNeteaseApi + '/cloud/match?uid=' + this.uid + "&sid=" + res.data.privateCloud.songId + '&asid=' + id
await axios.get(matchUrl, {
headers: {
"User-Agent": COMMON_USER_AGENT,
"Cookie": this.neteaseCookie
},
}).then(res => {
logger.info('歌曲信息匹配成功')
})
.catch(error =>{
logger.error('歌曲信息匹配错误',error)
})
this.songCloudUpdate(e)
}
})
.catch(error => {
tryCount += 1;
logger.info('失败喽~再试一次')
if (tryCount < 3) {
tryUpload(); // 直接调用
} else {
logger.error('怎么想都传不上去吧', error)
}
}
)
};
tryUpload();
}
// 获取云盘歌单
async getCloudSong(e, cloudUpdate = false) {
let songList = await redisGetKey(REDIS_YUNZAI_CLOUDSONGLIST) || []
if (!songList[0] || cloudUpdate) {
const autoSelectNeteaseApi = await this.pickApi();
const limit = 100;
let offset = 0;
let cloudUrl = autoSelectNeteaseApi + `/user/cloud?limit=${limit}&offset=${offset}&timestamp=${Date.now()}`;
while (true) {
try {
const res = await axios.get(cloudUrl, {
headers: {
"User-Agent": COMMON_USER_AGENT,
"Cookie": this.neteaseCookie
}
});
const songs = res.data.data.map(({ simpleSong }) => ({
'songName': simpleSong.name,
'id': simpleSong.id,
'singerName': simpleSong.ar[0].name || '喵喵~',
'duration': '云盘'
}));
songList.push(...songs);
if (!res.data.hasMore) {
break;
}
offset += limit;
cloudUrl = autoSelectNeteaseApi + `/user/cloud?limit=${limit}&offset=${offset}`;
} catch (error) {
console.error("获取歌单失败", error);
break;
}
}
await redisSetKey(REDIS_YUNZAI_CLOUDSONGLIST, songList)
return songList;
} else {
return songList;
}
}
async cleanCloudData(e) {
await redisSetKey(REDIS_YUNZAI_CLOUDSONGLIST, [])
}
// 判断是否海外服务器
async isOverseasServer() {
@ -237,7 +444,6 @@ export class songRequest extends plugin {
}
// 检测cooike活性
async checkCooike(statusUrl) {
let status
await axios.get(statusUrl, {
@ -245,8 +451,9 @@ export class songRequest extends plugin {
"User-Agent": COMMON_USER_AGENT,
"Cookie": this.neteaseCookie
},
}).then(res => {
}).then(async res => {
const userInfo = res.data.data.profile
await config.updateField("tools", "neteaseUserId", res.data.data.profile.userId);
if (userInfo) {
logger.info('ck活着使用ck进行高音质下载')
status = true
@ -306,11 +513,11 @@ export class songRequest extends plugin {
},
}).then(res => {
const wikiData = res.data.data.blocks[1].creatives
try {
typelist.push(wikiData[0].resources[0]?.uiElement?.mainTitle?.title || "")
typelist.push(wikiData[0].resources[0].uiElement.mainTitle.title)
// 防止数据过深出错
const recTags = wikiData[1]
if (recTags?.resources[0]) {
if (recTags.resources[0]) {
for (let i = 0; i < Math.min(3, recTags.resources.length); i++) {
if (recTags.resources[i] && recTags.resources[i].uiElement && recTags.resources[i].uiElement.mainTitle.title) {
typelist.push(recTags.resources[i].uiElement.mainTitle.title)
@ -322,10 +529,7 @@ export class songRequest extends plugin {
if (wikiData[2].uiElement.mainTitle.title == 'BPM') {
typelist.push('BPM ' + wikiData[2].uiElement.textLinks[0].text)
} else {
typelist.push(wikiData[2].uiElement.textLinks[0].text || '')
}
} catch (error) {
logger.error('获取标签报错:', error)
typelist.push(wikiData[2].uiElement.textLinks[0].text)
}
typelist.push(AudioLevel)
})

View File

@ -29,6 +29,7 @@ songRequestMaxList: 10 # 网易云点歌请求最大列表数
neteaseCookie: '' # 网易云ck
neteaseCloudAPIServer: '' # 网易云自建服务器地址
neteaseCloudAudioQuality: exhigh # 网易云解析最高音质 默认exhigh(极高) 分类standard => 标准,higher => 较高, exhigh=>极高, lossless=>无损, hires=>Hi-Res, jyeffect => 高清环绕声, sky => 沉浸环绕声, dolby => 杜比全景声, jymaster => 超清母带
neteaseUserId: '' # 网易云用户ID 不要手动更改!!!!除非你非常清楚你在做什么
youtubeGraphicsOptions: 720 # YouTobe的下载画质0为原画1080720480自定义画面高度默认为720
youtubeClipTime: 0 # YouTobe限制的最大视频时长默认不开启单位秒 最好不要超过5分钟否则截取效率非常低

View File

@ -80,6 +80,11 @@ export const REDIS_YUNZAI_ISOVERSEA = "Yz:rconsole:tools:oversea";
*/
export const REDIS_YUNZAI_SONGINFO = "Yz:rconsole:tools:songinfo";
/**
* 缓存网易云云盘列表
* @type {string}
*/
export const REDIS_YUNZAI_CLOUDSONGLIST = "Yz:rconsole:tools:cloudsonglist";
/**
* 某些功能的解析白名单

View File

@ -4,6 +4,7 @@
"type": "module",
"dependencies": {
"axios": "^1.3.4",
"form-data": "^4.0.1",
"qrcode": "^1.5.3",
"p-queue": "^8.0.1"
}

View File

@ -128,6 +128,10 @@ html {
box-shadow: 0px 0px 3px rgba(255, 255, 255, 0.4);
}
.typeNav:first-child {
margin-left: 0px;
}
.logo{
width: 100%;
text-align: center;

View File

@ -62,11 +62,30 @@ html {
.navDuration {
color: #aaa;
width: 40px;
width: 70px;
font-size: 25px;
height: 100%;
text-align: center;
}
.cloudBox{
height: 100%;
width: 70px;
justify-content: center;
display: flex;
align-items: center;
}
.cloud{
color: #dd001b;
box-sizing: border-box;
padding: 0px 8px;
font-size: 22px;
height: 40px;
width: 70px;
border-radius: 10px;
display: flex;
justify-content: center;
border: 1px solid;
align-items: center;
}
@ -74,6 +93,7 @@ html {
width: 90px;
height: 90px;
border-radius: 8px;
flex-shrink: 0;
}
.bgicon {

View File

@ -22,7 +22,13 @@
<div class="singerText">{{ info.singerName }}</div>
</div>
</div>
{{ if info.duration == '云盘' }}
<div class="cloudBox">
<div class="cloud">{{ info.duration }}</div>
</div>
{{ else }}
<div class="navDuration">{{ info.duration }}</div>
{{ /if }}
</div>
{{ /each }}
<div class="bgicon">

View File

@ -9,3 +9,15 @@ export function formatTime(timestamp) {
return `${formattedMinutes}:${formattedSeconds}`;
}
export function toGBorTB(Bytes) {
const GB = 1024 ** 3;
let sizeInGB = Bytes / GB;
let unit = "GB";
if (sizeInGB > 1024) {
sizeInGB /= 1024;
unit = "TB";
}
sizeInGB = sizeInGB % 1 === 0 ? sizeInGB.toString() : sizeInGB.toFixed(2);
return sizeInGB + unit;
}