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 fetch from "node-fetch"; 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", }, { 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)))) { // 获取密钥 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 neteaseCookie = cookie; }); } else { // 已经登陆过的,直接从redis取出 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)]); // 重组后存放到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)).cookie; if (realCookie === "") { return true; } // 获取每日推荐所有数据 const dailyRecommend = await getDailyRecommend(realCookie); // 由于数据过大,取前10 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)); } 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 let userInfo = JSON.parse(await redis.get(await this.getRedisKey(e.user_id))); const cookie = userInfo.cookie; // 如果不存在cookie if (!cookie) { e.reply("请先#网易云登录"); return ""; } // 解析cookie userInfo.cookie = store2ha1(cookie); return userInfo; } // 下载二维码 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); }); } /** * 下载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}`; } }