From 843c19f089852402785db182b05aa693d1250362 Mon Sep 17 00:00:00 2001 From: MoeFurina Date: Fri, 17 Apr 2026 20:14:15 +0800 Subject: [PATCH] update 4.31.0 --- README.MD | 57 ++++++++-- module/radio_sport_get.js | 9 ++ module/sati_resource_list.js | 11 ++ module/sati_resource_list_more.js | 13 +++ module/sati_resource_sub.js | 10 ++ module/sati_resource_sub_list.js | 7 ++ module/sati_tag_list.js | 7 ++ module/sati_timescene_resources_get.js | 13 +++ module/song_creators.js | 9 ++ package.json | 2 +- public/api.html | 29 ++++++ public/docs/home.md | 139 +++++++++++++++++++++++++ public/eapi_decrypt.html | 51 ++++++++- util/crypto.js | 36 +++++-- util/request.js | 21 ++-- 15 files changed, 381 insertions(+), 33 deletions(-) create mode 100644 module/radio_sport_get.js create mode 100644 module/sati_resource_list.js create mode 100644 module/sati_resource_list_more.js create mode 100644 module/sati_resource_sub.js create mode 100644 module/sati_resource_sub_list.js create mode 100644 module/sati_tag_list.js create mode 100644 module/sati_timescene_resources_get.js create mode 100644 module/song_creators.js diff --git a/README.MD b/README.MD index 096b961..64022ed 100644 --- a/README.MD +++ b/README.MD @@ -181,6 +181,12 @@ banner({ type: 0 }).then((res) => { [文档地址](https://docs-neteasecloudmusicapi.vercel.app) +## 调试工具 + +- `eapi` 请求参数或返回内容可在 `/eapi_decrypt.html` 里解析 +- 请求参数模式下, 解密结果可直接带到 `/api.html` 继续调试 +- 需要返回值加密时, 可传 `e_r=1`, `weapi` 和 `eapi` 都支持 + ## 功能特性 1. 登录 @@ -320,7 +326,7 @@ banner({ type: 0 }).then((res) => { 135. 电台 - 节目榜 136. 电台 - 新晋电台榜/热门电台榜 137. 类别热门电台 -138. 云村热评 +138. 云村热评(官方下架,暂不能用) 139. 电台 24 小时节目榜 140. 电台 24 小时主播榜 141. 电台最热主播榜 @@ -341,8 +347,8 @@ banner({ type: 0 }).then((res) => { 156. 国家编码列表 157. 首页-发现 158. 首页-发现-圆形图标入口列表 -159. 数字专辑-全部新碟 -160. 数字专辑-热门新碟 +159. 全部新碟 +160. 数字专辑-新碟上架 161. 数字专辑&数字单曲-榜单 162. 数字专辑-语种风格馆 163. 数字专辑详情 @@ -408,10 +414,10 @@ banner({ type: 0 }).then((res) => { 223. 领取云豆 224. 获取 VIP 信息 225. 音乐人签到 -226. 发送文本动态 -227. 获取客户端歌曲下载 url -228. 获取歌单所有歌曲 -229. 乐签信息 +226. 获取客户端歌曲下载 url +227. 获取歌单所有歌曲 +228. 乐签信息 +229. 获取歌手视频 230. 最近播放-歌曲 231. 最近播放-视频 232. 最近播放-声音 @@ -442,12 +448,12 @@ banner({ type: 0 }).then((res) => { 257. 验证接口-二维码生成 258. 验证接口-二维码检测 259. 听歌识曲 -260. 根据 nickname 获取 userid 接口 +260. 根据nickname获取userid接口 261. 播客声音列表 262. 专辑简要百科信息 263. 歌曲简要百科信息 264. 歌手简要百科信息 -265. mv 简要百科信息 +265. mv简要百科信息 266. 搜索歌手 267. 用户贡献内容 268. 用户贡献条目、积分、云贝数量 @@ -504,6 +510,39 @@ banner({ type: 0 }).then((res) => { 319. DIFM电台 - 收藏频道 320. 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. 播客声音详情 ## 单元测试 diff --git a/module/radio_sport_get.js b/module/radio_sport_get.js new file mode 100644 index 0000000..acba2ab --- /dev/null +++ b/module/radio_sport_get.js @@ -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)) +} diff --git a/module/sati_resource_list.js b/module/sati_resource_list.js new file mode 100644 index 0000000..f36dc64 --- /dev/null +++ b/module/sati_resource_list.js @@ -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)) +} diff --git a/module/sati_resource_list_more.js b/module/sati_resource_list_more.js new file mode 100644 index 0000000..5faa3f4 --- /dev/null +++ b/module/sati_resource_list_more.js @@ -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), + ) +} diff --git a/module/sati_resource_sub.js b/module/sati_resource_sub.js new file mode 100644 index 0000000..710df63 --- /dev/null +++ b/module/sati_resource_sub.js @@ -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)) +} diff --git a/module/sati_resource_sub_list.js b/module/sati_resource_sub_list.js new file mode 100644 index 0000000..9d41981 --- /dev/null +++ b/module/sati_resource_sub_list.js @@ -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)) +} diff --git a/module/sati_tag_list.js b/module/sati_tag_list.js new file mode 100644 index 0000000..a8fd6f1 --- /dev/null +++ b/module/sati_tag_list.js @@ -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)) +} diff --git a/module/sati_timescene_resources_get.js b/module/sati_timescene_resources_get.js new file mode 100644 index 0000000..6f6a56a --- /dev/null +++ b/module/sati_timescene_resources_get.js @@ -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), + ) +} diff --git a/module/song_creators.js b/module/song_creators.js new file mode 100644 index 0000000..835cabc --- /dev/null +++ b/module/song_creators.js @@ -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)) +} diff --git a/package.json b/package.json index 106055a..15765b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "NeteaseCloudMusicApi", - "version": "4.30.0", + "version": "4.31.0", "description": "网易云音乐 NodeJS 版 API", "scripts": { "start": "node app.js", diff --git a/public/api.html b/public/api.html index ee0c9d5..0c185c3 100644 --- a/public/api.html +++ b/public/api.html @@ -122,6 +122,35 @@ 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; + } + } + })(); diff --git a/public/docs/home.md b/public/docs/home.md index 3134753..1188ed2 100644 --- a/public/docs/home.md +++ b/public/docs/home.md @@ -337,6 +337,39 @@ 319. DIFM电台 - 收藏频道 320. 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 部署 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` +### 广播电台 - 收藏/取消收藏电台 + +说明: 登录后调用此接口, 传入电台 id, 可收藏或取消收藏广播电台 + +**必选参数:** + +`id`: 电台 id + +`t`: 操作类型, `1` 为收藏, 其余值为取消收藏 + +**接口地址:** `/broadcast/sub` + +**调用例子:** `/broadcast/sub?id=5&t=1` + ### 用户的创建歌单列表 说明 : 调用此接口, 传入用户id, 获取用户的创建歌单列表 @@ -5246,6 +5299,92 @@ let data = encodeURIComponent( **调用例子:** `/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, 可离线访问 diff --git a/public/eapi_decrypt.html b/public/eapi_decrypt.html index ef639d5..9c973e3 100644 --- a/public/eapi_decrypt.html +++ b/public/eapi_decrypt.html @@ -25,9 +25,21 @@

eapi 参数和返回内容解析

- +
+ + +
@@ -63,6 +75,21 @@ mounted() { 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: { async decrypt() { try { @@ -79,7 +106,23 @@ console.error(error) 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') diff --git a/util/crypto.js b/util/crypto.js index 733d89a..a6301e7 100644 --- a/util/crypto.js +++ b/util/crypto.js @@ -1,5 +1,6 @@ const CryptoJS = require('crypto-js') const forge = require('node-forge') +const zlib = require('zlib') const iv = '0102030405060708' const presetKey = '0CoJUm6Qyw8W8jud' 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 forgePublicKey = forge.pki.publicKeyFromPem(key) @@ -85,20 +86,37 @@ const eapi = (url, object) => { params: aesEncrypt(data, 'ecb', eapiKey, '', 'hex'), } } -const eapiResDecrypt = (encryptedParams) => { +const eapiResDecrypt = (encryptedParams, aeapi = false) => { // 使用aesDecrypt解密参数 try { - const decryptedData = aesDecrypt(encryptedParams, eapiKey, '', 'hex') - return JSON.parse(decryptedData) + const decrypted = aesDecrypt(encryptedParams, eapiKey, '', 'hex') // WordArray + + 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) { - console.log('eapiResDecrypt error:', error) + console.log(`eapiResDecrypt error:`, error) return null } } const eapiReqDecrypt = (encryptedParams) => { - // 使用aesDecrypt解密参数 - const decryptedData = aesDecrypt(encryptedParams, eapiKey, '', 'hex') - // 使用正则表达式解析出URL和数据 + // 使用 aesDecrypt 解密参数 + const decryptedData = aesDecrypt( + encryptedParams, + eapiKey, + '', + 'hex', + ).toString(CryptoJS.enc.Utf8) + // 使用正则表达式解析出 URL 和数据 const match = decryptedData.match(/(.*?)-36cd479b6b5-(.*?)-36cd479b6b5-(.*)/) if (match) { const url = match[1] @@ -106,7 +124,7 @@ const eapiReqDecrypt = (encryptedParams) => { return { url, data } } - // 如果没有匹配到,返回null + // 如果没有匹配到,返回 null return null } const decrypt = (cipher) => { diff --git a/util/request.js b/util/request.js index 4a73c3d..711afe8 100644 --- a/util/request.js +++ b/util/request.js @@ -188,6 +188,13 @@ const createRequest = (uri, data, options) => { 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) { case 'weapi': @@ -239,13 +246,6 @@ const createRequest = (uri, data, options) => { if (crypto === 'eapi') { // headers['x-aeapi'] = true // 服务器会使用gzip压缩返回值 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) url = (options.domain || API_DOMAIN) + '/eapi/' + uri.substr(5) } else if (crypto === 'api') { @@ -269,8 +269,9 @@ const createRequest = (uri, data, options) => { 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.responseType = 'arraybuffer' } @@ -318,7 +319,7 @@ const createRequest = (uri, data, options) => { ) try { - if (crypto === 'eapi' && data.e_r) { + if (use_e_r) { answer.body = encrypt.eapiResDecrypt( body.toString('hex').toUpperCase(), headers['x-aeapi'],