Electron项目:逆向网易云音乐API实现音乐搜索和下载
这个项目主要是基于Electron并通过逆向网易云音乐的搜索和音乐播放api,分析音乐请求的data参数,复现了加密算法,实现请求。
完整项目已开源到 github,需要自行下载ReverseWangYi-Music
项目效果展示
项目结构
该项目主要包含以下核心文件:
main.js
- Electron应用的主进程文件。renderer.js
- Electron应用的渲染进程文件。Encrypt.js
- 包含加密功能的辅助类。NeteaseCloudMusic.js
- 用于与网易云音乐API交互和下载音乐的类。SearchMusic.js
- 用于搜索音乐的类。
接下来,我将逐一介绍这些文件。
核心代码分析
Encrypt.js
Encrypt.js
文件定义了一个名为Encrypt
的类,该类包含了用于加密数据的方法。以下是类中两个核心方法的代码和分析:
const crypto = require('crypto');
class Encrypt {
constructor(text) {
this.data = {
encSecKey: '01ec48cb405730aa77f993a988cc1f5bc1938511d75f49eddc581f2fe2aaf18988853200564b2d4b1312cf6e0bb344425addce5a4c81b38b89a5973900946bd100b0f1865d22d2a8e5dd8be208eb5d6eb2f71309a165daeffe95355e1e44edd65bdf28088fe4f5e835a7d9f7569fc2530f9d17c00b51cfafbe421eb462247ea3',
};
this.text = text;
this.key = '0CoJUm6Qyw8W8jud';
}
getFormData() {
const i = "4JknCzx6uEXUwxpU";
const firstEncrypt = this.aesEncrypt(this.text, this.key);
this.data.params = this.aesEncrypt(firstEncrypt, i);
return this.data;
}
aesEncrypt(text, key) {
const iv = Buffer.from('0102030405060708');
let padding = 16 - text.length % 16;
text += String.fromCharCode(padding).repeat(padding);
const cipher = crypto.createCipheriv('aes-128-cbc', Buffer.from(key, 'utf-8'), iv);
return cipher.update(text, 'utf8', 'base64') + cipher.final('base64');
}
}
module.exports = Encrypt;
getFormData
方法:这个方法是获取加密后的表单数据的核心方法。它首先调用aesEncrypt
方法进行一次加密,然后再次调用aesEncrypt
方法进行第二次加密,以确保数据的安全性。aesEncrypt
方法:这个方法实现了AES加密算法,它接收文本和密钥作为参数,并返回加密后的文本。
main.js
main.js
文件是Electron应用的主进程文件,它负责创建应用窗口和处理IPC(Inter-Process Communication)消息。以下是文件中几个核心事件处理函数的代码和分析:
const { app, BrowserWindow, ipcMain } = require('electron');
const SearchMusic = require('./SearchMusic');
const NeteaseCloudMusic = require('./NeteaseCloudMusic');
const fs = require('fs');
const path = require('path');
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
},
});
win.loadFile('index.html');
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
ipcMain.on('search-song', async (event, songName) => {
const searchMusic = new SearchMusic(JSON.stringify({ s: songName, limit: 30, type: 1, csrf_token: '' }));
const songs = await searchMusic.search();
event.sender.send('search-result', songs);
});
ipcMain.on('download-song', async (event, song) => {
const neteaseCloudMusic = new NeteaseCloudMusic(song);
try {
await neteaseCloudMusic.music();
event.sender.send('download-status', '下载完成');
} catch (error) {
console.error(error);
event.sender.send('download-status', '下载失败: ' + error.message);
}
});
ipcMain.on('update-cookie', (event, newCookie) => {
try {
// 获取配置文件的路径
let configPath = path.resolve(__dirname, './headerConfig.js');
// 清除之前的模块缓存
delete require.cache[require.resolve('./headerConfig.js')];
// 重新加载配置文件
let config = require('./headerConfig.js');
// 更新cookie值
config.headers['cookie'] = newCookie;
// 将更新后的配置对象转换为字符串
let configData = "module.exports = " + JSON.stringify(config, null, 4) + ";";
// 写入更新后的配置对象到文件
fs.writeFileSync(configPath, configData);
// 回应渲染进程
event.reply('update-cookie-status', '更新成功');
} catch (error) {
console.error(error);
event.reply('update-cookie-status', '更新失败: ' + error.message);
}
});
createWindow
函数:这个函数负责创建应用的主窗口。它设置了窗口的初始大小和加载的HTML文件。- IPC事件处理函数:这些函数负责处理来自渲染进程的IPC消息。例如,
search-song
事件处理函数负责处理歌曲搜索请求,而download-song
事件处理函数则负责处理歌曲下载请求。
renderer.js
renderer.js
文件是Electron应用的渲染进程文件,它负责与HTML页面交互和处理来自主进程的IPC消息。以下是文件中几个核心事件监听器和IPC消息处理函数的代码和分析:
const { ipcRenderer } = require('electron');
function showAlert(title, text, icon) {
Swal.fire({ title, text, icon });
}
function toggleCookieInputDisplay() {
const cookieInput = document.getElementById('cookieInput');
cookieInput.style.display = cookieInput.style.display === 'none' ? 'block' : 'none';
}
document.getElementById('searchButton').addEventListener('click', () => {
const songName = document.getElementById('songName').value;
ipcRenderer.send('search-song', songName);
});
ipcRenderer.on('search-result', (event, songs) => {
const tableBody = document.getElementById('resultTable').getElementsByTagName('tbody')[0];
tableBody.innerHTML = '';
songs.forEach(song => {
const row = tableBody.insertRow();
row.insertCell(0).textContent = song.song_id;
row.insertCell(1).textContent = song.song_name;
row.insertCell(2).textContent = song.singer;
const downloadCell = row.insertCell(3);
const downloadButton = document.createElement('button');
downloadButton.textContent = '下载';
downloadButton.classList.add('btn', 'btn-success');
downloadButton.addEventListener('click', () => {
ipcRenderer.send('download-song', song);
});
downloadCell.appendChild(downloadButton);
});
});
document.getElementById('updateCookieButton').addEventListener('click', () => {
const cookieInput = document.getElementById('cookieInput');
if (cookieInput.style.display === 'none') {
cookieInput.style.display = 'block';
} else {
Swal.fire({
title: '你确定吗?',
text: "你即将更新Cookie。",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: '是的,更新它!',
cancelButtonText: '取消'
}).then((result) => {
if (result.isConfirmed) {
const newCookie = cookieInput.value;
ipcRenderer.send('update-cookie', newCookie);
toggleCookieInputDisplay();
} else {
toggleCookieInputDisplay();
}
});
}
});
ipcRenderer.on('update-cookie-status', (event, status) => {
const icon = status.includes('失败') ? 'error' : 'success';
showAlert('更新Cookie状态', status, icon);
});
ipcRenderer.on('download-status', (event, status) => {
const icon = status.includes('失败') ? 'error' : 'success';
showAlert('下载状态', status, icon);
});
- 事件监听器:这些监听器负责处理用户界面的交互。例如,搜索按钮的点击事件监听器负责获取用户输入的歌曲名并发送IPC消息到主进程进行搜索。
- IPC消息处理函数:这些函数负责响应主进程发送的IPC消息。例如,
search-result
消息处理函数负责接收主进程发送的搜索结果并更新UI。
NeteaseCloudMusic.js & SearchMusic.js
这两个文件分别定义了NeteaseCloudMusic
和SearchMusic
类,这两个类包含了与网易云音乐API交互的核心方法。在这两个类中,我使用Encrypt
类来加密请求数据,并使用axios
库来发送HTTP请求到API服务器。
NeteaseCloudMusic.js
const axios = require('axios');
const fs = require('fs');
const Encrypt = require('./Encrypt');
const config = require('./headerConfig');
class NeteaseCloudMusic {
constructor(song) {
this.url = 'https://music.163.com/weapi/song/enhance/player/url/v1?csrf_token=';
this.headers = config.headers;
this.text = `{"ids":"[${song.song_id}]","level":"standard","encodeType":"aac","csrf_token":""}`;
this.name = song.song_name;
this.singer = song.singer;
}
async music() {
const data = new Encrypt(this.text).getFormData();
try {
const res = await axios.post(this.url, data, { headers: this.headers });
const songUrl = res.data.data[0].url;
console.log(songUrl);
if (!songUrl) {
throw new Error('会员歌曲,没有链接');
}
const content = await this.download(songUrl);
this.save(content);
} catch (error) {
console.error(error);
throw error; // 抛出错误让外层的 catch 块捕获
}
}
async download(url) {
try {
const res = await axios({
url,
method: 'GET',
responseType: 'arraybuffer', // 说明我们期望得到一个二进制的buffer
});
return res.data;
} catch (error) {
console.error('Error downloading the song: ', error);
throw error;
}
}
save(content) {
if (content) {
const path = __dirname;
if (!fs.existsSync(`${path}/musicDownLoad`)) {
fs.mkdirSync(`${path}/musicDownLoad`);
}
const musicPath = `${path}/musicDownLoad/${this.name} ${this.singer}.m4a`;
if (!fs.existsSync(musicPath)) {
fs.writeFileSync(musicPath, content);
}
}
}
}
module.exports = NeteaseCloudMusic;
- 这部分代码主要是实现音乐的下载并保存到指定的 musicDownLoad 目录。
SearchMusic.js
const axios = require('axios');
const Encrypt = require('./Encrypt'); // Assuming Encrypt class is in 'Encrypt.js' file
const config = require('./headerConfig');
class SearchMusic {
constructor(text) {
this.url = 'https://music.163.com/weapi/cloudsearch/get/web?csrf_token=';
this.headers = config.headers;
this.text = text;
}
async search() {
const data = new Encrypt(this.text).getFormData();
try {
const res = await axios.post(this.url, data, { headers: this.headers });
const songList = res.data.result.songs.map(song => ({
song_id: song.id,
song_name: song.name,
singer: song.ar[0].name,
}));
return songList;
} catch (error) {
console.error(error);
}
}
}
module.exports = SearchMusic;