From ba9c6deaeed2705e4c332e3526de7c2bfc2a56eb Mon Sep 17 00:00:00 2001 From: MoeFurina Date: Fri, 17 Apr 2026 20:41:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=8A=A9=E7=9C=A0?= =?UTF-8?q?=E8=A7=A3=E5=8E=8B=E7=9B=B8=E5=85=B3=E6=A8=A1=E5=9D=97=20&&=20b?= =?UTF-8?q?ump=204.32.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- module/radio_sport_get.js | 9 +++ module/register_anonimous.js | 3 +- 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 | 107 +++++++++++++++++++++++++ public/eapi_decrypt.html | 52 ++++++++++-- server.js | 41 +++++++--- util/crypto.js | 36 ++++++--- util/request.js | 43 +++++----- 16 files changed, 341 insertions(+), 51 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/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/register_anonimous.js b/module/register_anonimous.js index 48c773c..fb510cd 100644 --- a/module/register_anonimous.js +++ b/module/register_anonimous.js @@ -2,6 +2,7 @@ const CryptoJS = require('crypto-js') const path = require('path') const fs = require('fs') const ID_XOR_KEY_1 = '3go8&$8*3*3h0k(2)2' +const logger = require('../util/logger.js') const createOption = require('../util/option.js') const { generateDeviceId } = require('../util/index') @@ -23,7 +24,7 @@ function cloudmusic_dll_encode_id(some_id) { module.exports = async (query, request) => { const deviceId = generateDeviceId() - console.log(`[register_anonimous] deviceId: ${deviceId}`) + logger.info(`Successfully registered anonimous token, deviceId: ${deviceId}`) global.deviceId = deviceId const encodedId = CryptoJS.enc.Base64.stringify( CryptoJS.enc.Utf8.parse( 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 184c857..f970421 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@neteasecloudmusicapienhanced/api", - "version": "4.31.0", + "version": "4.32.0", "description": "全网最全的网易云音乐API接口 || A revival project for NeteaseCloudMusicApi Node.js Services (Half Refactor & Enhanced) || 网易云音乐 API 备份 + 增强 || 本项目自原版v4.28.0版本后开始自行维护", "scripts": { "dev": "nodemon app.js", diff --git a/public/api.html b/public/api.html index 1144bc7..a6f8576 100644 --- a/public/api.html +++ b/public/api.html @@ -188,6 +188,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 f54a9ef..019a6a8 100644 --- a/public/docs/home.md +++ b/public/docs/home.md @@ -209,6 +209,12 @@ $ sudo docker build . -t netease-music-api $ sudo docker run -d -p 3000:3000 netease-music-api ``` +## 调试工具 + +- `eapi` 请求参数或返回内容可在 `/eapi_decrypt.html` 里解析 +- 请求参数模式下, 解密结果可直接带到 `/api.html` 继续调试 +- 需要返回值加密时, 可传 `e_r=1`, `weapi` 和 `eapi` 都支持 + ## 接口文档 ### 调用前须知 @@ -5031,6 +5037,21 @@ let data = encodeURIComponent( **调用例子:** `/vip/sign/info` +### 广播电台 - 收藏/取消收藏电台 + +说明: 登录后调用此接口, 传入电台 id, 可收藏或取消收藏广播电台 + +**必选参数:** + +`id`: 电台 id + +`t`: 操作类型, `1` 为收藏, 其余值为取消收藏 + +**接口地址:** `/broadcast/sub` + +**调用例子:** `/broadcast/sub?id=5&t=1` + + ### 用户的创建歌单列表 说明 : 调用此接口, 传入用户id, 获取用户的创建歌单列表 @@ -5209,6 +5230,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 061a6e8..fa2d013 100644 --- a/public/eapi_decrypt.html +++ b/public/eapi_decrypt.html @@ -96,6 +96,10 @@ transition: background 0.2s ease; } + button + button { + margin-left: 12px; + } + button:hover { background: #555; } @@ -166,6 +170,7 @@ +
@@ -194,12 +199,29 @@ mounted() { this.decrypt() }, - methods: { - formatResult(result) { + 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 { - return JSON.stringify(JSON.parse(result), null, 2) - } catch (e) { - return result + JSON.parse(this.result) + return true + } catch (error) { + return false + } + }, + }, + methods: { + formatResult(value) { + if (value == null || value === '') return '' + try { + const parsed = typeof value === 'string' ? JSON.parse(value) : value + return JSON.stringify(parsed, null, 2) + } catch (error) { + return String(value) } }, async decrypt() { @@ -215,9 +237,25 @@ console.log(res.data); } catch (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') diff --git a/server.js b/server.js index ff58fc1..a28f081 100644 --- a/server.js +++ b/server.js @@ -156,6 +156,29 @@ function getCorsAllowOrigin(allowOrigins, requestOrigin) { return null } +function createConsoleSpinner(message = '启动中') { + if (!process.stdout.isTTY) { + return { + stop() {}, + } + } + + const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] + let index = 0 + process.stdout.write(`${frames[index]} ${message}...`) + const timer = setInterval(() => { + index = (index + 1) % frames.length + process.stdout.write(`\r${frames[index]} ${message}...`) + }, 80) + + return { + stop() { + clearInterval(timer) + process.stdout.write(`\r✔ ${message} 完成。\n`) + }, + } +} + /** * Construct the server of NCM API. * @@ -387,6 +410,8 @@ async function serveNcmApi(options) { const port = Number(options.port || process.env.PORT || '3000') const host = options.host || process.env.HOST || '' + const spinner = createConsoleSpinner('服务启动中') + const checkVersionSubmission = options.checkVersion && checkVersion().then(({ npmVersion, ourVersion, status }) => { @@ -403,21 +428,15 @@ async function serveNcmApi(options) { constructServerSubmission, ]) + spinner.stop() + /** @type {import('express').Express & ExpressExtension} */ const appExt = app appExt.server = app.listen(port, host, () => { console.log(` - _ _ _____ __ __ - | \\ | |/ ____| \\/ | - | \\| | | | \\ / | - | . \` | | | |\\/| | - | |\\ | |____| | | | - |_| \\_|\\_____|_| |_| - `) - console.log(` - ╔═╗╔═╗╦ ╔═╗╔╗╔╦ ╦╔═╗╔╗╔╔═╗╔═╗╔╦╗ - ╠═╣╠═╝║ ║╣ ║║║╠═╣╠═╣║║║║ ║╣ ║║ - ╩ ╩╩ ╩ ╚═╝╝╚╝╩ ╩╩ ╩╝╚╝╚═╝╚═╝═╩╝ + ╔═╗╔═╗╦ ╔═╗╔╗╔╦ ╦╔═╗╔╗╔╔═╗╔═╗╔╦╗ + ╠═╣╠═╝║ ║╣ ║║║╠═╣╠═╣║║║║ ║╣ ║║ + ╩ ╩╩ ╩ ╚═╝╝╚╝╩ ╩╩ ╩╝╚╝╚═╝╚═╝═╩╝ `) logger.info(` - Server started successfully @ http://${host ? host : 'localhost'}:${port} 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 b1fab84..a71553d 100644 --- a/util/request.js +++ b/util/request.js @@ -3,7 +3,6 @@ const encrypt = require('./crypto') const CryptoJS = require('crypto-js') const { default: axios } = require('axios') const { PacProxyAgent } = require('pac-proxy-agent') -const logger = require('./logger') const http = require('http') const https = require('https') const tunnel = require('tunnel') @@ -160,10 +159,8 @@ const createRequest = (uri, data, options) => { return new Promise((resolve, reject) => { // 变量声明和初始化 const headers = options.headers ? { ...options.headers } : {} - const ip = - options.realIP || - options.ip || - (options.randomCNIP ? generateRandomChineseIP() : '') + const ip = options.realIP || options.ip || '' + // IP头设置 if (ip) { headers['X-Real-IP'] = ip @@ -191,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': @@ -242,13 +246,7 @@ 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') { @@ -258,10 +256,10 @@ const createRequest = (uri, data, options) => { break default: - logger.error('Unknown Crypto:', crypto) + console.log('[ERR]', 'Unknown Crypto:', crypto) break } - // logger.info(url); + // console.log(url); // settings创建 let settings = { method: 'POST', @@ -272,8 +270,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' } @@ -303,16 +302,16 @@ const createRequest = (uri, data, options) => { settings.httpAgent = agent settings.proxy = false } else { - logger.error('代理配置无效,不使用代理') + console.error('代理配置无效,不使用代理') } } catch (e) { - logger.error('代理URL解析失败:', e.message) + console.error('代理URL解析失败:', e.message) } } } else { settings.proxy = false } - // logger.info(settings.headers); + // console.log(settings.headers); axios(settings) .then((res) => { const body = res.data @@ -321,7 +320,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'], @@ -352,14 +351,14 @@ const createRequest = (uri, data, options) => { if (answer.status === 200) { resolve(answer) } else { - logger.error(answer) + console.log('[ERR]', answer) reject(answer) } }) .catch((err) => { answer.status = 502 answer.body = { code: 502, msg: err.message || err } - logger.error(answer) + console.log('[ERR]', answer) reject(answer) }) })