From bddfa2f1483cf78b0170b52436bc8144176bcfcb Mon Sep 17 00:00:00 2001 From: zhiyu1998 Date: Tue, 14 Mar 2023 21:15:57 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9C=A8=20feat:=20=E7=BD=91=E6=98=93?= =?UTF-8?q?=E4=BA=91=E5=8A=9F=E8=83=BD=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/neteasepro.js | 138 +++++++++++++++++++++++++++++++++++++++++++++ utils/encrypt.js | 57 +++++++++++++++++++ utils/netease.js | 87 ++++++++++++++++++++++++++++ 3 files changed, 282 insertions(+) create mode 100644 apps/neteasepro.js create mode 100644 utils/encrypt.js create mode 100644 utils/netease.js diff --git a/apps/neteasepro.js b/apps/neteasepro.js new file mode 100644 index 0000000..02b6f21 --- /dev/null +++ b/apps/neteasepro.js @@ -0,0 +1,138 @@ +import plugin from "../../../lib/plugins/plugin.js"; +import axios from "axios"; +import fs from 'node:fs'; +import {segment} from "oicq"; +import {getQrCode, getKey, getLoginStatus, getDailyRecommend, getCookies} from '../utils/netease.js'; +import {ha12store, store2ha1} from '../utils/encrypt.js'; + + +export class neteasepro extends plugin { + constructor() { + super({ + /** 功能名称 */ + name: 'R插件网易云音乐解析', + /** 功能描述 */ + dsc: '网易云音乐解析Pro', + /** https://oicqjs.github.io/oicq/#events */ + event: 'message', + /** 优先级,数字越小等级越高 */ + priority: 1, + rule: [ + { + /** 命令正则匹配 */ + reg: '#网易云登陆', + /** 执行方法 */ + fnc: 'neteaseCloudLogin' + }, + { + reg: '#网易云每日推荐', + fnc: 'neteaseDailyRecommend' + } + ] + }) + } + + async neteaseCloudLogin(e) { + let userInfo; + // 如果不存在cookie + if (!await redis.exists(await this.getRedisKey(e.user_id))) { + // 获取密钥 + const key = await getKey(); + // console.log(key); + // 获取二维码 + const qrPic = await getQrCode(key); + // 下载qrcode + await this.downloadQrCode(qrPic).then(path => { + // 发送二维码 + e.reply(segment.image(fs.readFileSync(path))) + }) + // 定时轮询 + await this.poll(key).then(async cookie => { + // 存放到redis + await redis.set(await this.getRedisKey(e.user_id), ha12store(cookie)) + }); + } + // 从redis中获取 + const realCookie = await store2ha1(await redis.get(await this.getRedisKey(e.user_id))); + // 获取用户信息 + userInfo = await getLoginStatus(realCookie); + // 提取信息 + const {nickname, avatarUrl} = userInfo.profile; + e.reply(["欢迎使用 🎶网易云音乐 🎶," + nickname, segment.image(avatarUrl)]) + } + + async neteaseDailyRecommend(e) { + const realCookie = await this.aopBefore(e); + if (realCookie === "") { + return true; + } + // 获取每日推荐所有数据 + const dailyRecommend = await getDailyRecommend(realCookie); + // 由于数据过大,取前10 + let combineMsg = [] + dailyRecommend.dailySongs.slice(0, 10).forEach(item => { + combineMsg.push([`${item?.id}: ${item?.name}-${item?.ar?.[0].name}-${item?.al?.name}`, segment.image(item?.al?.picUrl)]) + }) + await e.reply(await Bot.makeForwardMsg(combineMsg)); + } + + // 切面方法检测cookie + async aopBefore(e) { + // 取出cookie + const cookie = await redis.get(await this.getRedisKey(e.user_id)); + // 如果不存在cookie + if (!cookie) { + e.reply("请先#网易云登录"); + return ""; + } + // 解析cookie + return store2ha1(cookie); + } + + // 下载二维码 + async downloadQrCode(qrPic) { + return axios + .get(qrPic, { + 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((resp) => { + const filepath = "./netease_qr.jpg"; + const writer = fs.createWriteStream(filepath); + resp.data.pipe(writer); + return new Promise((resolve, reject) => { + writer.on("finish", () => resolve(filepath)); + writer.on("error", reject); + }); + }); + } + + // 定时轮询 + async poll(key) { + let timer; + return new Promise((resolve, reject) => { + timer = setInterval(async () => { + const statusRes = await getCookies(key) + console.log(statusRes) + if (statusRes.code === 800) { + clearInterval(timer) + reject('二维码已过期,请重新获取') + } + if (statusRes.code === 803) { + // 这一步会返回cookie + clearInterval(timer) + const cookie = statusRes.cookie; + resolve(/__csrf=[0-9a-z]+;/.exec(cookie)[0] + /MUSIC_U=[0-9a-z]+;/.exec(cookie)[0]); + } + }, 3000) + }); + } + + // 获取redis的key + async getRedisKey(user_id) { + return `Yz:rconsole:netease:${user_id}`; + } +} \ No newline at end of file diff --git a/utils/encrypt.js b/utils/encrypt.js new file mode 100644 index 0000000..49d2894 --- /dev/null +++ b/utils/encrypt.js @@ -0,0 +1,57 @@ +// AES加密 +import crypto from "crypto"; + +const key = crypto.createHash("sha256").update("rconsole").digest(); + +/** + * AES解密 + * @param ha1 + * @returns {Promise} + */ +async function ha12store(ha1) { + // IV.E + const iv = crypto.randomBytes(16); + const c = crypto.createCipheriv("aes-256-cbc", key, iv); + let e = c.update(ha1, "binary", "hex"); + e += c.final("hex"); + return iv.toString("hex") + "." + e; +} + +/** + * AES解密 + * @param passstore + * @returns {Promise} + */ +async function store2ha1(passstore) { + try { + const parts = passstore.split("."); + if (parts.length === 2) { + // 新的加密方式 with IV: IV.E + const c = crypto.createDecipheriv( + "aes-256-cbc", + key, + Buffer.from(parts[0], "hex") + ); + let d = c.update(parts[1], "hex", "binary"); + d += c.final("binary"); + return d; + } else { + // 旧加密方式 without IV: E + const c = crypto.createDecipher("aes192", key); + let d = c.update(passstore, "hex", "binary"); + d += c.final("binary"); + return d; + } + } catch (e) { + console.log( + "在[default]部分设置的passwordSecret无法解密信息。请确保所有节点的passwordSecret相同。如果您更改了密码保密信息,可能需要重新添加用户。", + e + ); + process.exit(1); + } +} + +export { + ha12store, + store2ha1 +} \ No newline at end of file diff --git a/utils/netease.js b/utils/netease.js new file mode 100644 index 0000000..30b6543 --- /dev/null +++ b/utils/netease.js @@ -0,0 +1,87 @@ +// 获取cookie +import fetch from "node-fetch"; +import axios from "axios"; + +const BASE_URL = "http://127.0.0.1:3000" + +/** + * 获取cookie + * @param key + * @returns {Promise} + */ +async function getCookies(key) { + const cookieUrl = `${BASE_URL}/login/qr/check?key=${key}×tamp=${Date.now()}`; + return fetch(cookieUrl).then(async (resp) => { + return await resp.json(); + }); +} + +/** + * 获取登陆状态 + * @param cookie + * @returns {Promise>} + */ +async function getLoginStatus(cookie) { + return axios({ + url: `${BASE_URL}/login/status?timestamp=${Date.now()}`, + method: 'post', + data: { + cookie, + }, + }) + .then(resp => { + console.log(resp.data.data) + return resp.data.data + }); +} + +/** + * 获取每日推荐 + * @param cookie + * @returns {Promise>} + */ +async function getDailyRecommend(cookie) { + return axios({ + url: `${BASE_URL}/recommend/songs?timestamp=${Date.now()}`, + method: 'get', + data: { + cookie, + }, + }) + .then(resp => { + return resp.data.data + }); +} + +/** + * 获取密匙 + * @returns {Promise<*>} + */ +async function getKey() { + const keyUrl = `${BASE_URL}/login/qr/key?timestamp=${Date.now()}`; + return await fetch(keyUrl).then(async (resp) => { + const respJson = await resp.json(); + return respJson.data.unikey; + }); +} + +/** + * 获取二维码 + * @param key + * @returns {Promise<*>} + */ +async function getQrCode(key) { + const qrPicUrl = `${BASE_URL}/login/qr/create?key=${key}&qrimg=true×tamp=${Date.now()}`; + return await fetch(qrPicUrl).then(async (resp) => { + const respJson = await resp.json(); + return respJson.data.qrimg; + }); +} + +export { + getCookies, + getLoginStatus, + getDailyRecommend, + getKey, + getQrCode, +} \ No newline at end of file From c5874ab374566841c9f6579627fd2e389528cf6c Mon Sep 17 00:00:00 2001 From: zhiyu1998 Date: Wed, 15 Mar 2023 01:21:29 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E2=9C=A8=20feat:=20=E7=BD=91=E6=98=93?= =?UTF-8?q?=E4=BA=91=E5=8A=9F=E8=83=BD=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/neteasepro.js | 150 ++++++++++++++++++++++++++++++++++++++++----- apps/tools.js | 69 --------------------- utils/encrypt.js | 5 +- utils/netease.js | 47 +++++++++++++- 4 files changed, 183 insertions(+), 88 deletions(-) diff --git a/apps/neteasepro.js b/apps/neteasepro.js index 02b6f21..61fdbb3 100644 --- a/apps/neteasepro.js +++ b/apps/neteasepro.js @@ -2,8 +2,9 @@ import plugin from "../../../lib/plugins/plugin.js"; import axios from "axios"; import fs from 'node:fs'; import {segment} from "oicq"; -import {getQrCode, getKey, getLoginStatus, getDailyRecommend, getCookies} from '../utils/netease.js'; +import {getQrCode, getKey, getLoginStatus, getDailyRecommend, getCookies, getUserRecord, checkMusic, getSong} from '../utils/netease.js'; import {ha12store, store2ha1} from '../utils/encrypt.js'; +import fetch from "node-fetch"; export class neteasepro extends plugin { @@ -27,13 +28,21 @@ export class neteasepro extends plugin { { reg: '#网易云每日推荐', fnc: 'neteaseDailyRecommend' - } + }, + { + reg: '#网易云听歌排行', + fnc: 'neteaseListenRank' + }, + { + reg: "music.163.com", + fnc: "netease", + }, ] }) } async neteaseCloudLogin(e) { - let userInfo; + let neteaseCookie; // 如果不存在cookie if (!await redis.exists(await this.getRedisKey(e.user_id))) { // 获取密钥 @@ -49,44 +58,121 @@ export class neteasepro extends plugin { // 定时轮询 await this.poll(key).then(async cookie => { // 存放到redis - await redis.set(await this.getRedisKey(e.user_id), ha12store(cookie)) + neteaseCookie = cookie }); + } else { + // 已经登陆过的,直接从redis取出 + neteaseCookie = await store2ha1(JSON.parse(await redis.get(await this.getRedisKey(e.user_id))).cookie) } - // 从redis中获取 - const realCookie = await store2ha1(await redis.get(await this.getRedisKey(e.user_id))); // 获取用户信息 - userInfo = await getLoginStatus(realCookie); + const userInfo = await getLoginStatus(neteaseCookie); // 提取信息 - const {nickname, avatarUrl} = userInfo.profile; + const {userId, nickname, avatarUrl} = userInfo.profile; e.reply(["欢迎使用 🎶网易云音乐 🎶," + nickname, segment.image(avatarUrl)]) + // 重组后存放到redis {uid, cookie} + await redis.set(await this.getRedisKey(e.user_id), JSON.stringify({ + uid: userId, + cookie: (await ha12store(neteaseCookie)) + })); + return true } async neteaseDailyRecommend(e) { - const realCookie = await this.aopBefore(e); + const realCookie = (await this.aopBefore(e)).cookie; if (realCookie === "") { return true; } // 获取每日推荐所有数据 const dailyRecommend = await getDailyRecommend(realCookie); // 由于数据过大,取前10 - let combineMsg = [] - dailyRecommend.dailySongs.slice(0, 10).forEach(item => { - combineMsg.push([`${item?.id}: ${item?.name}-${item?.ar?.[0].name}-${item?.al?.name}`, segment.image(item?.al?.picUrl)]) + const combineMsg = await dailyRecommend.dailySongs.slice(0, 10).map(item => { + // 组合数据 + return { + message: [segment.text(`${item?.id}: ${item?.name}-${item?.ar?.[0].name}-${item?.al?.name}`) + , segment.image(item?.al?.picUrl)], + nickname: e.sender.card || e.user_id, + user_id: e.user_id, + } }) await e.reply(await Bot.makeForwardMsg(combineMsg)); } - // 切面方法检测cookie + async neteaseListenRank(e) { + const userInfo = await this.aopBefore(e) + const realCookie = userInfo.cookie; + if (realCookie === "") { + return true; + } + // 获取用户id + const uid = userInfo.uid; + // 获取听歌排行榜 + const userRecord = await getUserRecord(uid) + let rankId = 0; + e.reply(" 😘亲,这是你的听歌排行榜Top10") + const rank = userRecord.weekData.slice(0, 10).map(item => { + // 组合数据 + const song = item.song; + rankId++; + return { + message: [segment.text(`No.${rankId} ${song?.id}: ${song?.name}-${song?.ar?.[0].name}-${song?.al?.name}`) + , segment.image(song?.al?.picUrl)], + nickname: e.sender.card || e.user_id, + user_id: e.user_id, + } + }) + await e.reply(await Bot.makeForwardMsg(rank)); + } + + async netease(e) { + const message = e.msg === undefined ? e.message.shift().data.replaceAll("\\", "") : e.msg.trim(); + 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 id = musicUrlReg2.exec(message)[3] || musicUrlReg.exec(message)[2] || /id=(\d+)/.exec(message)[1]; + const musicJson = JSON.parse(message) + const {musicUrl, preview, title, desc} = musicJson.meta.music || musicJson.meta.news; + console.log(musicUrl, preview, title, desc) + // 如果没有登陆,就使用官方接口 + e.reply([`识别:网易云音乐,${title}--${desc}`, segment.image(preview)]); + if (!await redis.exists(await this.getRedisKey(e.user_id))) { + this.downloadMp3(`music.163.com/song/media/outer/url?id=${id}`, 'follow').then(path => { + Bot.acquireGfs(e.group_id).upload(fs.readFileSync(path), '/', `${title.replace(/[\/\?<>\\:\*\|".… ]/g, '')}.mp3`) + }) + .catch(err => { + console.error(`下载音乐失败,错误信息为: ${err.message}`); + }); + return true; + } + // 检查当前歌曲是否可用 + const checkOne = await checkMusic(id); + if (checkOne.success === 'false') { + e.reply(checkOne.message); + return true; + } + const userInfo = await this.aopBefore(e) + // 可用,开始下载 + const userDownloadUrl = (await getSong(id, await userInfo.cookie))[0].url + await this.downloadMp3(userDownloadUrl).then(path => { + Bot.acquireGfs(e.group_id).upload(fs.readFileSync(path), '/', `${title.replace(/[\/\?<>\\:\*\|".… ]/g, '')}.mp3`) + }) + .catch(err => { + console.error(`下载音乐失败,错误信息为: ${err.message}`); + }); + return true; + } + + // 切面方法检测cookie & 获取cookie和uid async aopBefore(e) { // 取出cookie - const cookie = await redis.get(await this.getRedisKey(e.user_id)); + let userInfo = JSON.parse(await redis.get(await this.getRedisKey(e.user_id))); + const cookie = userInfo.cookie; // 如果不存在cookie if (!cookie) { e.reply("请先#网易云登录"); return ""; } // 解析cookie - return store2ha1(cookie); + userInfo.cookie = store2ha1(cookie) + return userInfo; } // 下载二维码 @@ -131,6 +217,40 @@ export class neteasepro extends plugin { }); } + /** + * 下载mp3 + * @param mp3Url + * @param redirect + * @returns {Promise} + */ + async downloadMp3(mp3Url, redirect='manual') { + return fetch(mp3Url, { + 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", + redirect: redirect + }) + .then(res => { + const path = `./data/rcmp4/${this.e.group_id || this.e.user_id}/temp.mp3` + const fileStream = fs.createWriteStream(path); + res.body.pipe(fileStream); + return new Promise((resolve, reject) => { + fileStream.on("finish", () => { + fileStream.close(() => { + resolve(path); + }); + }); + fileStream.on("error", err => { + fs.unlink(path, () => { + reject(err); + }); + }); + }); + }) + } + // 获取redis的key async getRedisKey(user_id) { return `Yz:rconsole:netease:${user_id}`; diff --git a/apps/tools.js b/apps/tools.js index 5174c57..bf4f13a 100644 --- a/apps/tools.js +++ b/apps/tools.js @@ -48,10 +48,6 @@ export class tools extends plugin { reg: "(.*)(twitter.com)", fnc: "twitter", }, - { - reg: "music.163.com", - fnc: "netease", - }, { reg: "(acfun.cn)", fnc: "acfun", @@ -640,37 +636,6 @@ export class tools extends plugin { } } - async netease(e) { - const message = e.msg === undefined ? e.message.shift().data.replaceAll("\\", "") : e.msg.trim(); - const musicUrlReg = /(http:|https:)\/\/music.163.com\/song\/media\/outer\/url\?id=(\d+)/; - const id = musicUrlReg.exec(message)[2] || /id=(\d+)/.exec(message)[1]; - const musicJson = JSON.parse(message) - const {musicUrl, preview, title, desc} = musicJson.meta.music - // 如果没有下载地址跳出if - if (_.isNull(musicUrl) || _.isUndefined(musicUrl)) { - e.reply(`识别:网易云音乐,解析失败!`); - return - } else { - fetch(`https://www.oranges1.top/neteaseapi.do/song/url?id=${id}`, { - 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", - }, - }).then(async resp => { - const url = await JSON.parse(await resp.text()).data[0].url - // 反之解析官方地址 - e.reply([`识别:网易云音乐,${title}--${desc}`, segment.image(preview)]); - this.downloadMp3(url, 'follow').then(path => { - Bot.acquireGfs(e.group_id).upload(fs.readFileSync(path), '/', `${title.replace(/[\/\?<>\\:\*\|".… ]/g, '')}.mp3`) - }) - .catch(err => { - console.error(`下载音乐失败,错误信息为: ${err.message}`); - }); - }) - } - return true; - } - /** * 哔哩哔哩下载 * @param title @@ -828,38 +793,4 @@ export class tools extends plugin { writer.on("error", reject); }); } - - /** - * 下载mp3 - * @param mp3Url - * @param redirect - * @returns {Promise} - */ - async downloadMp3(mp3Url, redirect='manual') { - return fetch(mp3Url, { - 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", - redirect: redirect - }) - .then(res => { - const path = `${this.defaultPath}${this.e.group_id || this.e.user_id}/temp.mp3` - const fileStream = fs.createWriteStream(path); - res.body.pipe(fileStream); - return new Promise((resolve, reject) => { - fileStream.on("finish", () => { - fileStream.close(() => { - resolve(path); - }); - }); - fileStream.on("error", err => { - fs.unlink(path, () => { - reject(err); - }); - }); - }); - }) - } } diff --git a/utils/encrypt.js b/utils/encrypt.js index 49d2894..93d529b 100644 --- a/utils/encrypt.js +++ b/utils/encrypt.js @@ -4,7 +4,7 @@ import crypto from "crypto"; const key = crypto.createHash("sha256").update("rconsole").digest(); /** - * AES解密 + * AES加密 * @param ha1 * @returns {Promise} */ @@ -43,11 +43,10 @@ async function store2ha1(passstore) { return d; } } catch (e) { - console.log( + console.error( "在[default]部分设置的passwordSecret无法解密信息。请确保所有节点的passwordSecret相同。如果您更改了密码保密信息,可能需要重新添加用户。", e ); - process.exit(1); } } diff --git a/utils/netease.js b/utils/netease.js index 30b6543..4d80943 100644 --- a/utils/netease.js +++ b/utils/netease.js @@ -30,7 +30,6 @@ async function getLoginStatus(cookie) { }, }) .then(resp => { - console.log(resp.data.data) return resp.data.data }); } @@ -78,10 +77,56 @@ async function getQrCode(key) { }); } +/** + * 获取听歌排行榜 + * @param uid + * @returns {Promise>} + */ +async function getUserRecord(uid) { + return axios({ + url: `${BASE_URL}/user/record?uid=${uid}&type=1×tamp=${Date.now()}`, + method: 'get', + }) + .then(resp => { + return resp.data + }); +} + +/** + * 检查当前歌曲是否可用 + * @param id + * @returns {Promise>} 返回{success:true|false, message: 'ok'} + */ +async function checkMusic(id) { + return axios({ + url: `${BASE_URL}/check/music?id=${id}×tamp=${Date.now()}`, + method: 'get', + }) + .then(resp => { + return resp.data + }); +} + +async function getSong(id, cookie) { + return axios({ + url: `${BASE_URL}/song/url/v1?id=${id}&level=standard×tamp=${Date.now()}`, + method: 'post', + data: { + cookie, + }, + }) + .then(resp => { + return resp.data.data + }); +} + export { getCookies, getLoginStatus, getDailyRecommend, getKey, getQrCode, + getUserRecord, + checkMusic, + getSong } \ No newline at end of file From cec0f2da03471393424faa2f3f82b08a2a65bcf8 Mon Sep 17 00:00:00 2001 From: zhiyu1998 Date: Wed, 15 Mar 2023 13:19:49 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=A6=84=20refactor:=20=E6=9B=B4?= =?UTF-8?q?=E6=94=B9=E5=9C=B0=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- utils/netease.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/netease.js b/utils/netease.js index 4d80943..fb41e68 100644 --- a/utils/netease.js +++ b/utils/netease.js @@ -2,7 +2,7 @@ import fetch from "node-fetch"; import axios from "axios"; -const BASE_URL = "http://127.0.0.1:3000" +const BASE_URL = "http://cloud-music.pl-fe.cn" /** * 获取cookie From 8426215d5f7543ae157215bb2a068915d89406ab Mon Sep 17 00:00:00 2001 From: zhiyu1998 Date: Wed, 15 Mar 2023 23:46:38 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=8C=88=20style:=20=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/neteasepro.js | 200 ++++++++++++++++++++++++++------------------- 1 file changed, 118 insertions(+), 82 deletions(-) diff --git a/apps/neteasepro.js b/apps/neteasepro.js index 61fdbb3..8c2189f 100644 --- a/apps/neteasepro.js +++ b/apps/neteasepro.js @@ -1,50 +1,58 @@ import plugin from "../../../lib/plugins/plugin.js"; import axios from "axios"; -import fs from 'node:fs'; -import {segment} from "oicq"; -import {getQrCode, getKey, getLoginStatus, getDailyRecommend, getCookies, getUserRecord, checkMusic, getSong} from '../utils/netease.js'; -import {ha12store, store2ha1} from '../utils/encrypt.js'; +import fs from "node:fs"; +import { segment } from "oicq"; +import { + getQrCode, + getKey, + getLoginStatus, + getDailyRecommend, + getCookies, + getUserRecord, + checkMusic, + getSong, +} from "../utils/netease.js"; +import { ha12store, store2ha1 } from "../utils/encrypt.js"; import fetch from "node-fetch"; - export class neteasepro extends plugin { constructor() { super({ /** 功能名称 */ - name: 'R插件网易云音乐解析', + name: "R插件网易云音乐解析", /** 功能描述 */ - dsc: '网易云音乐解析Pro', + dsc: "网易云音乐解析Pro", /** https://oicqjs.github.io/oicq/#events */ - event: 'message', + event: "message", /** 优先级,数字越小等级越高 */ priority: 1, rule: [ { /** 命令正则匹配 */ - reg: '#网易云登陆', + reg: "#网易云登陆", /** 执行方法 */ - fnc: 'neteaseCloudLogin' + fnc: "neteaseCloudLogin", }, { - reg: '#网易云每日推荐', - fnc: 'neteaseDailyRecommend' + reg: "#网易云每日推荐", + fnc: "neteaseDailyRecommend", }, { - reg: '#网易云听歌排行', - fnc: 'neteaseListenRank' + reg: "#网易云听歌排行", + fnc: "neteaseListenRank", }, { reg: "music.163.com", fnc: "netease", }, - ] - }) + ], + }); } async neteaseCloudLogin(e) { let neteaseCookie; // 如果不存在cookie - if (!await redis.exists(await this.getRedisKey(e.user_id))) { + if (!(await redis.exists(await this.getRedisKey(e.user_id)))) { // 获取密钥 const key = await getKey(); // console.log(key); @@ -53,28 +61,33 @@ export class neteasepro extends plugin { // 下载qrcode await this.downloadQrCode(qrPic).then(path => { // 发送二维码 - e.reply(segment.image(fs.readFileSync(path))) - }) + e.reply(segment.image(fs.readFileSync(path))); + }); // 定时轮询 await this.poll(key).then(async cookie => { // 存放到redis - neteaseCookie = cookie + neteaseCookie = cookie; }); } else { // 已经登陆过的,直接从redis取出 - neteaseCookie = await store2ha1(JSON.parse(await redis.get(await this.getRedisKey(e.user_id))).cookie) + neteaseCookie = await store2ha1( + JSON.parse(await redis.get(await this.getRedisKey(e.user_id))).cookie, + ); } // 获取用户信息 const userInfo = await getLoginStatus(neteaseCookie); // 提取信息 - const {userId, nickname, avatarUrl} = userInfo.profile; - e.reply(["欢迎使用 🎶网易云音乐 🎶," + nickname, segment.image(avatarUrl)]) + const { userId, nickname, avatarUrl } = userInfo.profile; + e.reply(["欢迎使用 🎶网易云音乐 🎶," + nickname, segment.image(avatarUrl)]); // 重组后存放到redis {uid, cookie} - await redis.set(await this.getRedisKey(e.user_id), JSON.stringify({ - uid: userId, - cookie: (await ha12store(neteaseCookie)) - })); - return true + await redis.set( + await this.getRedisKey(e.user_id), + JSON.stringify({ + uid: userId, + cookie: await ha12store(neteaseCookie), + }), + ); + return true; } async neteaseDailyRecommend(e) { @@ -88,17 +101,21 @@ export class neteasepro extends plugin { const combineMsg = await dailyRecommend.dailySongs.slice(0, 10).map(item => { // 组合数据 return { - message: [segment.text(`${item?.id}: ${item?.name}-${item?.ar?.[0].name}-${item?.al?.name}`) - , segment.image(item?.al?.picUrl)], + message: [ + segment.text( + `${item?.id}: ${item?.name}-${item?.ar?.[0].name}-${item?.al?.name}`, + ), + segment.image(item?.al?.picUrl), + ], nickname: e.sender.card || e.user_id, user_id: e.user_id, - } - }) + }; + }); await e.reply(await Bot.makeForwardMsg(combineMsg)); } async neteaseListenRank(e) { - const userInfo = await this.aopBefore(e) + const userInfo = await this.aopBefore(e); const realCookie = userInfo.cookie; if (realCookie === "") { return true; @@ -106,37 +123,50 @@ export class neteasepro extends plugin { // 获取用户id const uid = userInfo.uid; // 获取听歌排行榜 - const userRecord = await getUserRecord(uid) + const userRecord = await getUserRecord(uid); let rankId = 0; - e.reply(" 😘亲,这是你的听歌排行榜Top10") + e.reply(" 😘亲,这是你的听歌排行榜Top10"); const rank = userRecord.weekData.slice(0, 10).map(item => { // 组合数据 const song = item.song; rankId++; return { - message: [segment.text(`No.${rankId} ${song?.id}: ${song?.name}-${song?.ar?.[0].name}-${song?.al?.name}`) - , segment.image(song?.al?.picUrl)], + message: [ + segment.text( + `No.${rankId} ${song?.id}: ${song?.name}-${song?.ar?.[0].name}-${song?.al?.name}`, + ), + segment.image(song?.al?.picUrl), + ], nickname: e.sender.card || e.user_id, user_id: e.user_id, - } - }) + }; + }); await e.reply(await Bot.makeForwardMsg(rank)); } async netease(e) { - const message = e.msg === undefined ? e.message.shift().data.replaceAll("\\", "") : e.msg.trim(); + const message = + e.msg === undefined ? e.message.shift().data.replaceAll("\\", "") : e.msg.trim(); 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 id = musicUrlReg2.exec(message)[3] || musicUrlReg.exec(message)[2] || /id=(\d+)/.exec(message)[1]; - const musicJson = JSON.parse(message) - const {musicUrl, preview, title, desc} = musicJson.meta.music || musicJson.meta.news; - console.log(musicUrl, preview, title, desc) + const id = + musicUrlReg2.exec(message)[3] || + musicUrlReg.exec(message)[2] || + /id=(\d+)/.exec(message)[1]; + const musicJson = JSON.parse(message); + const { musicUrl, preview, title, desc } = musicJson.meta.music || musicJson.meta.news; + console.log(musicUrl, preview, title, desc); // 如果没有登陆,就使用官方接口 e.reply([`识别:网易云音乐,${title}--${desc}`, segment.image(preview)]); - if (!await redis.exists(await this.getRedisKey(e.user_id))) { - this.downloadMp3(`music.163.com/song/media/outer/url?id=${id}`, 'follow').then(path => { - Bot.acquireGfs(e.group_id).upload(fs.readFileSync(path), '/', `${title.replace(/[\/\?<>\\:\*\|".… ]/g, '')}.mp3`) - }) + if (!(await redis.exists(await this.getRedisKey(e.user_id)))) { + this.downloadMp3(`music.163.com/song/media/outer/url?id=${id}`, "follow") + .then(path => { + Bot.acquireGfs(e.group_id).upload( + fs.readFileSync(path), + "/", + `${title.replace(/[\/\?<>\\:\*\|".… ]/g, "")}.mp3`, + ); + }) .catch(err => { console.error(`下载音乐失败,错误信息为: ${err.message}`); }); @@ -144,16 +174,21 @@ export class neteasepro extends plugin { } // 检查当前歌曲是否可用 const checkOne = await checkMusic(id); - if (checkOne.success === 'false') { + if (checkOne.success === "false") { e.reply(checkOne.message); return true; } - const userInfo = await this.aopBefore(e) + const userInfo = await this.aopBefore(e); // 可用,开始下载 - const userDownloadUrl = (await getSong(id, await userInfo.cookie))[0].url - await this.downloadMp3(userDownloadUrl).then(path => { - Bot.acquireGfs(e.group_id).upload(fs.readFileSync(path), '/', `${title.replace(/[\/\?<>\\:\*\|".… ]/g, '')}.mp3`) - }) + const userDownloadUrl = (await getSong(id, await userInfo.cookie))[0].url; + await this.downloadMp3(userDownloadUrl) + .then(path => { + Bot.acquireGfs(e.group_id).upload( + fs.readFileSync(path), + "/", + `${title.replace(/[\/\?<>\\:\*\|".… ]/g, "")}.mp3`, + ); + }) .catch(err => { console.error(`下载音乐失败,错误信息为: ${err.message}`); }); @@ -171,7 +206,7 @@ export class neteasepro extends plugin { return ""; } // 解析cookie - userInfo.cookie = store2ha1(cookie) + userInfo.cookie = store2ha1(cookie); return userInfo; } @@ -185,7 +220,7 @@ export class neteasepro extends plugin { }, responseType: "stream", }) - .then((resp) => { + .then(resp => { const filepath = "./netease_qr.jpg"; const writer = fs.createWriteStream(filepath); resp.data.pipe(writer); @@ -201,19 +236,21 @@ export class neteasepro extends plugin { let timer; return new Promise((resolve, reject) => { timer = setInterval(async () => { - const statusRes = await getCookies(key) - console.log(statusRes) + const statusRes = await getCookies(key); + console.log(statusRes); if (statusRes.code === 800) { - clearInterval(timer) - reject('二维码已过期,请重新获取') + clearInterval(timer); + reject("二维码已过期,请重新获取"); } if (statusRes.code === 803) { // 这一步会返回cookie - clearInterval(timer) + clearInterval(timer); const cookie = statusRes.cookie; - resolve(/__csrf=[0-9a-z]+;/.exec(cookie)[0] + /MUSIC_U=[0-9a-z]+;/.exec(cookie)[0]); + resolve( + /__csrf=[0-9a-z]+;/.exec(cookie)[0] + /MUSIC_U=[0-9a-z]+;/.exec(cookie)[0], + ); } - }, 3000) + }, 3000); }); } @@ -223,36 +260,35 @@ export class neteasepro extends plugin { * @param redirect * @returns {Promise} */ - async downloadMp3(mp3Url, redirect='manual') { + async downloadMp3(mp3Url, redirect = "manual") { return fetch(mp3Url, { 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", - redirect: redirect - }) - .then(res => { - const path = `./data/rcmp4/${this.e.group_id || this.e.user_id}/temp.mp3` - const fileStream = fs.createWriteStream(path); - res.body.pipe(fileStream); - return new Promise((resolve, reject) => { - fileStream.on("finish", () => { - fileStream.close(() => { - resolve(path); - }); - }); - fileStream.on("error", err => { - fs.unlink(path, () => { - reject(err); - }); + redirect: redirect, + }).then(res => { + const path = `./data/rcmp4/${this.e.group_id || this.e.user_id}/temp.mp3`; + const fileStream = fs.createWriteStream(path); + res.body.pipe(fileStream); + return new Promise((resolve, reject) => { + fileStream.on("finish", () => { + fileStream.close(() => { + resolve(path); }); }); - }) + fileStream.on("error", err => { + fs.unlink(path, () => { + reject(err); + }); + }); + }); + }); } // 获取redis的key async getRedisKey(user_id) { return `Yz:rconsole:netease:${user_id}`; } -} \ No newline at end of file +} From 648ed593c707bbd3fde865631c4ebc7a50602cf8 Mon Sep 17 00:00:00 2001 From: zhiyu1998 Date: Wed, 15 Mar 2023 23:46:57 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=8C=88=20style:=20=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- utils/encrypt.js | 13 +++-------- utils/netease.js | 57 ++++++++++++++++++++++-------------------------- 2 files changed, 29 insertions(+), 41 deletions(-) diff --git a/utils/encrypt.js b/utils/encrypt.js index 93d529b..ea34dc8 100644 --- a/utils/encrypt.js +++ b/utils/encrypt.js @@ -27,11 +27,7 @@ async function store2ha1(passstore) { const parts = passstore.split("."); if (parts.length === 2) { // 新的加密方式 with IV: IV.E - const c = crypto.createDecipheriv( - "aes-256-cbc", - key, - Buffer.from(parts[0], "hex") - ); + const c = crypto.createDecipheriv("aes-256-cbc", key, Buffer.from(parts[0], "hex")); let d = c.update(parts[1], "hex", "binary"); d += c.final("binary"); return d; @@ -45,12 +41,9 @@ async function store2ha1(passstore) { } catch (e) { console.error( "在[default]部分设置的passwordSecret无法解密信息。请确保所有节点的passwordSecret相同。如果您更改了密码保密信息,可能需要重新添加用户。", - e + e, ); } } -export { - ha12store, - store2ha1 -} \ No newline at end of file +export { ha12store, store2ha1 }; diff --git a/utils/netease.js b/utils/netease.js index fb41e68..f1e91c5 100644 --- a/utils/netease.js +++ b/utils/netease.js @@ -2,7 +2,7 @@ import fetch from "node-fetch"; import axios from "axios"; -const BASE_URL = "http://cloud-music.pl-fe.cn" +const BASE_URL = "http://cloud-music.pl-fe.cn"; /** * 获取cookie @@ -11,7 +11,7 @@ const BASE_URL = "http://cloud-music.pl-fe.cn" */ async function getCookies(key) { const cookieUrl = `${BASE_URL}/login/qr/check?key=${key}×tamp=${Date.now()}`; - return fetch(cookieUrl).then(async (resp) => { + return fetch(cookieUrl).then(async resp => { return await resp.json(); }); } @@ -24,14 +24,13 @@ async function getCookies(key) { async function getLoginStatus(cookie) { return axios({ url: `${BASE_URL}/login/status?timestamp=${Date.now()}`, - method: 'post', + method: "post", data: { cookie, }, - }) - .then(resp => { - return resp.data.data - }); + }).then(resp => { + return resp.data.data; + }); } /** @@ -42,14 +41,13 @@ async function getLoginStatus(cookie) { async function getDailyRecommend(cookie) { return axios({ url: `${BASE_URL}/recommend/songs?timestamp=${Date.now()}`, - method: 'get', + method: "get", data: { cookie, }, - }) - .then(resp => { - return resp.data.data - }); + }).then(resp => { + return resp.data.data; + }); } /** @@ -58,7 +56,7 @@ async function getDailyRecommend(cookie) { */ async function getKey() { const keyUrl = `${BASE_URL}/login/qr/key?timestamp=${Date.now()}`; - return await fetch(keyUrl).then(async (resp) => { + return await fetch(keyUrl).then(async resp => { const respJson = await resp.json(); return respJson.data.unikey; }); @@ -71,7 +69,7 @@ async function getKey() { */ async function getQrCode(key) { const qrPicUrl = `${BASE_URL}/login/qr/create?key=${key}&qrimg=true×tamp=${Date.now()}`; - return await fetch(qrPicUrl).then(async (resp) => { + return await fetch(qrPicUrl).then(async resp => { const respJson = await resp.json(); return respJson.data.qrimg; }); @@ -85,11 +83,10 @@ async function getQrCode(key) { async function getUserRecord(uid) { return axios({ url: `${BASE_URL}/user/record?uid=${uid}&type=1×tamp=${Date.now()}`, - method: 'get', - }) - .then(resp => { - return resp.data - }); + method: "get", + }).then(resp => { + return resp.data; + }); } /** @@ -100,24 +97,22 @@ async function getUserRecord(uid) { async function checkMusic(id) { return axios({ url: `${BASE_URL}/check/music?id=${id}×tamp=${Date.now()}`, - method: 'get', - }) - .then(resp => { - return resp.data - }); + method: "get", + }).then(resp => { + return resp.data; + }); } async function getSong(id, cookie) { return axios({ url: `${BASE_URL}/song/url/v1?id=${id}&level=standard×tamp=${Date.now()}`, - method: 'post', + method: "post", data: { cookie, }, - }) - .then(resp => { - return resp.data.data - }); + }).then(resp => { + return resp.data.data; + }); } export { @@ -128,5 +123,5 @@ export { getQrCode, getUserRecord, checkMusic, - getSong -} \ No newline at end of file + getSong, +}; From 9958568d05a6fe0375a8732218717d0e69dd4aff Mon Sep 17 00:00:00 2001 From: zhiyu1998 Date: Thu, 16 Mar 2023 01:00:24 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E2=9C=A8=20feat:=20=E7=BD=91=E6=98=93?= =?UTF-8?q?=E4=BA=91=E9=93=BE=E6=8E=A5=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/neteasepro.js | 64 +++++++++++++++++++++++++++++----------------- utils/netease.js | 10 ++++++++ 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/apps/neteasepro.js b/apps/neteasepro.js index 8c2189f..5d30fb1 100644 --- a/apps/neteasepro.js +++ b/apps/neteasepro.js @@ -11,6 +11,7 @@ import { getUserRecord, checkMusic, getSong, + getSongDetail, } from "../utils/netease.js"; import { ha12store, store2ha1 } from "../utils/encrypt.js"; import fetch from "node-fetch"; @@ -153,24 +154,27 @@ export class neteasepro extends plugin { musicUrlReg2.exec(message)[3] || musicUrlReg.exec(message)[2] || /id=(\d+)/.exec(message)[1]; - const musicJson = JSON.parse(message); - const { musicUrl, preview, title, desc } = musicJson.meta.music || musicJson.meta.news; - console.log(musicUrl, preview, title, desc); - // 如果没有登陆,就使用官方接口 - e.reply([`识别:网易云音乐,${title}--${desc}`, segment.image(preview)]); - if (!(await redis.exists(await this.getRedisKey(e.user_id)))) { - this.downloadMp3(`music.163.com/song/media/outer/url?id=${id}`, "follow") - .then(path => { - Bot.acquireGfs(e.group_id).upload( - fs.readFileSync(path), - "/", - `${title.replace(/[\/\?<>\\:\*\|".… ]/g, "")}.mp3`, - ); - }) - .catch(err => { - console.error(`下载音乐失败,错误信息为: ${err.message}`); - }); - return true; + const isMessageJson = await this.isJSON(message); + if (isMessageJson) { + const musicJson = JSON.parse(message); + const { preview, title, desc } = musicJson.meta.music || musicJson.meta.news; + // console.log(musicUrl, preview, title, desc); + // 如果没有登陆,就使用官方接口 + e.reply([`识别:网易云音乐,${title}--${desc}`, segment.image(preview)]); + if (!(await redis.exists(await this.getRedisKey(e.user_id)))) { + this.downloadMp3(`music.163.com/song/media/outer/url?id=${id}`, "follow") + .then(path => { + Bot.acquireGfs(e.group_id).upload( + fs.readFileSync(path), + "/", + `${title.replace(/[\/\?<>\\:\*\|".… ]/g, "")}.mp3`, + ); + }) + .catch(err => { + console.error(`下载音乐失败,错误信息为: ${err.message}`); + }); + return true; + } } // 检查当前歌曲是否可用 const checkOne = await checkMusic(id); @@ -181,13 +185,13 @@ export class neteasepro extends plugin { const userInfo = await this.aopBefore(e); // 可用,开始下载 const userDownloadUrl = (await getSong(id, await userInfo.cookie))[0].url; + const title = await getSongDetail(id).then(res => { + const song = res.songs[0]; + return `${song?.name}-${song?.ar?.[0].name}`.replace(/[\/\?<>\\:\*\|".… ]/g, ""); + }); await this.downloadMp3(userDownloadUrl) .then(path => { - Bot.acquireGfs(e.group_id).upload( - fs.readFileSync(path), - "/", - `${title.replace(/[\/\?<>\\:\*\|".… ]/g, "")}.mp3`, - ); + Bot.acquireGfs(e.group_id).upload(fs.readFileSync(path), "/", `${title}.mp3`); }) .catch(err => { console.error(`下载音乐失败,错误信息为: ${err.message}`); @@ -231,13 +235,25 @@ export class neteasepro extends plugin { }); } + async isJSON(str) { + if (typeof str !== "string") { + return false; + } + try { + JSON.parse(str); + return true; + } catch (e) { + return false; + } + } + // 定时轮询 async poll(key) { let timer; return new Promise((resolve, reject) => { timer = setInterval(async () => { const statusRes = await getCookies(key); - console.log(statusRes); + // console.log(statusRes); if (statusRes.code === 800) { clearInterval(timer); reject("二维码已过期,请重新获取"); diff --git a/utils/netease.js b/utils/netease.js index f1e91c5..7876d22 100644 --- a/utils/netease.js +++ b/utils/netease.js @@ -115,6 +115,15 @@ async function getSong(id, cookie) { }); } +async function getSongDetail(ids) { + return axios({ + url: `${BASE_URL}/song/detail?ids=${ids}×tamp=${Date.now()}`, + method: "get", + }).then(resp => { + return resp.data; + }); +} + export { getCookies, getLoginStatus, @@ -124,4 +133,5 @@ export { getUserRecord, checkMusic, getSong, + getSongDetail };