@neteasecloudmusicapienhanced/api 4.30.3 → 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.
package/README.MD CHANGED
@@ -130,7 +130,7 @@ $ sudo docker run -d -p 3000:3000 ncm-api
130
130
 
131
131
  | 变量名 | 默认值 | 说明 |
132
132
  |----------------------------|--------------------------------------|----------------------------------------------------|
133
- | **CORS_ALLOW_ORIGIN** | `*` | 允许跨域请求的域名。若需要限制,请指定具体域名(例如 `https://example.com`)。 |
133
+ | **CORS_ALLOW_ORIGIN** | `*` | 允许跨域请求的域名。可填写单个源,或使用逗号分隔多个源(例如 `https://a.com,https://b.com`)。 |
134
134
  | **ENABLE_PROXY** | `false` | 是否启用反向代理功能。 |
135
135
  | **PROXY_URL** | `https://your-proxy-url.com/?proxy=` | 代理服务地址。仅当 `ENABLE_PROXY=true` 时生效。 |
136
136
  | **ENABLE_GENERAL_UNBLOCK** | `true` | 是否启用全局解灰(推荐开启)。开启后所有歌曲都尝试自动解锁。 |
@@ -0,0 +1,9 @@
1
+ // DIFM电台 - 分类
2
+
3
+ const createOption = require('../util/option.js')
4
+ module.exports = (query, request) => {
5
+ const data = {
6
+ sources: query.sources || '[0]',
7
+ }
8
+ return request(`/api/dj/difm/all/style/channel/v2`, data, createOption(query))
9
+ }
@@ -0,0 +1,9 @@
1
+ // DIFM电台 - 收藏频道
2
+
3
+ const createOption = require('../util/option.js')
4
+ module.exports = (query, request) => {
5
+ const data = {
6
+ id: query.id,
7
+ }
8
+ return request(`/api/dj/difm/channel/subscribe`, data, createOption(query))
9
+ }
@@ -0,0 +1,9 @@
1
+ // DIFM电台 - 取消收藏频道
2
+
3
+ const createOption = require('../util/option.js')
4
+ module.exports = (query, request) => {
5
+ const data = {
6
+ id: query.id,
7
+ }
8
+ return request(`/api/dj/difm/channel/unsubscribe`, data, createOption(query))
9
+ }
@@ -0,0 +1,11 @@
1
+ // DIFM电台 - 播放列表
2
+
3
+ const createOption = require('../util/option.js')
4
+ module.exports = (query, request) => {
5
+ const data = {
6
+ limit: query.limit || 5,
7
+ source: query.source || 0,
8
+ channelId: query.channelId,
9
+ }
10
+ return request(`/api/dj/difm/playing/tracks/list`, data, createOption(query))
11
+ }
@@ -0,0 +1,13 @@
1
+ // DIFM电台 - 收藏列表
2
+
3
+ const createOption = require('../util/option.js')
4
+ module.exports = (query, request) => {
5
+ const data = {
6
+ sources: query.sources || '[0]',
7
+ }
8
+ return request(
9
+ `/api/dj/difm/subscribe/channels/get/v2`,
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
+ 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.30.3",
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",
@@ -65,14 +65,15 @@
65
65
  "data"
66
66
  ],
67
67
  "dependencies": {
68
- "@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.2.3",
69
- "axios": "^1.13.5",
68
+ "@neteasecloudmusicapienhanced/unblockmusic-utils": "^0.2.4",
69
+ "axios": "^1.13.6",
70
70
  "crypto-js": "^4.2.0",
71
71
  "dotenv": "^17.3.1",
72
72
  "express": "^5.2.1",
73
73
  "express-fileupload": "^1.5.2",
74
- "music-metadata": "^11.12.1",
75
- "node-forge": "^1.3.3",
74
+ "gzip": "^0.1.0",
75
+ "music-metadata": "^11.12.3",
76
+ "node-forge": "^1.4.0",
76
77
  "pac-proxy-agent": "^7.2.0",
77
78
  "qrcode": "^1.5.4",
78
79
  "safe-decode-uri-component": "^1.2.1",
@@ -81,22 +82,22 @@
81
82
  "yargs": "^18.0.0"
82
83
  },
83
84
  "devDependencies": {
84
- "@eslint/eslintrc": "^3.3.3",
85
- "@eslint/js": "^9.39.3",
85
+ "@eslint/eslintrc": "^3.3.5",
86
+ "@eslint/js": "^9.39.4",
86
87
  "@types/express": "^5.0.6",
87
88
  "@types/express-fileupload": "^1.5.1",
88
89
  "@types/mocha": "^10.0.10",
89
- "@types/node": "25.0.9",
90
- "@typescript-eslint/eslint-plugin": "^8.56.0",
91
- "@typescript-eslint/parser": "^8.56.0",
92
- "eslint": "^9.39.3",
90
+ "@types/node": "25.5.0",
91
+ "@typescript-eslint/eslint-plugin": "^8.57.2",
92
+ "@typescript-eslint/parser": "^8.57.2",
93
+ "eslint": "^9.39.4",
93
94
  "eslint-config-prettier": "^10.1.8",
94
95
  "eslint-plugin-html": "^8.1.4",
95
96
  "eslint-plugin-prettier": "^5.5.5",
96
- "globals": "^16.5.0",
97
+ "globals": "^17.4.0",
97
98
  "husky": "^9.1.7",
98
99
  "intelli-espower-loader": "^1.1.0",
99
- "lint-staged": "^16.2.7",
100
+ "lint-staged": "^16.4.0",
100
101
  "mocha": "^11.7.5",
101
102
  "nodemon": "^3.1.14",
102
103
  "pkg": "^5.8.1",
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, 获取用户的创建歌单列表
@@ -5143,6 +5164,158 @@ let data = encodeURIComponent(
5143
5164
 
5144
5165
  **调用例子 :** `/comment/reply?id=2058263032&commentId=123456789&content=我也觉得这首歌很棒!`
5145
5166
 
5167
+ ### DIFM电台 - 分类
5168
+
5169
+ 说明: 调用此接口, 获取DIFM电台分类
5170
+
5171
+ **必选参数 :**
5172
+
5173
+ `sources`: 来源列表, 0: 最嗨电音 1: 古典电台 2: 爵士电台
5174
+
5175
+ **接口地址:** `/dj/difm/all/style/channel`
5176
+
5177
+ **调用例子:** `/dj/difm/all/style/channel?sources=[0]`
5178
+
5179
+ ### DIFM电台 - 收藏列表
5180
+
5181
+ 说明: 调用此接口, 获取DIFM电台收藏列表
5182
+
5183
+ **必选参数 :**
5184
+
5185
+ `sources`: 来源列表, 0: 最嗨电音 1: 古典电台 2: 爵士电台
5186
+
5187
+ **接口地址:** `/dj/difm/subscribe/channels/get`
5188
+
5189
+ **调用例子:** `/dj/difm/subscribe/channels/get?sources=[0]`
5190
+
5191
+ ### DIFM电台 - 收藏频道
5192
+
5193
+ 说明: 调用此接口, 可收藏DIFM频道
5194
+
5195
+ **必选参数 :**
5196
+
5197
+ `id`: 频道id
5198
+
5199
+ **接口地址:** `/dj/difm/channel/subscribe`
5200
+
5201
+ **调用例子:** `/dj/difm/channel/subscribe?id=1`
5202
+
5203
+ ### DIFM电台 - 取消收藏频道
5204
+
5205
+ 说明: 调用此接口, 可取消收藏DIFM频道
5206
+
5207
+ **必选参数 :**
5208
+
5209
+ `id`: 频道id
5210
+
5211
+ **接口地址:** `/dj/difm/channel/unsubscribe`
5212
+
5213
+ **调用例子:** `/dj/difm/channel/unsubscribe?id=1`
5214
+
5215
+ ### DIFM电台 - 播放列表
5216
+
5217
+ 说明: 调用此接口, 获取DIFM播放列表
5218
+
5219
+ **必选参数 :**
5220
+
5221
+ `source`: 来源, 0: 最嗨电音 1: 古典电台 2: 爵士电台
5222
+
5223
+ `channelId`: 频道id
5224
+
5225
+ **可选参数 :**
5226
+
5227
+ `limit`: 返回数量, 默认为 5
5228
+
5229
+ **接口地址:** `/dj/difm/playing/tracks/list`
5230
+
5231
+ **调用例子:** `/dj/difm/playing/tracks/list?source=0&channelId=1012`
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
+
5146
5319
  ## 离线访问此文档
5147
5320
 
5148
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
@@ -127,15 +127,68 @@ async function checkVersion() {
127
127
  })
128
128
  }
129
129
 
130
+ function parseCorsAllowOrigins(corsAllowOrigin) {
131
+ if (!corsAllowOrigin) {
132
+ return null
133
+ }
134
+
135
+ const origins = corsAllowOrigin
136
+ .split(',')
137
+ .map((origin) => origin.trim())
138
+ .filter(Boolean)
139
+
140
+ return origins.length > 0 ? origins : null
141
+ }
142
+
143
+ function getCorsAllowOrigin(allowOrigins, requestOrigin) {
144
+ if (!allowOrigins) {
145
+ return requestOrigin || '*'
146
+ }
147
+
148
+ if (allowOrigins.includes('*')) {
149
+ return '*'
150
+ }
151
+
152
+ if (requestOrigin && allowOrigins.includes(requestOrigin)) {
153
+ return requestOrigin
154
+ }
155
+
156
+ return null
157
+ }
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
+
130
182
  /**
131
183
  * Construct the server of NCM API.
132
184
  *
133
185
  * @param {ModuleDefinition[]} [moduleDefs] Customized module definitions [advanced]
134
186
  * @returns {Promise<import("express").Express>} The server instance.
135
187
  */
136
- async function consturctServer(moduleDefs) {
188
+ async function constructServer(moduleDefs) {
137
189
  const app = express()
138
190
  const { CORS_ALLOW_ORIGIN } = process.env
191
+ const allowOrigins = parseCorsAllowOrigins(CORS_ALLOW_ORIGIN)
139
192
  app.set('trust proxy', true)
140
193
 
141
194
  /**
@@ -147,10 +200,17 @@ async function consturctServer(moduleDefs) {
147
200
  */
148
201
  app.use((req, res, next) => {
149
202
  if (req.path !== '/' && !req.path.includes('.')) {
203
+ const corsAllowOrigin = getCorsAllowOrigin(
204
+ allowOrigins,
205
+ req.headers.origin,
206
+ )
207
+ const shouldSetVaryHeader = corsAllowOrigin && corsAllowOrigin !== '*'
150
208
  res.set({
151
209
  'Access-Control-Allow-Credentials': true,
152
- 'Access-Control-Allow-Origin':
153
- CORS_ALLOW_ORIGIN || req.headers.origin || '*',
210
+ ...(corsAllowOrigin
211
+ ? { 'Access-Control-Allow-Origin': corsAllowOrigin }
212
+ : {}),
213
+ ...(shouldSetVaryHeader ? { Vary: 'Origin' } : {}),
154
214
  'Access-Control-Allow-Headers': 'X-Requested-With,Content-Type',
155
215
  'Access-Control-Allow-Methods': 'PUT,POST,GET,DELETE,OPTIONS',
156
216
  'Content-Type': 'application/json; charset=utf-8',
@@ -350,6 +410,8 @@ async function serveNcmApi(options) {
350
410
  const port = Number(options.port || process.env.PORT || '3000')
351
411
  const host = options.host || process.env.HOST || ''
352
412
 
413
+ const spinner = createConsoleSpinner('服务启动中')
414
+
353
415
  const checkVersionSubmission =
354
416
  options.checkVersion &&
355
417
  checkVersion().then(({ npmVersion, ourVersion, status }) => {
@@ -359,28 +421,22 @@ async function serveNcmApi(options) {
359
421
  )
360
422
  }
361
423
  })
362
- const constructServerSubmission = consturctServer(options.moduleDefs)
424
+ const constructServerSubmission = constructServer(options.moduleDefs)
363
425
 
364
426
  const [_, app] = await Promise.all([
365
427
  checkVersionSubmission,
366
428
  constructServerSubmission,
367
429
  ])
368
430
 
431
+ spinner.stop()
432
+
369
433
  /** @type {import('express').Express & ExpressExtension} */
370
434
  const appExt = app
371
435
  appExt.server = app.listen(port, host, () => {
372
436
  console.log(`
373
- _ _ _____ __ __
374
- | \\ | |/ ____| \\/ |
375
- | \\| | | | \\ / |
376
- | . \` | | | |\\/| |
377
- | |\\ | |____| | | |
378
- |_| \\_|\\_____|_| |_|
379
- `)
380
- console.log(`
381
- ╔═╗╔═╗╦ ╔═╗╔╗╔╦ ╦╔═╗╔╗╔╔═╗╔═╗╔╦╗
382
- ╠═╣╠═╝║ ║╣ ║║║╠═╣╠═╣║║║║ ║╣ ║║
383
- ╩ ╩╩ ╩ ╚═╝╝╚╝╩ ╩╩ ╩╝╚╝╚═╝╚═╝═╩╝
437
+ ╔═╗╔═╗╦ ╔═╗╔╗╔╦ ╦╔═╗╔╗╔╔═╗╔═╗╔╦╗
438
+ ╠═╣╠═╝║ ║╣ ║║║╠═╣╠═╣║║║║ ║╣ ║║
439
+ ╩ ╩╩ ╚═╝╝╚╝╩ ╩╩ ╩╝╚╝╚═╝╚═╝═╩╝
384
440
  `)
385
441
  logger.info(`
386
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':
@@ -240,14 +244,9 @@ const createRequest = (uri, data, options) => {
240
244
  headers['User-Agent'] = options.ua || chooseUserAgent('api', 'iphone')
241
245
 
242
246
  if (crypto === 'eapi') {
247
+ // headers['x-aeapi'] = true // 服务器会使用gzip压缩返回值
243
248
  data.header = header
244
- data.e_r = toBoolean(
245
- options.e_r !== undefined
246
- ? options.e_r
247
- : data.e_r !== undefined
248
- ? data.e_r
249
- : ENCRYPT_RESPONSE,
250
- )
249
+
251
250
  encryptData = encrypt.eapi(uri, data)
252
251
  url = (options.domain || API_DOMAIN) + '/eapi/' + uri.substr(5)
253
252
  } else if (crypto === 'api') {
@@ -257,10 +256,10 @@ const createRequest = (uri, data, options) => {
257
256
  break
258
257
 
259
258
  default:
260
- logger.error('Unknown Crypto:', crypto)
259
+ console.log('[ERR]', 'Unknown Crypto:', crypto)
261
260
  break
262
261
  }
263
- // logger.info(url);
262
+ // console.log(url);
264
263
  // settings创建
265
264
  let settings = {
266
265
  method: 'POST',
@@ -271,8 +270,9 @@ const createRequest = (uri, data, options) => {
271
270
  httpsAgent: createHttpsAgent(),
272
271
  }
273
272
 
274
- // e_r处理
275
- if (data.e_r) {
273
+ // 使用返回值加密
274
+ const use_e_r = (crypto === 'eapi' || crypto === 'weapi') && data.e_r
275
+ if (use_e_r) {
276
276
  settings.encoding = null
277
277
  settings.responseType = 'arraybuffer'
278
278
  }
@@ -302,16 +302,16 @@ const createRequest = (uri, data, options) => {
302
302
  settings.httpAgent = agent
303
303
  settings.proxy = false
304
304
  } else {
305
- logger.error('代理配置无效,不使用代理')
305
+ console.error('代理配置无效,不使用代理')
306
306
  }
307
307
  } catch (e) {
308
- logger.error('代理URL解析失败:', e.message)
308
+ console.error('代理URL解析失败:', e.message)
309
309
  }
310
310
  }
311
311
  } else {
312
312
  settings.proxy = false
313
313
  }
314
- // logger.info(settings.headers);
314
+ // console.log(settings.headers);
315
315
  axios(settings)
316
316
  .then((res) => {
317
317
  const body = res.data
@@ -320,9 +320,10 @@ const createRequest = (uri, data, options) => {
320
320
  )
321
321
 
322
322
  try {
323
- if (crypto === 'eapi' && data.e_r) {
323
+ if (use_e_r) {
324
324
  answer.body = encrypt.eapiResDecrypt(
325
325
  body.toString('hex').toUpperCase(),
326
+ headers['x-aeapi'],
326
327
  )
327
328
  } else {
328
329
  answer.body =
@@ -350,14 +351,14 @@ const createRequest = (uri, data, options) => {
350
351
  if (answer.status === 200) {
351
352
  resolve(answer)
352
353
  } else {
353
- logger.error(answer)
354
+ console.log('[ERR]', answer)
354
355
  reject(answer)
355
356
  }
356
357
  })
357
358
  .catch((err) => {
358
359
  answer.status = 502
359
360
  answer.body = { code: 502, msg: err.message || err }
360
- logger.error(answer)
361
+ console.log('[ERR]', answer)
361
362
  reject(answer)
362
363
  })
363
364
  })