diff --git a/apps/songRequest.js b/apps/songRequest.js index 3112c31..9b1a782 100644 --- a/apps/songRequest.js +++ b/apps/songRequest.js @@ -6,12 +6,14 @@ 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, REDIS_YUNZAI_CLOUDSONGLIST } from "../constants/constant.js"; -import { downloadAudio } from "../utils/common.js"; +import { downloadAudio, retryAxiosReq } 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 { sendMusicCard, getGroupFileUrl } from "../utils/yunzai-util.js"; import config from "../model/config.js"; import FormData from 'form-data'; +import NodeID3 from 'node-id3'; +import { isError } from "node:util"; let FileSuffix = 'flac' @@ -53,6 +55,11 @@ export class songRequest extends plugin { reg: '^#?清除云盘缓存$', fnc: 'cleanCloudData', permission: 'master' + }, + { + reg: '^#?获取群文件$', + fnc: 'getLatestDocument', + permission: 'master' } ] }); @@ -95,16 +102,16 @@ 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 detailUrl = autoSelectNeteaseApi + "/song/detail?ids={}" //歌曲详情API + let detailUrl = autoSelectNeteaseApi + "/song/detail?ids={}&time=" + Date.now() //歌曲详情API if (e.msg.replace(/\s+/g, "").match(/点歌(.+)/)) { - const songKeyWord = e.msg.replace(/\s+/g, "").match(/点歌(.+)/)[1] + const songKeyWord = e.msg.replace(/\s+/g, "").match(/点歌(.+)/)[1].replace(/[^\w\u4e00-\u9fa5]/g, '') // 获取云盘歌单列表 const cloudSongList = await this.getCloudSong() // 搜索云盘歌单并进行搜索 const matchedSongs = await cloudSongList.filter(({ songName, singerName }) => - songName.includes(songKeyWord) || singerName.includes(songKeyWord) + songName.includes(songKeyWord) || singerName.includes(songKeyWord) || songName == songKeyWord || singerName == songKeyWord ); - // 计算列表数 计算偏移量 + // 计算列表数 let songListCount = matchedSongs.length >= this.songRequestMaxList ? this.songRequestMaxList : matchedSongs.length let searchCount = this.songRequestMaxList - songListCount for (let i = 0; i < songListCount; i++) { @@ -317,51 +324,47 @@ 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 + '.' + FileSuffix - let tryCount = 0 + let path = this.getCurDownloadPath(e) + '/' + desc + '-' + title + '.' + FileSuffix const tryUpload = async () => { - let formData = new FormData() - await formData.append('songFile', fs.createReadStream(path)) + let formData = new FormData(); + formData.append('songFile', fs.createReadStream(path)); const headers = { ...formData.getHeaders(), 'Cookie': this.neteaseCookie, }; - const updateUrl = autoSelectNeteaseApi + `/cloud?time=${Date.now()}` - 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, { + const updateUrl = `${autoSelectNeteaseApi}/cloud?time=${Date.now()}`; + try { + const res = await axios({ + method: 'post', + url: updateUrl, + headers: headers, + data: formData, + }); + if (res.data.code == 200) { + let matchUrl = `${autoSelectNeteaseApi}/cloud/match?uid=${this.uid}&sid=${res.data.privateCloud.songId}&asid=${id}`; + try { + const matchRes = 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) + }); + logger.info('歌曲信息匹配成功'); + } catch (error) { + logger.error('歌曲信息匹配错误', error); } + this.songCloudUpdate(e); + return res; + + } else { + throw new Error('上传失败,响应不正确'); } - ) + } catch (error) { + throw error; + } }; - tryUpload(); + await retryAxiosReq(() => tryUpload()) + await checkAndRemoveFile(path) } // 获取云盘歌单 @@ -380,10 +383,10 @@ export class songRequest extends plugin { "Cookie": this.neteaseCookie } }); - const songs = res.data.data.map(({ simpleSong }) => ({ - 'songName': simpleSong.name, - 'id': simpleSong.id, - 'singerName': simpleSong.ar[0].name || '喵喵~', + const songs = res.data.data.map(({ songId, songName, artist }) => ({ + 'songName': songName, + 'id': songId, + 'singerName': artist || '喵喵~', 'duration': '云盘' })); songList.push(...songs); @@ -404,6 +407,70 @@ export class songRequest extends plugin { } } + // 群文件上传云盘 + async getLatestDocument(e) { + const autoSelectNeteaseApi = await this.pickApi() + const cleanPath = await getGroupFileUrl(e) + logger.info(cleanPath) + // 拓展名 + const extension = cleanPath.match(/\.\w+$/); + // 获取文件路径 + const dirPath = cleanPath.substring(0, cleanPath.lastIndexOf('/')); + // 获取文件名 + const fileName = cleanPath.split('/').pop().replace(/\.\w+$/, ''); + logger.info(fileName) + const parts = fileName.match(/([\u4e00-\u9fa5a-zA-Z0-9]+)\s*-\s*([\u4e00-\u9fa5a-zA-Z0-9]+)/); + logger.info(parts) + logger.info(parts[1].trim()) + const newFileName = dirPath + '/' + parts[2].trim() + extension + // 进行元数据编辑 + if (parts) { + const tags = { + title: parts[2].trim(), + artist: parts[1].trim() + }; + // 写入元数据 + let success = NodeID3.write(tags, cleanPath) + if (fs.existsSync(newFileName)) { + logger.info(`音频已存在`); + fs.unlinkSync(newFileName); + } + // 文件重命名 + fs.renameSync(cleanPath, newFileName) + if (success) logger.info('写入元数据成功') + } else { + logger.info('未按照标准命名') + } + // 上传请求 + const tryUpload = async () => { + let formData = new FormData() + await formData.append('songFile', fs.createReadStream(newFileName)) + const headers = { + ...formData.getHeaders(), + 'Cookie': this.neteaseCookie, + }; + const updateUrl = autoSelectNeteaseApi + `/cloud?time=${Date.now()}` + try { + const res = await axios({ + method: 'post', + url: updateUrl, + headers: headers, + data: formData, + }); + this.songCloudUpdate(e); + return res; + + } catch (error) { + throw error; + } + }; + // 重试 + await retryAxiosReq(() => tryUpload()) + checkAndRemoveFile(newFileName) + } + + + // 清除缓存 async cleanCloudData(e) { await redisSetKey(REDIS_YUNZAI_CLOUDSONGLIST, []) } @@ -494,7 +561,7 @@ export class songRequest extends plugin { const AudioSize = bytesToMB(resp.data.data?.[0]?.size) // 获取歌曲标题 - let title = songInfo[pickNumber].songName + '-' + songInfo[pickNumber].singerName + let title = songInfo[pickNumber].singerName + '-' + songInfo[pickNumber].songName let typelist = [] // 歌曲百科API await axios.get(songWikiUrl, { @@ -503,24 +570,25 @@ export class songRequest extends plugin { // "Cookie": this.neteaseCookie }, }).then(res => { - const wikiData = res.data.data.blocks[1].creatives - - typelist.push(wikiData[0].resources[0].uiElement.mainTitle.title) - // 防止数据过深出错 - const recTags = wikiData[1] - 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) + const wikiData = res.data.data.blocks[1]?.creatives || [] + if (wikiData[0]) { + typelist.push(wikiData[0].resources[0].uiElement.mainTitle.title) + // 防止数据过深出错 + const recTags = wikiData[1] + 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) + } } + } else { + if (recTags.uiElement.textLinks[0].text) typelist.push(recTags.uiElement.textLinks[0].text) + } + 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) } - } else { - if (recTags.uiElement.textLinks[0].text) typelist.push(recTags.uiElement.textLinks[0].text) - } - 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) } typelist.push(AudioLevel) }) diff --git a/package.json b/package.json index 49fd5d3..8c73196 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "dependencies": { "axios": "^1.3.4", "form-data": "^4.0.1", + "node-id3": "^0.2.6", "qrcode": "^1.5.3", "p-queue": "^8.0.1" } diff --git a/utils/yunzai-util.js b/utils/yunzai-util.js index 88d119e..e0df574 100644 --- a/utils/yunzai-util.js +++ b/utils/yunzai-util.js @@ -34,3 +34,38 @@ export async function sendMusicCard(e, platformType, musicId) { ] }); } + + +/** + * 获取群文件Url地址 + * @param e + * @param count 获取群聊条数 + */ + +export async function getGroupFileUrl(e, count = 10) { + const latestChat = await e.bot.sendApi("get_group_msg_history", { + "group_id": e.group_id, + "count": count + }); + const messages = latestChat.data.messages; + let file_id = ""; + for (let i = messages.length - 1; i >= 0; i--) { + const message = messages?.[i]?.message; + if (message?.[0]?.type === "file") { + file_id = message?.[0].data?.file_id; + break; + } + } + if (file_id === "") { + logger.info('未找到群文件') + return ""; + } + // 获取文件信息 + let latestFileUrl = await e.bot.sendApi("get_group_file_url", { + "group_id": e.group_id, + "file_id": file_id + }); + let path = decodeURIComponent(latestFileUrl.data.url) + const cleanPath = path.startsWith('file:///') ? path.replace('file:///', '') : path; + return cleanPath +} \ No newline at end of file