@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 +1 -1
- package/module/dj_difm_all_style_channel.js +9 -0
- package/module/dj_difm_channel_subscribe.js +9 -0
- package/module/dj_difm_channel_unsubscribe.js +9 -0
- package/module/dj_difm_playing_tracks_list.js +11 -0
- package/module/dj_difm_subscribe_channels_get.js +13 -0
- package/module/radio_sport_get.js +9 -0
- package/module/register_anonimous.js +2 -1
- package/module/sati_resource_list.js +11 -0
- package/module/sati_resource_list_more.js +13 -0
- package/module/sati_resource_sub.js +10 -0
- package/module/sati_resource_sub_list.js +7 -0
- package/module/sati_tag_list.js +7 -0
- package/module/sati_timescene_resources_get.js +13 -0
- package/module/song_creators.js +9 -0
- package/package.json +14 -13
- package/public/api.html +29 -0
- package/public/docs/home.md +173 -0
- package/public/eapi_decrypt.html +44 -6
- package/server.js +71 -15
- package/util/crypto.js +27 -9
- package/util/request.js +23 -22
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** | `*` |
|
|
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,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
|
+
}
|
|
@@ -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
|
-
|
|
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
|
+
firstQuery: false,
|
|
7
|
+
}
|
|
8
|
+
return request(
|
|
9
|
+
`/api/voice/sati/timescene/resources/get`,
|
|
10
|
+
data,
|
|
11
|
+
createOption(query),
|
|
12
|
+
)
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neteasecloudmusicapienhanced/api",
|
|
3
|
-
"version": "4.
|
|
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.
|
|
69
|
-
"axios": "^1.13.
|
|
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
|
-
"
|
|
75
|
-
"
|
|
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.
|
|
85
|
-
"@eslint/js": "^9.39.
|
|
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
|
|
90
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
91
|
-
"@typescript-eslint/parser": "^8.
|
|
92
|
-
"eslint": "^9.39.
|
|
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": "^
|
|
97
|
+
"globals": "^17.4.0",
|
|
97
98
|
"husky": "^9.1.7",
|
|
98
99
|
"intelli-espower-loader": "^1.1.0",
|
|
99
|
-
"lint-staged": "^16.
|
|
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>
|
package/public/docs/home.md
CHANGED
|
@@ -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, 可离线访问
|
package/public/eapi_decrypt.html
CHANGED
|
@@ -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(
|
|
218
|
+
formatResult(value) {
|
|
219
|
+
if (value == null || value === '') return ''
|
|
199
220
|
try {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
|
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
|
-
|
|
153
|
-
|
|
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 =
|
|
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
|
|
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
|
|
92
|
-
|
|
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(
|
|
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(
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
259
|
+
console.log('[ERR]', 'Unknown Crypto:', crypto)
|
|
261
260
|
break
|
|
262
261
|
}
|
|
263
|
-
//
|
|
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
|
-
//
|
|
275
|
-
|
|
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
|
-
|
|
305
|
+
console.error('代理配置无效,不使用代理')
|
|
306
306
|
}
|
|
307
307
|
} catch (e) {
|
|
308
|
-
|
|
308
|
+
console.error('代理URL解析失败:', e.message)
|
|
309
309
|
}
|
|
310
310
|
}
|
|
311
311
|
} else {
|
|
312
312
|
settings.proxy = false
|
|
313
313
|
}
|
|
314
|
-
//
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
361
|
+
console.log('[ERR]', answer)
|
|
361
362
|
reject(answer)
|
|
362
363
|
})
|
|
363
364
|
})
|