update 4.31.0

This commit is contained in:
ElyPrism 2026-04-17 20:14:15 +08:00
parent c50aeddf5f
commit 843c19f089
No known key found for this signature in database
15 changed files with 381 additions and 33 deletions

View File

@ -181,6 +181,12 @@ banner({ type: 0 }).then((res) => {
[文档地址](https://docs-neteasecloudmusicapi.vercel.app) [文档地址](https://docs-neteasecloudmusicapi.vercel.app)
## 调试工具
- `eapi` 请求参数或返回内容可在 `/eapi_decrypt.html` 里解析
- 请求参数模式下, 解密结果可直接带到 `/api.html` 继续调试
- 需要返回值加密时, 可传 `e_r=1`, `weapi` 和 `eapi` 都支持
## 功能特性 ## 功能特性
1. 登录 1. 登录
@ -320,7 +326,7 @@ banner({ type: 0 }).then((res) => {
135. 电台 - 节目榜 135. 电台 - 节目榜
136. 电台 - 新晋电台榜/热门电台榜 136. 电台 - 新晋电台榜/热门电台榜
137. 类别热门电台 137. 类别热门电台
138. 云村热评 138. 云村热评(官方下架,暂不能用)
139. 电台 24 小时节目榜 139. 电台 24 小时节目榜
140. 电台 24 小时主播榜 140. 电台 24 小时主播榜
141. 电台最热主播榜 141. 电台最热主播榜
@ -341,8 +347,8 @@ banner({ type: 0 }).then((res) => {
156. 国家编码列表 156. 国家编码列表
157. 首页-发现 157. 首页-发现
158. 首页-发现-圆形图标入口列表 158. 首页-发现-圆形图标入口列表
159. 数字专辑-全部新碟 159. 全部新碟
160. 数字专辑-热门新碟 160. 数字专辑-新碟上架
161. 数字专辑&数字单曲-榜单 161. 数字专辑&数字单曲-榜单
162. 数字专辑-语种风格馆 162. 数字专辑-语种风格馆
163. 数字专辑详情 163. 数字专辑详情
@ -408,10 +414,10 @@ banner({ type: 0 }).then((res) => {
223. 领取云豆 223. 领取云豆
224. 获取 VIP 信息 224. 获取 VIP 信息
225. 音乐人签到 225. 音乐人签到
226. 发送文本动态 226. 获取客户端歌曲下载 url
227. 获取客户端歌曲下载 url 227. 获取歌单所有歌曲
228. 获取歌单所有歌曲 228. 乐签信息
229. 乐签信息 229. 获取歌手视频
230. 最近播放-歌曲 230. 最近播放-歌曲
231. 最近播放-视频 231. 最近播放-视频
232. 最近播放-声音 232. 最近播放-声音
@ -442,12 +448,12 @@ banner({ type: 0 }).then((res) => {
257. 验证接口-二维码生成 257. 验证接口-二维码生成
258. 验证接口-二维码检测 258. 验证接口-二维码检测
259. 听歌识曲 259. 听歌识曲
260. 根据 nickname 获取 userid 接口 260. 根据nickname获取userid接口
261. 播客声音列表 261. 播客声音列表
262. 专辑简要百科信息 262. 专辑简要百科信息
263. 歌曲简要百科信息 263. 歌曲简要百科信息
264. 歌手简要百科信息 264. 歌手简要百科信息
265. mv 简要百科信息 265. mv简要百科信息
266. 搜索歌手 266. 搜索歌手
267. 用户贡献内容 267. 用户贡献内容
268. 用户贡献条目、积分、云贝数量 268. 用户贡献条目、积分、云贝数量
@ -504,6 +510,39 @@ banner({ type: 0 }).then((res) => {
319. DIFM电台 - 收藏频道 319. DIFM电台 - 收藏频道
320. DIFM电台 - 取消收藏频道 320. DIFM电台 - 取消收藏频道
321. DIFM电台 - 播放列表 321. DIFM电台 - 播放列表
322. 助眠解压 - 特定时间场景下的推荐资源
323. 助眠解压 - 标签列表
324. 助眠解压 - 获取标签下资源列表
325. 助眠解压 - 查看同类推荐
326. 助眠解压 - 收藏
327. 助眠解压 - 收藏列表
328. 跑步漫游
329. 歌曲创作者信息
330. 获取用户详情
331. 更新用户信息
332. 更新歌单
333. 歌手分类列表
334. 收藏/取消收藏歌手
335. 获取音乐 url
336. 获取音乐 url - 新版
337. 搜索多重匹配
338. 对歌单添加或删除歌曲
339. 获取逐字歌词
340. 资源点赞( MV,电台,视频)
341. 每日推荐歌曲-不感兴趣
342. mv 地址
343. 排行榜详情
344. 电台 - 付费精品
345. 电台 - 付费精选
346. 发送私信(带专辑)
347. 云贝
348. 一起听相关
349. 获取 VIP 信息(app端)
350. 公开隐私歌单
351. 云村星评馆 - 简要评论
352. 私人 DJ
353. 播客列表
354. 播客声音详情
## 单元测试 ## 单元测试

View File

@ -0,0 +1,9 @@
// 跑步漫游
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
bpm: query.bpm || 50,
}
return request(`/api/radio/sport/get`, data, createOption(query))
}

View File

@ -0,0 +1,11 @@
// 助眠解压 - 获取标签下资源列表
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
tag: query.tag,
firstQuery: false,
}
return request(`/api/voice/sati/resource/list`, data, createOption(query))
}

View File

@ -0,0 +1,13 @@
// 助眠解压 - 查看同类推荐
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
id: query.id,
}
return request(
`/api/voice/sati/resource/list/more/v1`,
data,
createOption(query),
)
}

View File

@ -0,0 +1,10 @@
// 助眠解压 - 收藏
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
id: query.id,
cancel: query.cancel || false,
}
return request(`/api/voice/sati/resource/sub`, data, createOption(query))
}

View File

@ -0,0 +1,7 @@
// 助眠解压 - 收藏列表
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {}
return request(`/api/voice/sati/resource/sub/list`, data, createOption(query))
}

7
module/sati_tag_list.js Normal file
View File

@ -0,0 +1,7 @@
// 助眠解压 - 标签列表
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {}
return request(`/api/voice/sati/tag/list`, data, createOption(query))
}

View File

@ -0,0 +1,13 @@
// 助眠解压 - 特定时间场景下的推荐资源
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
firstQuery: false,
}
return request(
`/api/voice/sati/timescene/resources/get`,
data,
createOption(query),
)
}

9
module/song_creators.js Normal file
View File

@ -0,0 +1,9 @@
// 歌曲创作者信息
const createOption = require('../util/option.js')
module.exports = (query, request) => {
const data = {
songId: query.id,
}
return request(`/api/song/creators`, data, createOption(query))
}

View File

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

View File

@ -122,6 +122,35 @@
document.getElementById('result').value = 'Request failed: ' + error.message; document.getElementById('result').value = 'Request failed: ' + error.message;
} }
} }
(function fillFromQuery() {
const params = new URLSearchParams(window.location.search);
if (!params.toString()) return;
const uri = params.get('uri');
const crypto = params.get('crypto');
const dataParam = params.get('data');
if (uri) {
document.getElementById('uri').value = uri;
}
if (crypto) {
const cryptoSelect = document.getElementById('crypto');
if ([...cryptoSelect.options].some((opt) => opt.value === crypto)) {
cryptoSelect.value = crypto;
}
}
if (dataParam) {
const decoded = dataParam;
try {
document.getElementById('data').value = JSON.stringify(
JSON.parse(decoded),
null,
2,
);
} catch (error) {
document.getElementById('data').value = decoded;
}
}
})();
</script> </script>
</body> </body>
</html> </html>

