@neteasecloudmusicapienhanced/api 4.31.0 → 4.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ // 跑步漫游
2
+
3
+ const createOption = require('../util/option.js')
4
+ module.exports = (query, request) => {
5
+ const data = {
6
+ bpm: query.bpm || 50,
7
+ }
8
+ return request(`/api/radio/sport/get`, data, createOption(query))
9
+ }
@@ -2,6 +2,7 @@ const CryptoJS = require('crypto-js')
2
2
  const path = require('path')
3
3
  const fs = require('fs')
4
4
  const ID_XOR_KEY_1 = '3go8&$8*3*3h0k(2)2'
5
+ const logger = require('../util/logger.js')
5
6
 
6
7
  const createOption = require('../util/option.js')
7
8
  const { generateDeviceId } = require('../util/index')
@@ -23,7 +24,7 @@ function cloudmusic_dll_encode_id(some_id) {
23
24
 
24
25
  module.exports = async (query, request) => {
25
26
  const deviceId = generateDeviceId()
26
- console.log(`[register_anonimous] deviceId: ${deviceId}`)
27
+ logger.info(`Successfully registered anonimous token, deviceId: ${deviceId}`)
27
28
  global.deviceId = deviceId
28
29
  const encodedId = CryptoJS.enc.Base64.stringify(
29
30
  CryptoJS.enc.Utf8.parse(
@@ -0,0 +1,11 @@
1
+ // 助眠解压 - 获取标签下资源列表
2
+
3
+ const createOption = require('../util/option.js')
4
+ module.exports = (query, request) => {
5
+ const data = {
6
+ tag: query.tag,
7
+ firstQuery: false,
8
+ }
9
+
10
+ return request(`/api/voice/sati/resource/list`, data, createOption(query))
11
+ }
@@ -0,0 +1,13 @@
1
+ // 助眠解压 - 查看同类推荐
2
+
3
+ const createOption = require('../util/option.js')
4
+ module.exports = (query, request) => {
5
+ const data = {
6
+ id: query.id,
7
+ }
8
+ return request(
9
+ `/api/voice/sati/resource/list/more/v1`,
10
+ data,
11
+ createOption(query),
12
+ )
13
+ }
@@ -0,0 +1,10 @@
1
+ // 助眠解压 - 收藏
2
+
3
+ const createOption = require('../util/option.js')
4
+ module.exports = (query, request) => {
5
+ const data = {
6
+ id: query.id,
7
+ cancel: query.cancel || false,
8
+ }
9
+ return request(`/api/voice/sati/resource/sub`, data, createOption(query))
10
+ }
@@ -0,0 +1,7 @@
1
+ // 助眠解压 - 收藏列表
2
+
3
+ const createOption = require('../util/option.js')
4
+ module.exports = (query, request) => {
5
+ const data = {}
6
+ return request(`/api/voice/sati/resource/sub/list`, data, createOption(query))
7
+ }
@@ -0,0 +1,7 @@
1
+ // 助眠解压 - 标签列表
2
+
3
+ const createOption = require('../util/option.js')
4
+ module.exports = (query, request) => {
5
+ const data = {}
6
+ return request(`/api/voice/sati/tag/list`, data, createOption(query))
7
+ }
@@ -0,0 +1,13 @@
1
+ // 助眠解压 - 特定时间场景下的推荐资源
2
+
3
+ const createOption = require('../util/option.js')
4
+ module.exports = (query, request) => {
5
+ const data = {
6
+ firstQuery: false,
7
+ }
8
+ return request(
9
+ `/api/voice/sati/timescene/resources/get`,
10
+ data,
11
+ createOption(query),
12
+ )
13
+ }
@@ -0,0 +1,9 @@
1
+ // 歌曲创作者信息
2
+
3
+ const createOption = require('../util/option.js')
4
+ module.exports = (query, request) => {
5
+ const data = {
6
+ songId: query.id,
7
+ }
8
+ return request(`/api/song/creators`, data, createOption(query))
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neteasecloudmusicapienhanced/api",
3
- "version": "4.31.0",
3
+ "version": "4.32.0",
4
4
  "description": "全网最全的网易云音乐API接口 || A revival project for NeteaseCloudMusicApi Node.js Services (Half Refactor & Enhanced) || 网易云音乐 API 备份 + 增强 || 本项目自原版v4.28.0版本后开始自行维护",
5
5
  "scripts": {
6
6
  "dev": "nodemon app.js",
package/public/api.html CHANGED
@@ -188,6 +188,35 @@
188
188
  document.getElementById('result').value = 'Request failed: ' + error.message;
189
189
  }
190
190
  }
191
+
192
+ (function fillFromQuery() {
193
+ const params = new URLSearchParams(window.location.search);
194
+ if (!params.toString()) return;
195
+ const uri = params.get('uri');
196
+ const crypto = params.get('crypto');
197
+ const dataParam = params.get('data');
198
+ if (uri) {
199
+ document.getElementById('uri').value = uri;
200
+ }
201
+ if (crypto) {
202
+ const cryptoSelect = document.getElementById('crypto');
203
+ if ([...cryptoSelect.options].some((opt) => opt.value === crypto)) {
204
+ cryptoSelect.value = crypto;
205
+ }
206
+ }
207
+ if (dataParam) {
208
+ const decoded = dataParam;
209
+ try {
210
+ document.getElementById('data').value = JSON.stringify(
211
+ JSON.parse(decoded),
212
+ null,
213
+ 2,
214
+ );
215
+ } catch (error) {
216
+ document.getElementById('data').value = decoded;
217
+ }
218
+ }
219
+ })();
191
220
  </script>
192
221
  </body>
193
222
  </html>
@@ -209,6 +209,12 @@ $ sudo docker build . -t netease-music-api
209
209
  $ sudo docker run -d -p 3000:3000 netease-music-api
210
210
  ```
211
211
 
212
+ ## 调试工具
213
+
214
+ - `eapi` 请求参数或返回内容可在 `/eapi_decrypt.html` 里解析
215
+ - 请求参数模式下, 解密结果可直接带到 `/api.html` 继续调试
216
+ - 需要返回值加密时, 可传 `e_r=1`, `weapi` 和 `eapi` 都支持
217
+
212
218
  ## 接口文档
213
219
 
214
220
  ### 调用前须知
@@ -5031,6 +5037,21 @@ let data = encodeURIComponent(
5031
5037
 
5032
5038
  **调用例子:** `/vip/sign/info`
5033
5039
 
5040
+ ### 广播电台 - 收藏/取消收藏电台
5041
+
5042
+ 说明: 登录后调用此接口, 传入电台 id, 可收藏或取消收藏广播电台
5043
+
5044
+ **必选参数:**
5045
+
5046
+ `id`: 电台 id
5047
+
5048
+ `t`: 操作类型, `1` 为收藏, 其余值为取消收藏
5049
+
5050
+ **接口地址:** `/broadcast/sub`
5051
+
5052
+ **调用例子:** `/broadcast/sub?id=5&t=1`
5053
+
5054
+
5034
5055
  ### 用户的创建歌单列表
5035
5056
 
5036
5057
  说明 : 调用此接口, 传入用户id, 获取用户的创建歌单列表
@@ -5209,6 +5230,92 @@ let data = encodeURIComponent(
5209
5230
 
5210
5231
  **调用例子:** `/dj/difm/playing/tracks/list?source=0&channelId=1012`
5211
5232
 
5233
+ ### 助眠解压 - 特定时间场景下的推荐资源
5234
+
5235
+ 说明: 调用此接口, 获取特定时间场景下的推荐资源
5236
+
5237
+ **接口地址:** `/sati/timescene/resources/get`
5238
+
5239
+ **调用例子:** `/sati/timescene/resources/get`
5240
+
5241
+ ### 助眠解压 - 标签列表
5242
+
5243
+ 说明: 调用此接口, 获取标签列表
5244
+
5245
+ **接口地址:** `/sati/tag/list`
5246
+
5247
+ **调用例子:** `/sati/tag/list`
5248
+
5249
+ ### 助眠解压 - 获取标签下资源列表
5250
+
5251
+ 说明: 调用此接口, 获取标签下资源列表; 接口返回的`trackId`可以用于请求`/song/url/v1`接口,用于获取声音的下载地址
5252
+
5253
+ **必选参数 :**
5254
+
5255
+ `tag`: 标签, 由标签列表接口得到
5256
+
5257
+ **接口地址:** `/sati/resource/list`
5258
+
5259
+ **调用例子:** `/sati/resource/list?tag=naturalMusic`
5260
+
5261
+ ### 助眠解压 - 查看同类推荐
5262
+
5263
+ 说明: 调用此接口, 查看同类推荐
5264
+
5265
+ **必选参数 :**
5266
+
5267
+ `id`: id, `/sati/tag/list`接口返回的`trackId`
5268
+
5269
+ **接口地址:** `/sati/resource/list/more`
5270
+
5271
+ **调用例子:** `/sati/resource/list/more?id=167003`
5272
+
5273
+ ### 助眠解压 - 收藏列表
5274
+
5275
+ 说明: 调用此接口, 获取收藏列表
5276
+
5277
+ **接口地址:** `/sati/resource/sub/list`
5278
+
5279
+ **调用例子:** `/sati/resource/sub/list`
5280
+
5281
+ ### 助眠解压 - 收藏
5282
+
5283
+ 说明: 调用此接口, 收藏声音
5284
+
5285
+ **必选参数 :**
5286
+
5287
+ `id`: id, `/sati/tag/list`接口返回的`trackId`
5288
+
5289
+ **可选参数 :**
5290
+
5291
+ `cancel`: 是否取消收藏, 默认不取消
5292
+
5293
+ **接口地址:** `/sati/resource/sub`
5294
+
5295
+ **调用例子:** `/sati/resource/sub?id=167003`
5296
+
5297
+ ### 跑步漫游
5298
+
5299
+ 说明: 调用此接口,获取跑步漫游的歌曲信息
5300
+
5301
+ **必选参数:**
5302
+
5303
+ `bpm`: 步频
5304
+
5305
+ **接口地址:** `/radio/sport/get`
5306
+
5307
+ **调用例子:** `/radio/sport/get?bpm=50`
5308
+
5309
+ ### 歌曲创作者信息
5310
+
5311
+ 说明 : 调用此接口, 传入音乐 id 可获得对应音乐的创作者信息
5312
+
5313
+ **必选参数 :** `id`: 音乐 id
5314
+
5315
+ **接口地址 :** `/song/creators`
5316
+
5317
+ **调用例子 :** `/song/creators?id=33894312`
5318
+
5212
5319
  ## 离线访问此文档
5213
5320
 
5214
5321
  此文档同时也是 Progressive Web Apps(PWA), 加入了 serviceWorker, 可离线访问
@@ -96,6 +96,10 @@
96
96
  transition: background 0.2s ease;
97
97
  }
98
98
 
99
+ button + button {
100
+ margin-left: 12px;
101
+ }
102
+
99
103
  button:hover {
100
104
  background: #555;
101
105
  }
@@ -166,6 +170,7 @@
166
170
  </div>
167
171
 
168
172
  <button @click="decrypt">解密</button>
173
+ <button @click="sendToApi" :disabled="!canSend" :class="[{ 'opacity-50 cursor-not-allowed pointer-events-none': !canSend },]">填入 API 调试</button>
169
174
 
170
175
  <div class="result-section">
171
176
  <label>解密结果:</label>
@@ -194,12 +199,29 @@
194
199
  mounted() {
195
200
  this.decrypt()
196
201
  },
202
+ computed: {
203
+ isRequestMode() {
204
+ return this.isReq === true || this.isReq === 'true'
205
+ },
206
+ canSend() {
207
+ if (!this.isRequestMode) return false
208
+ if (!this.result || this.result === '{}' || this.result === 'null') return false
209
+ try {
210
+ JSON.parse(this.result)
211
+ return true
212
+ } catch (error) {
213
+ return false
214
+ }
215
+ },
216
+ },
197
217
  methods: {
198
- formatResult(result) {
218
+ formatResult(value) {
219
+ if (value == null || value === '') return ''
199
220
  try {
200
- return JSON.stringify(JSON.parse(result), null, 2)
201
- } catch (e) {
202
- return result
221
+ const parsed = typeof value === 'string' ? JSON.parse(value) : value
222
+ return JSON.stringify(parsed, null, 2)
223
+ } catch (error) {
224
+ return String(value)
203
225
  }
204
226
  },
205
227
  async decrypt() {
@@ -215,9 +237,25 @@
215
237
  console.log(res.data);
216
238
  } catch (error) {
217
239
  console.error(error)
218
- alert(error?.response?.data?.message || '解密失败,数据格式错误')
240
+ alert(error?.response?.data?.message || '解密失败,数据格式错误')
219
241
  }
220
- }
242
+ },
243
+ sendToApi() {
244
+ if (!this.canSend) return
245
+ const payload = JSON.parse(this.result)
246
+ const params = new URLSearchParams()
247
+ params.set('uri', payload.uri || payload.url || payload.path || '')
248
+ params.set('crypto', 'eapi')
249
+ const data =
250
+ payload.params ||
251
+ payload.data ||
252
+ payload.body ||
253
+ payload.payload ||
254
+ payload.request ||
255
+ {}
256
+ params.set('data', JSON.stringify(data))
257
+ window.open(`/api.html?${params.toString()}`, '_blank')
258
+ },
221
259
  }
222
260
  })
223
261
  app.mount('#app')
package/server.js CHANGED
@@ -156,6 +156,29 @@ function getCorsAllowOrigin(allowOrigins, requestOrigin) {
156
156
  return null
157
157
  }
158
158
 
159
+ function createConsoleSpinner(message = '启动中') {
160
+ if (!process.stdout.isTTY) {
161
+ return {
162
+ stop() {},
163
+ }
164
+ }
165
+
166
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
167
+ let index = 0
168
+ process.stdout.write(`${frames[index]} ${message}...`)
169
+ const timer = setInterval(() => {
170
+ index = (index + 1) % frames.length
171
+ process.stdout.write(`\r${frames[index]} ${message}...`)
172
+ }, 80)
173
+
174
+ return {
175
+ stop() {
176
+ clearInterval(timer)
177
+ process.stdout.write(`\r✔ ${message} 完成。\n`)
178
+ },
179
+ }
180
+ }
181
+
159
182
  /**
160
183
  * Construct the server of NCM API.
161
184
  *
@@ -387,6 +410,8 @@ async function serveNcmApi(options) {
387
410
  const port = Number(options.port || process.env.PORT || '3000')
388
411
  const host = options.host || process.env.HOST || ''
389
412
 
413
+ const spinner = createConsoleSpinner('服务启动中')
414
+
390
415
  const checkVersionSubmission =
391
416
  options.checkVersion &&
392
417
  checkVersion().then(({ npmVersion, ourVersion, status }) => {
@@ -403,21 +428,15 @@ async function serveNcmApi(options) {
403
428
  constructServerSubmission,
404
429
  ])
405
430
 
431
+ spinner.stop()
432
+
406
433
  /** @type {import('express').Express & ExpressExtension} */
407
434
  const appExt = app
408
435
  appExt.server = app.listen(port, host, () => {
409
436
  console.log(`
410
- _ _ _____ __ __
411
- | \\ | |/ ____| \\/ |
412
- | \\| | | | \\ / |
413
- | . \` | | | |\\/| |
414
- | |\\ | |____| | | |
415
- |_| \\_|\\_____|_| |_|
416
- `)
417
- console.log(`
418
- ╔═╗╔═╗╦ ╔═╗╔╗╔╦ ╦╔═╗╔╗╔╔═╗╔═╗╔╦╗
419
- ╠═╣╠═╝║ ║╣ ║║║╠═╣╠═╣║║║║ ║╣ ║║
420
- ╩ ╩╩ ╩ ╚═╝╝╚╝╩ ╩╩ ╩╝╚╝╚═╝╚═╝═╩╝
437
+ ╔═╗╔═╗╦ ╔═╗╔╗╔╦ ╦╔═╗╔╗╔╔═╗╔═╗╔╦╗
438
+ ╠═╣╠═╝║ ║╣ ║║║╠═╣╠═╣║║║║ ║╣ ║║
439
+ ╩ ╩╩ ╚═╝╝╚╝╩ ╩╩ ╩╝╚╝╚═╝╚═╝═╩╝
421
440
  `)
422
441
  logger.info(`
423
442
  - Server started successfully @ http://${host ? host : 'localhost'}:${port}
package/util/crypto.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const CryptoJS = require('crypto-js')
2
2
  const forge = require('node-forge')
3
+ const zlib = require('zlib')
3
4
  const iv = '0102030405060708'
4
5
  const presetKey = '0CoJUm6Qyw8W8jud'
5
6
  const linuxapiKey = 'rFgB&h#%2?^eDg:Q'
@@ -44,7 +45,7 @@ const aesDecrypt = (ciphertext, key, iv, format = 'base64') => {
44
45
  },
45
46
  )
46
47
  }
47
- return bytes.toString(CryptoJS.enc.Utf8)
48
+ return bytes
48
49
  }
49
50
  const rsaEncrypt = (str, key) => {
50
51
  const forgePublicKey = forge.pki.publicKeyFromPem(key)
@@ -85,20 +86,37 @@ const eapi = (url, object) => {
85
86
  params: aesEncrypt(data, 'ecb', eapiKey, '', 'hex'),
86
87
  }
87
88
  }
88
- const eapiResDecrypt = (encryptedParams) => {
89
+ const eapiResDecrypt = (encryptedParams, aeapi = false) => {
89
90
  // 使用aesDecrypt解密参数
90
91
  try {
91
- const decryptedData = aesDecrypt(encryptedParams, eapiKey, '', 'hex')
92
- return JSON.parse(decryptedData)
92
+ const decrypted = aesDecrypt(encryptedParams, eapiKey, '', 'hex') // WordArray
93
+
94
+ if (aeapi) {
95
+ // 带压缩的解密:先转 Base64 再解压
96
+ const decryptedBuffer = Buffer.from(
97
+ decrypted.toString(CryptoJS.enc.Base64),
98
+ 'base64',
99
+ )
100
+ const decompressed = zlib.gunzipSync(decryptedBuffer)
101
+ return JSON.parse(decompressed.toString())
102
+ } else {
103
+ // 普通解密:直接转 UTF-8 字符串
104
+ return JSON.parse(decrypted.toString(CryptoJS.enc.Utf8))
105
+ }
93
106
  } catch (error) {
94
- console.log('eapiResDecrypt error:', error)
107
+ console.log(`eapiResDecrypt error:`, error)
95
108
  return null
96
109
  }
97
110
  }
98
111
  const eapiReqDecrypt = (encryptedParams) => {
99
- // 使用aesDecrypt解密参数
100
- const decryptedData = aesDecrypt(encryptedParams, eapiKey, '', 'hex')
101
- // 使用正则表达式解析出URL和数据
112
+ // 使用 aesDecrypt 解密参数
113
+ const decryptedData = aesDecrypt(
114
+ encryptedParams,
115
+ eapiKey,
116
+ '',
117
+ 'hex',
118
+ ).toString(CryptoJS.enc.Utf8)
119
+ // 使用正则表达式解析出 URL 和数据
102
120
  const match = decryptedData.match(/(.*?)-36cd479b6b5-(.*?)-36cd479b6b5-(.*)/)
103
121
  if (match) {
104
122
  const url = match[1]
@@ -106,7 +124,7 @@ const eapiReqDecrypt = (encryptedParams) => {
106
124
  return { url, data }
107
125
  }
108
126
 
109
- // 如果没有匹配到,返回null
127
+ // 如果没有匹配到,返回 null
110
128
  return null
111
129
  }
112
130
  const decrypt = (cipher) => {
package/util/request.js CHANGED
@@ -3,7 +3,6 @@ const encrypt = require('./crypto')
3
3
  const CryptoJS = require('crypto-js')
4
4
  const { default: axios } = require('axios')
5
5
  const { PacProxyAgent } = require('pac-proxy-agent')
6
- const logger = require('./logger')
7
6
  const http = require('http')
8
7
  const https = require('https')
9
8
  const tunnel = require('tunnel')
@@ -160,10 +159,8 @@ const createRequest = (uri, data, options) => {
160
159
  return new Promise((resolve, reject) => {
161
160
  // 变量声明和初始化
162
161
  const headers = options.headers ? { ...options.headers } : {}
163
- const ip =
164
- options.realIP ||
165
- options.ip ||
166
- (options.randomCNIP ? generateRandomChineseIP() : '')
162
+ const ip = options.realIP || options.ip || ''
163
+
167
164
  // IP头设置
168
165
  if (ip) {
169
166
  headers['X-Real-IP'] = ip
@@ -191,6 +188,13 @@ const createRequest = (uri, data, options) => {
191
188
 
192
189
  const answer = { status: 500, body: {}, cookie: [] }
193
190
 
191
+ data.e_r = toBoolean(
192
+ options.e_r !== undefined
193
+ ? options.e_r
194
+ : data.e_r !== undefined
195
+ ? data.e_r
196
+ : ENCRYPT_RESPONSE,
197
+ )
194
198
  // 根据加密方式处理
195
199
  switch (crypto) {
196
200
  case 'weapi':
@@ -242,13 +246,7 @@ const createRequest = (uri, data, options) => {
242
246
  if (crypto === 'eapi') {
243
247
  // headers['x-aeapi'] = true // 服务器会使用gzip压缩返回值
244
248
  data.header = header
245
- data.e_r = toBoolean(
246
- options.e_r !== undefined
247
- ? options.e_r
248
- : data.e_r !== undefined
249
- ? data.e_r
250
- : ENCRYPT_RESPONSE,
251
- )
249
+
252
250
  encryptData = encrypt.eapi(uri, data)
253
251
  url = (options.domain || API_DOMAIN) + '/eapi/' + uri.substr(5)
254
252
  } else if (crypto === 'api') {
@@ -258,10 +256,10 @@ const createRequest = (uri, data, options) => {
258
256
  break
259
257
 
260
258
  default:
261
- logger.error('Unknown Crypto:', crypto)
259
+ console.log('[ERR]', 'Unknown Crypto:', crypto)
262
260
  break
263
261
  }
264
- // logger.info(url);
262
+ // console.log(url);
265
263
  // settings创建
266
264
  let settings = {
267
265
  method: 'POST',
@@ -272,8 +270,9 @@ const createRequest = (uri, data, options) => {
272
270
  httpsAgent: createHttpsAgent(),
273
271
  }
274
272
 
275
- // e_r处理
276
- if (data.e_r) {
273
+ // 使用返回值加密
274
+ const use_e_r = (crypto === 'eapi' || crypto === 'weapi') && data.e_r
275
+ if (use_e_r) {
277
276
  settings.encoding = null
278
277
  settings.responseType = 'arraybuffer'
279
278
  }
@@ -303,16 +302,16 @@ const createRequest = (uri, data, options) => {
303
302
  settings.httpAgent = agent
304
303
  settings.proxy = false
305
304
  } else {
306
- logger.error('代理配置无效,不使用代理')
305
+ console.error('代理配置无效,不使用代理')
307
306
  }
308
307
  } catch (e) {
309
- logger.error('代理URL解析失败:', e.message)
308
+ console.error('代理URL解析失败:', e.message)
310
309
  }
311
310
  }
312
311
  } else {
313
312
  settings.proxy = false
314
313
  }
315
- // logger.info(settings.headers);
314
+ // console.log(settings.headers);
316
315
  axios(settings)
317
316
  .then((res) => {
318
317
  const body = res.data
@@ -321,7 +320,7 @@ const createRequest = (uri, data, options) => {
321
320
  )
322
321
 
323
322
  try {
324
- if (crypto === 'eapi' && data.e_r) {
323
+ if (use_e_r) {
325
324
  answer.body = encrypt.eapiResDecrypt(
326
325
  body.toString('hex').toUpperCase(),
327
326
  headers['x-aeapi'],
@@ -352,14 +351,14 @@ const createRequest = (uri, data, options) => {
352
351
  if (answer.status === 200) {
353
352
  resolve(answer)
354
353
  } else {
355
- logger.error(answer)
354
+ console.log('[ERR]', answer)
356
355
  reject(answer)
357
356
  }
358
357
  })
359
358
  .catch((err) => {
360
359
  answer.status = 502
361
360
  answer.body = { code: 502, msg: err.message || err }
362
- logger.error(answer)
361
+ console.log('[ERR]', answer)
363
362
  reject(answer)
364
363
  })
365
364
  })