This commit is contained in:
MagicalKudzu
2026-02-03 16:10:12 +08:00
parent 787538238e
commit d1ac4eef3d
8 changed files with 215 additions and 177 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -22,7 +22,7 @@
## 环境要求
需要 NodeJS 14+ 环境
需要 NodeJS 18+ 环境
## 安装
@@ -70,6 +70,45 @@ npx NeteaseCloudMusicApi@latest
使用此命令,可直接启动服务,无需下载或者 clone 项目
## Docker 容器运行
> 注意: 在 docker 中运行的时候, 由于使用了 request 来发请求, 所以会检查几个 proxy 相关的环境变量(如下所列), 这些环境变量 会影响到 request 的代理, 详情请参考[request 的文档](https://github.com/request/request#proxies), 如果这些环境变量 指向的代理不可用, 那么就会造成错误, 所以在使用 docker 的时候一定要注意这些环境变量. 不过, 要是你在 query 中加上了 proxy 参数, 那么环境变量会被覆盖, 就会用你通过 proxy 参数提供的代理了.
request 相关的环境变量
1. http_proxy
2. https_proxy
3. HTTP_PROXY
4. HTTPS_PROXY
5. no_proxy
6. NO_PROXY
```shell
docker pull binaryify/netease_cloud_music_api
docker run -d -p 3000:3000 --name netease_cloud_music_api binaryify/netease_cloud_music_api
// 或者
docker run -d -p 3000:3000 binaryify/netease_cloud_music_api
// 去掉或者设置相关的环境变量
docker run -d -p 3000:3000 --name netease_cloud_music_api -e http_proxy= -e https_proxy= -e no_proxy= -e HTTP_PROXY= -e HTTPS_PROXY= -e NO_PROXY= binaryify/netease_cloud_music_api
// 或者
docker run -d -p 3000:3000 -e http_proxy= -e https_proxy= -e no_proxy= -e HTTP_PROXY= -e HTTPS_PROXY= -e NO_PROXY= binaryify/netease_cloud_music_api
```
> 以下是自行 build docker 镜像方式
```
$ git clone https://gitlab.com/Binaryify/neteasecloudmusicapi.git && cd NeteaseCloudMusicApi
$ sudo docker build . -t netease-music-api
$ sudo docker run -d -p 3000:3000 netease-music-api
## Vercel 部署
v4.0.8 加入了 Vercel 配置文件,可以直接在 Vercel 下部署了,不需要自己的服务器

View File

@@ -18,7 +18,7 @@ module.exports = async (query, request) => {
let result = await request(
`/api/w/login/cellphone`,
data,
createOption(query),
createOption(query, 'weapi'),
)
if (result.body.code === 200) {

View File

@@ -10,9 +10,9 @@ case `uname` in
esac
if [ -z "$NODE_PATH" ]; then
export NODE_PATH="/Users/kudzu/Library/pnpm/global/5/.pnpm/NeteaseCloudMusicApi@4.28.0_c0457443e151ba9902c455ed8f2f0b0f/node_modules/NeteaseCloudMusicApi/node_modules:/Users/kudzu/Library/pnpm/global/5/.pnpm/NeteaseCloudMusicApi@4.28.0_c0457443e151ba9902c455ed8f2f0b0f/node_modules:/Users/kudzu/Library/pnpm/global/5/.pnpm/node_modules"
export NODE_PATH="/Users/kudzu/Library/pnpm/global/5/.pnpm/NeteaseCloudMusicApi@4.28.1_c316bfca3d49c7c85c2b0b37dab8c4e4/node_modules/NeteaseCloudMusicApi/node_modules:/Users/kudzu/Library/pnpm/global/5/.pnpm/NeteaseCloudMusicApi@4.28.1_c316bfca3d49c7c85c2b0b37dab8c4e4/node_modules:/Users/kudzu/Library/pnpm/global/5/.pnpm/node_modules"
else
export NODE_PATH="/Users/kudzu/Library/pnpm/global/5/.pnpm/NeteaseCloudMusicApi@4.28.0_c0457443e151ba9902c455ed8f2f0b0f/node_modules/NeteaseCloudMusicApi/node_modules:/Users/kudzu/Library/pnpm/global/5/.pnpm/NeteaseCloudMusicApi@4.28.0_c0457443e151ba9902c455ed8f2f0b0f/node_modules:/Users/kudzu/Library/pnpm/global/5/.pnpm/node_modules:$NODE_PATH"
export NODE_PATH="/Users/kudzu/Library/pnpm/global/5/.pnpm/NeteaseCloudMusicApi@4.28.1_c316bfca3d49c7c85c2b0b37dab8c4e4/node_modules/NeteaseCloudMusicApi/node_modules:/Users/kudzu/Library/pnpm/global/5/.pnpm/NeteaseCloudMusicApi@4.28.1_c316bfca3d49c7c85c2b0b37dab8c4e4/node_modules:/Users/kudzu/Library/pnpm/global/5/.pnpm/node_modules:$NODE_PATH"
fi
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../../app.js" "$@"

View File

@@ -1,6 +1,6 @@
{
"name": "NeteaseCloudMusicApi",
"version": "4.28.0",
"version": "4.28.1",
"description": "网易云音乐 NodeJS 版 API",
"scripts": {
"start": "node app.js",

BIN
public/.DS_Store vendored

Binary file not shown.

View File

@@ -1,169 +1,169 @@
const crypto = require("crypto");
const os = require("os");
class AdvancedClientSignGenerator {
/**
* 获取本机MAC地址
*/
static getRealMacAddress() {
try {
const interfaces = os.networkInterfaces();
for (let interfaceName in interfaces) {
const interface = interfaces[interfaceName];
for (let i = 0; i < interface.length; i++) {
const alias = interface[i];
// 排除内部地址和无效地址
if (
alias.mac &&
alias.mac !== "00:00:00:00:00:00" &&
!alias.internal
) {
return alias.mac.toUpperCase();
}
}
}
return null;
} catch (error) {
console.warn("获取MAC地址失败:", error.message);
return null;
}
}
/**
* 生成随机MAC地址
*/
static generateRandomMac() {
const chars = "0123456789ABCDEF";
let mac = "";
for (let i = 0; i < 6; i++) {
if (i > 0) mac += ":";
mac +=
chars[Math.floor(Math.random() * 16)] +
chars[Math.floor(Math.random() * 16)];
}
// 确保第一个字节是单播地址最低位为0
const firstByte = parseInt(mac.substring(0, 2), 16);
const unicastFirstByte = (firstByte & 0xfe)
.toString(16)
.padStart(2, "0")
.toUpperCase();
return unicastFirstByte + mac.substring(2);
}
/**
* 获取MAC地址优先真实否则随机
*/
static getMacAddress() {
const realMac = this.getRealMacAddress();
if (realMac) {
return realMac;
}
console.warn("无法获取真实MAC地址使用随机生成");
return this.generateRandomMac();
}
/**
* 字符串转HEX编码
*/
static stringToHex(str) {
return Buffer.from(str, "utf8").toString("hex").toUpperCase();
}
/**
* SHA-256哈希
*/
static sha256(data) {
return crypto.createHash("sha256").update(data, "utf8").digest("hex");
}
/**
* 生成随机设备ID
*/
static generateRandomDeviceId() {
const partLengths = [4, 4, 4, 4, 4, 4, 4, 5]; // 各部分长度
const chars = "0123456789ABCDEF";
const parts = partLengths.map((length) => {
let part = "";
for (let i = 0; i < length; i++) {
part += chars[Math.floor(Math.random() * 16)];
}
return part;
});
return parts.join("_");
}
/**
* 生成随机clientSign优先使用真实MAC否则随机
*/
static generateRandomClientSign(secretKey = "") {
// 获取MAC地址优先真实否则随机
const macAddress = this.getMacAddress();
// 生成随机设备ID
const deviceId = this.generateRandomDeviceId();
// 转换设备ID为HEX
const hexDeviceId = this.stringToHex(deviceId);
// 构造签名字符串
const signString = `${macAddress}@@@${hexDeviceId}`;
// 生成哈希
const hash = this.sha256(signString + secretKey);
return `${signString}@@@@@@${hash}`;
}
/**
* 批量生成多个随机签名
*/
static generateMultipleRandomSigns(count, secretKey = "") {
const signs = [];
for (let i = 0; i < count; i++) {
signs.push(this.generateRandomClientSign(secretKey));
}
return signs;
}
/**
* 使用指定参数生成签名
*/
static generateWithCustomDeviceId(macAddress, deviceId, secretKey = "") {
const hexDeviceId = this.stringToHex(deviceId);
const signString = `${macAddress}@@@${hexDeviceId}`;
const hash = this.sha256(signString + secretKey);
return `${signString}@@@@@@${hash}`;
}
/**
* 验证签名格式是否正确
*/
static validateClientSign(clientSign) {
try {
const parts = clientSign.split("@@@@@@");
if (parts.length !== 2) return false;
const [infoPart, hash] = parts;
const infoParts = infoPart.split("@@@");
if (infoParts.length !== 2) return false;
const [mac, hexDeviceId] = infoParts;
// 验证MAC地址格式
const macRegex = /^([0-9A-F]{2}:){5}[0-9A-F]{2}$/;
if (!macRegex.test(mac)) return false;
// 验证哈希格式
const hashRegex = /^[0-9a-f]{64}$/;
if (!hashRegex.test(hash)) return false;
return true;
} catch (error) {
return false;
}
}
}
module.exports = AdvancedClientSignGenerator;
const crypto = require('crypto')
const os = require('os')
class AdvancedClientSignGenerator {
/**
* 获取本机MAC地址
*/
static getRealMacAddress() {
try {
const interfaces = os.networkInterfaces()
for (let interfaceName in interfaces) {
const interfaceItem = interfaces[interfaceName]
for (let i = 0; i < interfaceItem.length; i++) {
const alias = interfaceItem[i]
// 排除内部地址和无效地址
if (
alias.mac &&
alias.mac !== '00:00:00:00:00:00' &&
!alias.internal
) {
return alias.mac.toUpperCase()
}
}
}
return null
} catch (error) {
console.warn('获取MAC地址失败:', error.message)
return null
}
}
/**
* 生成随机MAC地址
*/
static generateRandomMac() {
const chars = '0123456789ABCDEF'
let mac = ''
for (let i = 0; i < 6; i++) {
if (i > 0) mac += ':'
mac +=
chars[Math.floor(Math.random() * 16)] +
chars[Math.floor(Math.random() * 16)]
}
// 确保第一个字节是单播地址最低位为0
const firstByte = parseInt(mac.substring(0, 2), 16)
const unicastFirstByte = (firstByte & 0xfe)
.toString(16)
.padStart(2, '0')
.toUpperCase()
return unicastFirstByte + mac.substring(2)
}
/**
* 获取MAC地址优先真实否则随机
*/
static getMacAddress() {
const realMac = this.getRealMacAddress()
if (realMac) {
return realMac
}
console.warn('无法获取真实MAC地址使用随机生成')
return this.generateRandomMac()
}
/**
* 字符串转HEX编码
*/
static stringToHex(str) {
return Buffer.from(str, 'utf8').toString('hex').toUpperCase()
}
/**
* SHA-256哈希
*/
static sha256(data) {
return crypto.createHash('sha256').update(data, 'utf8').digest('hex')
}
/**
* 生成随机设备ID
*/
static generateRandomDeviceId() {
const partLengths = [4, 4, 4, 4, 4, 4, 4, 5] // 各部分长度
const chars = '0123456789ABCDEF'
const parts = partLengths.map((length) => {
let part = ''
for (let i = 0; i < length; i++) {
part += chars[Math.floor(Math.random() * 16)]
}
return part
})
return parts.join('_')
}
/**
* 生成随机clientSign优先使用真实MAC否则随机
*/
static generateRandomClientSign(secretKey = '') {
// 获取MAC地址优先真实否则随机
const macAddress = this.getMacAddress()
// 生成随机设备ID
const deviceId = this.generateRandomDeviceId()
// 转换设备ID为HEX
const hexDeviceId = this.stringToHex(deviceId)
// 构造签名字符串
const signString = `${macAddress}@@@${hexDeviceId}`
// 生成哈希
const hash = this.sha256(signString + secretKey)
return `${signString}@@@@@@${hash}`
}
/**
* 批量生成多个随机签名
*/
static generateMultipleRandomSigns(count, secretKey = '') {
const signs = []
for (let i = 0; i < count; i++) {
signs.push(this.generateRandomClientSign(secretKey))
}
return signs
}
/**
* 使用指定参数生成签名
*/
static generateWithCustomDeviceId(macAddress, deviceId, secretKey = '') {
const hexDeviceId = this.stringToHex(deviceId)
const signString = `${macAddress}@@@${hexDeviceId}`
const hash = this.sha256(signString + secretKey)
return `${signString}@@@@@@${hash}`
}
/**
* 验证签名格式是否正确
*/
static validateClientSign(clientSign) {
try {
const parts = clientSign.split('@@@@@@')
if (parts.length !== 2) return false
const [infoPart, hash] = parts
const infoParts = infoPart.split('@@@')
if (infoParts.length !== 2) return false
const [mac, hexDeviceId] = infoParts
// 验证MAC地址格式
const macRegex = /^([0-9A-F]{2}:){5}[0-9A-F]{2}$/
if (!macRegex.test(mac)) return false
// 验证哈希格式
const hashRegex = /^[0-9a-f]{64}$/
if (!hashRegex.test(hash)) return false
return true
} catch (error) {
return false
}
}
}
module.exports = AdvancedClientSignGenerator

View File

@@ -100,7 +100,7 @@ const SPECIAL_STATUS_CODES = new Set([201, 302, 400, 502, 800, 801, 802, 803])
// chooseUserAgent函数
const chooseUserAgent = (crypto, uaType = 'pc') => {
return userAgentMap[crypto]?.[uaType] || ''
return (userAgentMap[crypto] && userAgentMap[crypto][uaType]) || ''
}
// cookie处理
@@ -152,8 +152,7 @@ const createHeaderCookie = (header) => {
const generateRequestId = () => {
return `${now()}_${floor(random() * 1000)
.toString()
.padStart(4, "0")}`;
.padStart(4, '0')}`
}
const createRequest = (uri, data, options) => {