View File

@ -337,6 +337,39 @@
319. DIFM电台 - 收藏频道 319. DIFM电台 - 收藏频道
320. DIFM电台 - 取消收藏频道 320. DIFM电台 - 取消收藏频道
321. DIFM电台 - 播放列表 321. DIFM电台 - 播放列表
322. 助眠解压 - 特定时间场景下的推荐资源
323. 助眠解压 - 标签列表
324. 助眠解压 - 获取标签下资源列表
325. 助眠解压 - 查看同类推荐
326. 助眠解压 - 收藏
327. 助眠解压 - 收藏列表
328. 跑步漫游
329. 歌曲创作者信息
330. 获取用户详情
331. 更新用户信息
332. 更新歌单
333. 歌手分类列表
334. 收藏/取消收藏歌手
335. 获取音乐 url
336. 获取音乐 url - 新版
337. 搜索多重匹配
338. 对歌单添加或删除歌曲
339. 获取逐字歌词
340. 资源点赞( MV,电台,视频)
341. 每日推荐歌曲-不感兴趣
342. mv 地址
343. 排行榜详情
344. 电台 - 付费精品
345. 电台 - 付费精选
346. 发送私信(带专辑)
347. 云贝
348. 一起听相关
349. 获取 VIP 信息(app端)
350. 公开隐私歌单
351. 云村星评馆 - 简要评论
352. 私人 DJ
353. 播客列表
354. 播客声音详情
## 安装 ## 安装
@ -390,6 +423,12 @@ npx NeteaseCloudMusicApi@latest
``` ```
此命令每次执行都会使用最新版 此命令每次执行都会使用最新版
## 调试工具
- `eapi` 请求参数或返回内容可在 `/eapi_decrypt.html` 里解析
- 请求参数模式下, 解密结果可直接带到 `/api.html` 继续调试
- 需要返回值加密时, 可传 `e_r=1`, `weapi``eapi` 都支持
## Vercel 部署 ## Vercel 部署
v4.0.8 加入了 Vercel 配置文件,可以直接在 Vercel 下部署了,不需要自己的服务器(访问 Vercel 部署的接口,需要额外加一个 realIP 参数,如 `/song/url?id=191254&realIP=116.25.146.177`) v4.0.8 加入了 Vercel 配置文件,可以直接在 Vercel 下部署了,不需要自己的服务器(访问 Vercel 部署的接口,需要额外加一个 realIP 参数,如 `/song/url?id=191254&realIP=116.25.146.177`)
@ -5144,6 +5183,20 @@ let data = encodeURIComponent(
**调用例子:** `/broadcast/channel/list` **调用例子:** `/broadcast/channel/list`
### 广播电台 - 收藏/取消收藏电台
说明: 登录后调用此接口, 传入电台 id, 可收藏或取消收藏广播电台
**必选参数:**
`id`: 电台 id
`t`: 操作类型, `1` 为收藏, 其余值为取消收藏
**接口地址:** `/broadcast/sub`
**调用例子:** `/broadcast/sub?id=5&t=1`
### 用户的创建歌单列表 ### 用户的创建歌单列表
说明 : 调用此接口, 传入用户id, 获取用户的创建歌单列表 说明 : 调用此接口, 传入用户id, 获取用户的创建歌单列表
@ -5246,6 +5299,92 @@ let data = encodeURIComponent(
**调用例子:** `/dj/difm/playing/tracks/list?source=0&channelId=1012` **调用例子:** `/dj/difm/playing/tracks/list?source=0&channelId=1012`
### 助眠解压 - 特定时间场景下的推荐资源
说明: 调用此接口, 获取特定时间场景下的推荐资源
**接口地址:** `/sati/timescene/resources/get`
**调用例子:** `/sati/timescene/resources/get`
### 助眠解压 - 标签列表
说明: 调用此接口, 获取标签列表
**接口地址:** `/sati/tag/list`
**调用例子:** `/sati/tag/list`
### 助眠解压 - 获取标签下资源列表
说明: 调用此接口, 获取标签下资源列表; 接口返回的`trackId`可以用于请求`/song/url/v1`接口,用于获取声音的下载地址
**必选参数 :**
`tag`: 标签, 由标签列表接口得到
**接口地址:** `/sati/resource/list`
**调用例子:** `/sati/resource/list?tag=naturalMusic`
### 助眠解压 - 查看同类推荐
说明: 调用此接口, 查看同类推荐
**必选参数 :**
`id`: id, `/sati/tag/list`接口返回的`trackId`
**接口地址:** `/sati/resource/list/more`
**调用例子:** `/sati/resource/list/more?id=167003`
### 助眠解压 - 收藏列表
说明: 调用此接口, 获取收藏列表
**接口地址:** `/sati/resource/sub/list`
**调用例子:** `/sati/resource/sub/list`
### 助眠解压 - 收藏
说明: 调用此接口, 收藏声音
**必选参数 :**
`id`: id, `/sati/tag/list`接口返回的`trackId`
**可选参数 :**
`cancel`: 是否取消收藏, 默认不取消
**接口地址:** `/sati/resource/sub`
**调用例子:** `/sati/resource/sub?id=167003`
### 跑步漫游
说明: 调用此接口,获取跑步漫游的歌曲信息
**必选参数:**
`bpm`: 步频
**接口地址:** `/radio/sport/get`
**调用例子:** `/radio/sport/get?bpm=50`
### 歌曲创作者信息
说明 : 调用此接口, 传入音乐 id 可获得对应音乐的创作者信息
**必选参数 :** `id`: 音乐 id
**接口地址 :** `/song/creators`
**调用例子 :** `/song/creators?id=33894312`
## 离线访问此文档 ## 离线访问此文档
此文档同时也是 Progressive Web Apps(PWA), 加入了 serviceWorker, 可离线访问 此文档同时也是 Progressive Web Apps(PWA), 加入了 serviceWorker, 可离线访问

View File

@ -25,9 +25,21 @@
<div id="app" class="p-5 flex flex-col"> <div id="app" class="p-5 flex flex-col">
<h1 class="text-2xl font-bold mb-5">eapi 参数和返回内容解析</h1> <h1 class="text-2xl font-bold mb-5">eapi 参数和返回内容解析</h1>
<textarea class="border border-gray-300 p-3 mb-5" v-model="hexString" rows="10"></textarea> <textarea class="border border-gray-300 p-3 mb-5" v-model="hexString" rows="10"></textarea>
<div class="flex gap-3 mb-4">
<button @click="decrypt" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"> <button @click="decrypt" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
解密 解密
</button> </button>
<button
@click="sendToApi"
:disabled="!canSend"
:class="[
'bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded',
{ 'opacity-50 cursor-not-allowed pointer-events-none': !canSend },
]"
>
填入 API 调试
</button>
</div>
<div class="mt-3"> <div class="mt-3">
<input type="radio" id="format" name="format" v-model="isReq" value="true"> <input type="radio" id="format" name="format" v-model="isReq" value="true">
<label for="format" class="ml-2">请求数据request params(针对请求数据的 params)</label> <label for="format" class="ml-2">请求数据request params(针对请求数据的 params)</label>
@ -63,6 +75,21 @@
mounted() { mounted() {
this.decrypt() this.decrypt()
}, },
computed: {
isRequestMode() {
return this.isReq === true || this.isReq === 'true'
},
canSend() {
if (!this.isRequestMode) return false
if (!this.result || this.result === '{}' || this.result === 'null') return false
try {
JSON.parse(this.result)
return true
} catch (error) {
return false
}
},
},
methods: { methods: {
async decrypt() { async decrypt() {
try { try {
@ -79,7 +106,23 @@
console.error(error) console.error(error)
alert(error?.response?.data?.message || '解密失败,数据格式错误') alert(error?.response?.data?.message || '解密失败,数据格式错误')
} }
} },
sendToApi() {
if (!this.canSend) return
const payload = JSON.parse(this.result)
const params = new URLSearchParams()
params.set('uri', payload.uri || payload.url || payload.path || '')
params.set('crypto', 'eapi')
const data =
payload.params ||
payload.data ||
payload.body ||
payload.payload ||
payload.request ||
{}
params.set('data', JSON.stringify(data))
window.open(`/api.html?${params.toString()}`, '_blank')
},
} }
}) })
app.mount('#app') app.mount('#app')

View File

@ -1,5 +1,6 @@
const CryptoJS = require('crypto-js') const CryptoJS = require('crypto-js')
const forge = require('node-forge') const forge = require('node-forge')
const zlib = require('zlib')
const iv = '0102030405060708' const iv = '0102030405060708'
const presetKey = '0CoJUm6Qyw8W8jud' const presetKey = '0CoJUm6Qyw8W8jud'
const linuxapiKey = 'rFgB&h#%2?^eDg:Q' const linuxapiKey = 'rFgB&h#%2?^eDg:Q'
@ -44,7 +45,7 @@ const aesDecrypt = (ciphertext, key, iv, format = 'base64') => {
}, },
) )
} }
return bytes.toString(CryptoJS.enc.Utf8) return bytes
} }
const rsaEncrypt = (str, key) => { const rsaEncrypt = (str, key) => {
const forgePublicKey = forge.pki.publicKeyFromPem(key) const forgePublicKey = forge.pki.publicKeyFromPem(key)
@ -85,20 +86,37 @@ const eapi = (url, object) => {
params: aesEncrypt(data, 'ecb', eapiKey, '', 'hex'), params: aesEncrypt(data, 'ecb', eapiKey, '', 'hex'),
} }
} }
const eapiResDecrypt = (encryptedParams) => { const eapiResDecrypt = (encryptedParams, aeapi = false) => {
// 使用aesDecrypt解密参数 // 使用aesDecrypt解密参数
try { try {
const decryptedData = aesDecrypt(encryptedParams, eapiKey, '', 'hex') const decrypted = aesDecrypt(encryptedParams, eapiKey, '', 'hex') // WordArray
return JSON.parse(decryptedData)
if (aeapi) {
// 带压缩的解密:先转 Base64 再解压
const decryptedBuffer = Buffer.from(
decrypted.toString(CryptoJS.enc.Base64),
'base64',
)
const decompressed = zlib.gunzipSync(decryptedBuffer)
return JSON.parse(decompressed.toString())
} else {
// 普通解密:直接转 UTF-8 字符串
return JSON.parse(decrypted.toString(CryptoJS.enc.Utf8))
}
} catch (error) { } catch (error) {
console.log('eapiResDecrypt error:', error) console.log(`eapiResDecrypt error:`, error)
return null return null
} }
} }
const eapiReqDecrypt = (encryptedParams) => { const eapiReqDecrypt = (encryptedParams) => {
// 使用aesDecrypt解密参数 // 使用 aesDecrypt 解密参数
const decryptedData = aesDecrypt(encryptedParams, eapiKey, '', 'hex') const decryptedData = aesDecrypt(
// 使用正则表达式解析出URL和数据 encryptedParams,
eapiKey,
'',
'hex',
).toString(CryptoJS.enc.Utf8)
// 使用正则表达式解析出 URL 和数据
const match = decryptedData.match(/(.*?)-36cd479b6b5-(.*?)-36cd479b6b5-(.*)/) const match = decryptedData.match(/(.*?)-36cd479b6b5-(.*?)-36cd479b6b5-(.*)/)
if (match) { if (match) {
const url = match[1] const url = match[1]
@ -106,7 +124,7 @@ const eapiReqDecrypt = (encryptedParams) => {
return { url, data } return { url, data }
} }
// 如果没有匹配到,返回null // 如果没有匹配到,返回 null
return null return null
} }
const decrypt = (cipher) => { const decrypt = (cipher) => {

View File

@ -188,6 +188,13 @@ const createRequest = (uri, data, options) => {
const answer = { status: 500, body: {}, cookie: [] } const answer = { status: 500, body: {}, cookie: [] }
data.e_r = toBoolean(
options.e_r !== undefined
? options.e_r
: data.e_r !== undefined
? data.e_r
: ENCRYPT_RESPONSE,
)
// 根据加密方式处理 // 根据加密方式处理
switch (crypto) { switch (crypto) {
case 'weapi': case 'weapi':
@ -239,13 +246,6 @@ const createRequest = (uri, data, options) => {
if (crypto === 'eapi') { if (crypto === 'eapi') {
// headers['x-aeapi'] = true // 服务器会使用gzip压缩返回值 // headers['x-aeapi'] = true // 服务器会使用gzip压缩返回值
data.header = header data.header = header
data.e_r = toBoolean(
options.e_r !== undefined
? options.e_r
: data.e_r !== undefined
? data.e_r
: ENCRYPT_RESPONSE,
)
encryptData = encrypt.eapi(uri, data) encryptData = encrypt.eapi(uri, data)
url = (options.domain || API_DOMAIN) + '/eapi/' + uri.substr(5) url = (options.domain || API_DOMAIN) + '/eapi/' + uri.substr(5)
} else if (crypto === 'api') { } else if (crypto === 'api') {
@ -269,8 +269,9 @@ const createRequest = (uri, data, options) => {
httpsAgent: createHttpsAgent(), httpsAgent: createHttpsAgent(),
} }
// e_r处理 // 使用返回值加密
if (data.e_r) { const use_e_r = (crypto === 'eapi' || crypto === 'weapi') && data.e_r
if (use_e_r) {
settings.encoding = null settings.encoding = null
settings.responseType = 'arraybuffer' settings.responseType = 'arraybuffer'
} }
@ -318,7 +319,7 @@ const createRequest = (uri, data, options) => {
) )
try { try {
if (crypto === 'eapi' && data.e_r) { if (use_e_r) {
answer.body = encrypt.eapiResDecrypt( answer.body = encrypt.eapiResDecrypt(
body.toString('hex').toUpperCase(), body.toString('hex').toUpperCase(),
headers['x-aeapi'], headers['x-aeapi'],