@neteasecloudmusicapienhanced/api 4.29.12 → 4.29.14
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/module/cloud_lyric_get.js +11 -0
- package/module/song_url_match.js +30 -27
- package/module/song_url_ncmget.js +64 -62
- package/module/song_url_unblock.js +30 -27
- package/module/song_url_v1.js +16 -4
- package/package.json +20 -15
- package/public/docs/home.md +13 -0
- package/public/index.html +5 -5
- package/server.js +21 -13
- package/util/apicache.js +6 -3
- package/util/client-sign.js +61 -61
- package/util/logger.js +37 -24
- package/util/request.js +8 -6
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// 获取云盘歌词
|
|
2
|
+
const createOption = require('../util/option.js')
|
|
3
|
+
module.exports = (query, request) => {
|
|
4
|
+
const data = {
|
|
5
|
+
userId: query.uid,
|
|
6
|
+
songId: query.sid,
|
|
7
|
+
lv: -1,
|
|
8
|
+
kv: -1,
|
|
9
|
+
}
|
|
10
|
+
return request(`/api/cloud/lyric/get`, data, createOption(query, 'eapi'))
|
|
11
|
+
}
|
package/module/song_url_match.js
CHANGED
|
@@ -5,31 +5,34 @@ const createOption = require('../util/option.js')
|
|
|
5
5
|
const logger = require('../util/logger.js')
|
|
6
6
|
|
|
7
7
|
module.exports = async (query, request) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
body: {
|
|
21
|
-
code: 200,
|
|
22
|
-
data: result,
|
|
23
|
-
},
|
|
24
|
-
}
|
|
25
|
-
} catch (e) {
|
|
26
|
-
return {
|
|
27
|
-
status: 500,
|
|
28
|
-
body: {
|
|
29
|
-
code: 500,
|
|
30
|
-
msg: e.message || 'unblock error',
|
|
31
|
-
data: [],
|
|
32
|
-
},
|
|
33
|
-
}
|
|
8
|
+
try {
|
|
9
|
+
const match = require('@unblockneteasemusic/server')
|
|
10
|
+
const source = query.source
|
|
11
|
+
? query.source.split(',')
|
|
12
|
+
: ['pyncmd', 'bodian', 'kuwo', 'qq', 'migu', 'kugou']
|
|
13
|
+
const server = query.server ? query.server.split(',') : query.server
|
|
14
|
+
const result = await match(query.id, !server ? source : server)
|
|
15
|
+
const proxy = process.env.PROXY_URL
|
|
16
|
+
logger.info('开始解灰', query.id, result)
|
|
17
|
+
const useProxy = process.env.ENABLE_PROXY || 'false'
|
|
18
|
+
if (result.url.includes('kuwo')) {
|
|
19
|
+
result.proxyUrl = useProxy === 'true' ? proxy + result.url : result.url
|
|
34
20
|
}
|
|
35
|
-
|
|
21
|
+
return {
|
|
22
|
+
status: 200,
|
|
23
|
+
body: {
|
|
24
|
+
code: 200,
|
|
25
|
+
data: result,
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return {
|
|
30
|
+
status: 500,
|
|
31
|
+
body: {
|
|
32
|
+
code: 500,
|
|
33
|
+
msg: e.message || 'unblock error',
|
|
34
|
+
data: [],
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -5,71 +5,73 @@
|
|
|
5
5
|
const createOption = require('../util/option.js')
|
|
6
6
|
|
|
7
7
|
module.exports = async (query, request) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
8
|
+
try {
|
|
9
|
+
const { id, br = '320' } = query
|
|
10
|
+
if (!id) {
|
|
11
|
+
return {
|
|
12
|
+
status: 400,
|
|
13
|
+
body: {
|
|
14
|
+
code: 400,
|
|
15
|
+
message: '缺少必要参数 id',
|
|
16
|
+
data: [],
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const validBR = ['128', '192', '320', '740', '999']
|
|
21
|
+
// const covertBR = ['128000', '192000', '320000','740000', '999000']
|
|
22
|
+
if (!validBR.includes(br)) {
|
|
23
|
+
return {
|
|
24
|
+
status: 400,
|
|
25
|
+
body: {
|
|
26
|
+
code: 400,
|
|
27
|
+
message: '无效音质参数',
|
|
28
|
+
allowed_values: validBR,
|
|
29
|
+
data: [],
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
const apiUrl = new URL('https://music-api.gdstudio.xyz/api.php')
|
|
35
|
+
apiUrl.searchParams.append('types', 'url')
|
|
36
|
+
apiUrl.searchParams.append('id', id)
|
|
37
|
+
apiUrl.searchParams.append('br', br)
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
const response = await fetch(apiUrl.toString())
|
|
40
|
+
if (!response.ok) throw new Error(`API 响应状态: ${response.status}`)
|
|
41
|
+
const result = await response.json()
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
// 代理逻辑
|
|
44
|
+
const useProxy = process.env.ENABLE_PROXY || false
|
|
45
|
+
const proxy = process.env.PROXY_URL
|
|
46
|
+
if (useProxy && result.url && result.url.includes('kuwo')) {
|
|
47
|
+
result.proxyUrl = proxy + result.url.replace(/^http:\/\//, 'http/')
|
|
48
|
+
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
50
|
+
return {
|
|
51
|
+
status: 200,
|
|
52
|
+
body: {
|
|
53
|
+
code: 200,
|
|
54
|
+
message: '请求成功',
|
|
55
|
+
data: {
|
|
56
|
+
id,
|
|
57
|
+
br,
|
|
58
|
+
url: result.url,
|
|
59
|
+
...(proxy && result.proxyUrl ? { proxyUrl: result.proxyUrl } : {}),
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Error in song_url_ncmget:', error)
|
|
65
|
+
return {
|
|
66
|
+
status: 500,
|
|
67
|
+
body: {
|
|
68
|
+
code: 500,
|
|
69
|
+
message: '服务器处理请求失败',
|
|
70
|
+
...(process.env.NODE_ENV === 'development'
|
|
71
|
+
? { error: error.message }
|
|
72
|
+
: {}),
|
|
73
|
+
data: [],
|
|
74
|
+
},
|
|
74
75
|
}
|
|
76
|
+
}
|
|
75
77
|
}
|
|
@@ -5,31 +5,34 @@ const createOption = require('../util/option.js')
|
|
|
5
5
|
const logger = require('../util/logger.js')
|
|
6
6
|
|
|
7
7
|
module.exports = async (query, request) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
body: {
|
|
21
|
-
code: 200,
|
|
22
|
-
data: result,
|
|
23
|
-
},
|
|
24
|
-
}
|
|
25
|
-
} catch (e) {
|
|
26
|
-
return {
|
|
27
|
-
status: 500,
|
|
28
|
-
body: {
|
|
29
|
-
code: 500,
|
|
30
|
-
msg: e.message || 'unblock error',
|
|
31
|
-
data: [],
|
|
32
|
-
},
|
|
33
|
-
}
|
|
8
|
+
try {
|
|
9
|
+
const match = require('@unblockneteasemusic/server')
|
|
10
|
+
const source = query.source
|
|
11
|
+
? query.source.split(',')
|
|
12
|
+
: ['pyncmd', 'bodian', 'kuwo', 'qq', 'migu', 'kugou']
|
|
13
|
+
const server = query.server ? query.server.split(',') : query.server
|
|
14
|
+
const result = await match(query.id, !server ? source : server)
|
|
15
|
+
const proxy = process.env.PROXY_URL
|
|
16
|
+
logger.info('开始解灰', query.id, result)
|
|
17
|
+
const useProxy = process.env.ENABLE_PROXY || 'false'
|
|
18
|
+
if (result.url.includes('kuwo')) {
|
|
19
|
+
result.proxyUrl = useProxy === 'true' ? proxy + result.url : result.url
|
|
34
20
|
}
|
|
35
|
-
|
|
21
|
+
return {
|
|
22
|
+
status: 200,
|
|
23
|
+
body: {
|
|
24
|
+
code: 200,
|
|
25
|
+
data: result,
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
} catch (e) {
|
|
29
|
+
return {
|
|
30
|
+
status: 500,
|
|
31
|
+
body: {
|
|
32
|
+
code: 500,
|
|
33
|
+
msg: e.message || 'unblock error',
|
|
34
|
+
data: [],
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
package/module/song_url_v1.js
CHANGED
|
@@ -18,12 +18,24 @@ module.exports = async (query, request) => {
|
|
|
18
18
|
try {
|
|
19
19
|
const result = await match(query.id, source)
|
|
20
20
|
logger.info('开始解灰', query.id, result)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
// avoid optional chaining for compatibility
|
|
22
|
+
let url
|
|
23
|
+
if (Array.isArray(result)) {
|
|
24
|
+
url = result[0] && result[0].url ? result[0].url : result[0]
|
|
25
|
+
} else {
|
|
26
|
+
url = result && result.url ? result.url : result
|
|
24
27
|
}
|
|
25
|
-
|
|
28
|
+
// decide proxyUrl after we resolved the actual url value
|
|
29
|
+
let proxyUrl = ''
|
|
26
30
|
if (url) {
|
|
31
|
+
if (url.includes('kuwo')) {
|
|
32
|
+
const useProxy = process.env.ENABLE_PROXY || 'false'
|
|
33
|
+
if (useProxy === 'true' && process.env.PROXY_URL) {
|
|
34
|
+
proxyUrl = process.env.PROXY_URL + url
|
|
35
|
+
} else {
|
|
36
|
+
proxyUrl = url
|
|
37
|
+
}
|
|
38
|
+
}
|
|
27
39
|
return {
|
|
28
40
|
status: 200,
|
|
29
41
|
body: {
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neteasecloudmusicapienhanced/api",
|
|
3
|
-
"version": "4.29.
|
|
3
|
+
"version": "4.29.14",
|
|
4
4
|
"description": "为停更的网易云音乐 NodeJs API 提供持续的维护!",
|
|
5
5
|
"scripts": {
|
|
6
|
+
"dev": "nodemon app.js",
|
|
6
7
|
"start": "node app.js",
|
|
7
8
|
"test": "mocha -r intelli-espower-loader -t 60000 server.test.js main.test.js --exit",
|
|
8
9
|
"lint": "eslint \"**/*.{js,ts}\"",
|
|
@@ -66,7 +67,7 @@
|
|
|
66
67
|
],
|
|
67
68
|
"dependencies": {
|
|
68
69
|
"@unblockneteasemusic/server": "^0.28.0",
|
|
69
|
-
"axios": "^1.
|
|
70
|
+
"axios": "^1.13.2",
|
|
70
71
|
"crypto-js": "^4.2.0",
|
|
71
72
|
"dotenv": "^17.2.3",
|
|
72
73
|
"express": "^5.1.0",
|
|
@@ -79,26 +80,30 @@
|
|
|
79
80
|
"safe-decode-uri-component": "^1.2.1",
|
|
80
81
|
"tunnel": "^0.0.6",
|
|
81
82
|
"xml2js": "^0.6.2",
|
|
82
|
-
"yargs": "^
|
|
83
|
+
"yargs": "^18.0.0"
|
|
83
84
|
},
|
|
84
85
|
"devDependencies": {
|
|
85
|
-
"@
|
|
86
|
+
"@eslint/eslintrc": "^3.3.1",
|
|
87
|
+
"@eslint/js": "^9.39.1",
|
|
88
|
+
"@types/express": "^5.0.5",
|
|
86
89
|
"@types/express-fileupload": "^1.5.1",
|
|
87
|
-
"@types/mocha": "^
|
|
88
|
-
"@types/node": "24.
|
|
89
|
-
"@typescript-eslint/eslint-plugin": "
|
|
90
|
-
"@typescript-eslint/parser": "
|
|
91
|
-
"eslint": "
|
|
92
|
-
"eslint-config-prettier": "
|
|
90
|
+
"@types/mocha": "^10.0.10",
|
|
91
|
+
"@types/node": "24.9.1",
|
|
92
|
+
"@typescript-eslint/eslint-plugin": "8.46.2",
|
|
93
|
+
"@typescript-eslint/parser": "8.46.2",
|
|
94
|
+
"eslint": "9.39.0",
|
|
95
|
+
"eslint-config-prettier": "10.1.8",
|
|
93
96
|
"eslint-plugin-html": "8.1.3",
|
|
94
|
-
"eslint-plugin-prettier": "
|
|
95
|
-
"
|
|
97
|
+
"eslint-plugin-prettier": "5.5.4",
|
|
98
|
+
"globals": "^16.5.0",
|
|
99
|
+
"husky": "9.1.7",
|
|
96
100
|
"intelli-espower-loader": "1.1.0",
|
|
97
|
-
"lint-staged": "16.2.
|
|
98
|
-
"mocha": "11.7.
|
|
101
|
+
"lint-staged": "16.2.6",
|
|
102
|
+
"mocha": "11.7.4",
|
|
103
|
+
"nodemon": "^3.1.10",
|
|
99
104
|
"pkg": "^5.8.1",
|
|
100
105
|
"power-assert": "1.6.1",
|
|
101
106
|
"prettier": "3.6.2",
|
|
102
|
-
"typescript": "
|
|
107
|
+
"typescript": "5.9.3"
|
|
103
108
|
}
|
|
104
109
|
}
|
package/public/docs/home.md
CHANGED
|
@@ -4848,6 +4848,19 @@ let data = encodeURIComponent(
|
|
|
4848
4848
|
|
|
4849
4849
|
**调用例子:** `/broadcast/channel/list`
|
|
4850
4850
|
|
|
4851
|
+
### 获取云盘歌词
|
|
4852
|
+
说明: 调用此接口, 获取云盘歌曲的歌词,歌词来自此文件的音乐元数据`LYRICS`标签。
|
|
4853
|
+
|
|
4854
|
+
**可选参数 :**
|
|
4855
|
+
|
|
4856
|
+
`uid`: 用户 id
|
|
4857
|
+
|
|
4858
|
+
`sid`: 云盘的歌曲 id
|
|
4859
|
+
|
|
4860
|
+
**接口地址:** `/cloud/lyric/get`
|
|
4861
|
+
|
|
4862
|
+
**调用例子:** `/cloud/lyric/get`
|
|
4863
|
+
|
|
4851
4864
|
## 离线访问此文档
|
|
4852
4865
|
|
|
4853
4866
|
此文档同时也是 Progressive Web Apps(PWA), 加入了 serviceWorker, 可离线访问
|
package/public/index.html
CHANGED
|
@@ -60,10 +60,10 @@
|
|
|
60
60
|
<section class="block">
|
|
61
61
|
<h2>常用接口</h2>
|
|
62
62
|
<ul class="links">
|
|
63
|
-
<li><a href="/search?keywords
|
|
64
|
-
<li><a href="/song/detail?ids=
|
|
65
|
-
<li><a href="/comment/music?id=
|
|
66
|
-
<li><a href="/song/url/v1?id=
|
|
63
|
+
<li><a href="/search?keywords=妖精小姐的魔法邀约">搜索音乐: <code>GET /search</code></a></li>
|
|
64
|
+
<li><a href="/song/detail?ids=2756058128">获取音乐详情: <code>GET /song/detail</code></a></li>
|
|
65
|
+
<li><a href="/comment/music?id=2756058128&limit=1">获取音乐评论: <code>GET /comment/music</code></a></li>
|
|
66
|
+
<li><a href="/song/url/v1?id=2756058128&level=exhigh">获取音乐播放链接: <code>GET /song/url/v1</code></a></li>
|
|
67
67
|
</ul>
|
|
68
68
|
</section>
|
|
69
69
|
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
<h2>调试部分</h2>
|
|
72
72
|
<pre><code>curl -s {origin}/inner/version
|
|
73
73
|
curl -s {origin}/search?keywords=网易云</code></pre>
|
|
74
|
-
<p style="margin-top:10px"> · <a href="/api.html">交互式调试</a> · <a href="/qrlogin.html">二维码登录示例</a> · <a href="/unblock_test.html">解灰测试</a></p> · <a href="/audio_match_demo/index.html">听歌识曲 Demo</a></p> · <a href="/
|
|
74
|
+
<p style="margin-top:10px"> · <a href="/api.html">交互式调试</a> · <a href="/qrlogin.html">二维码登录示例</a> · <a href="/unblock_test.html">解灰测试</a></p> · <a href="/audio_match_demo/index.html">听歌识曲 Demo</a></p> · <a href="/cloud.html">云盘上传</a></p> · <a href="/playlist_import.html">歌单导入</a></p> · <a href="/eapi_decrypt.html">EAPI 解密</p>
|
|
75
75
|
</section>
|
|
76
76
|
|
|
77
77
|
<footer class="site-footer">
|
package/server.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require(
|
|
1
|
+
require('dotenv').config()
|
|
2
2
|
const fs = require('fs')
|
|
3
3
|
const path = require('path')
|
|
4
4
|
const express = require('express')
|
|
@@ -248,20 +248,28 @@ async function consturctServer(moduleDefs) {
|
|
|
248
248
|
(req.baseUrl === '/song/url/v1' || req.baseUrl === '/song/url') &&
|
|
249
249
|
process.env.ENABLE_GENERAL_UNBLOCK === 'true'
|
|
250
250
|
) {
|
|
251
|
-
const song = moduleResponse
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
251
|
+
const song = moduleResponse.body.data[0]
|
|
252
|
+
if (
|
|
253
|
+
song.freeTrialInfo !== null ||
|
|
254
|
+
!song.url ||
|
|
255
|
+
[1, 4].includes(song.fee)
|
|
256
|
+
) {
|
|
257
|
+
const match = require('@unblockneteasemusic/server')
|
|
258
|
+
const source = process.env.UNBLOCK_SOURCE
|
|
259
|
+
? process.env.UNBLOCK_SOURCE.split(',')
|
|
260
|
+
: ['pyncmd', 'bodian', 'kuwo', 'qq', 'migu', 'kugou']
|
|
261
|
+
logger.info('开始解灰', source)
|
|
262
|
+
const { url } = await match(req.query.id, source)
|
|
263
|
+
song.url = url
|
|
264
|
+
song.freeTrialInfo = 'null'
|
|
265
|
+
logger.info('解灰成功!')
|
|
260
266
|
}
|
|
261
|
-
if (song.url.includes('kuwo')) {
|
|
262
|
-
const proxy = process.env.PROXY_URL
|
|
267
|
+
if (song.url && song.url.includes('kuwo')) {
|
|
268
|
+
const proxy = process.env.PROXY_URL
|
|
263
269
|
const useProxy = process.env.ENABLE_PROXY || 'false'
|
|
264
|
-
if (useProxy === 'true' && proxy) {
|
|
270
|
+
if (useProxy === 'true' && proxy) {
|
|
271
|
+
song.proxyUrl = proxy + song.url
|
|
272
|
+
}
|
|
265
273
|
}
|
|
266
274
|
}
|
|
267
275
|
|
package/util/apicache.js
CHANGED
|
@@ -155,9 +155,12 @@ function ApiCache() {
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
// add automatic cache clearing from duration, includes max limit on setTimeout
|
|
158
|
-
timers[key] = setTimeout(
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
timers[key] = setTimeout(
|
|
159
|
+
function () {
|
|
160
|
+
instance.clear(key, true)
|
|
161
|
+
},
|
|
162
|
+
Math.min(duration, 2147483647),
|
|
163
|
+
)
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
function accumulateContent(res, content) {
|
package/util/client-sign.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const crypto = require(
|
|
2
|
-
const os = require(
|
|
1
|
+
const crypto = require('crypto')
|
|
2
|
+
const os = require('os')
|
|
3
3
|
|
|
4
4
|
class AdvancedClientSignGenerator {
|
|
5
5
|
/**
|
|
@@ -7,25 +7,25 @@ class AdvancedClientSignGenerator {
|
|
|
7
7
|
*/
|
|
8
8
|
static getRealMacAddress() {
|
|
9
9
|
try {
|
|
10
|
-
const interfaces = os.networkInterfaces()
|
|
10
|
+
const interfaces = os.networkInterfaces()
|
|
11
11
|
for (let interfaceName in interfaces) {
|
|
12
|
-
const
|
|
13
|
-
for (let i = 0; i <
|
|
14
|
-
const alias =
|
|
12
|
+
const networkInterface = interfaces[interfaceName]
|
|
13
|
+
for (let i = 0; i < networkInterface.length; i++) {
|
|
14
|
+
const alias = networkInterface[i]
|
|
15
15
|
// 排除内部地址和无效地址
|
|
16
16
|
if (
|
|
17
17
|
alias.mac &&
|
|
18
|
-
alias.mac !==
|
|
18
|
+
alias.mac !== '00:00:00:00:00:00' &&
|
|
19
19
|
!alias.internal
|
|
20
20
|
) {
|
|
21
|
-
return alias.mac.toUpperCase()
|
|
21
|
+
return alias.mac.toUpperCase()
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
return null
|
|
25
|
+
return null
|
|
26
26
|
} catch (error) {
|
|
27
|
-
console.warn(
|
|
28
|
-
return null
|
|
27
|
+
console.warn('获取MAC地址失败:', error.message)
|
|
28
|
+
return null
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -33,108 +33,108 @@ class AdvancedClientSignGenerator {
|
|
|
33
33
|
* 生成随机MAC地址
|
|
34
34
|
*/
|
|
35
35
|
static generateRandomMac() {
|
|
36
|
-
const chars =
|
|
37
|
-
let mac =
|
|
36
|
+
const chars = '0123456789ABCDEF'
|
|
37
|
+
let mac = ''
|
|
38
38
|
for (let i = 0; i < 6; i++) {
|
|
39
|
-
if (i > 0) mac +=
|
|
39
|
+
if (i > 0) mac += ':'
|
|
40
40
|
mac +=
|
|
41
41
|
chars[Math.floor(Math.random() * 16)] +
|
|
42
|
-
chars[Math.floor(Math.random() * 16)]
|
|
42
|
+
chars[Math.floor(Math.random() * 16)]
|
|
43
43
|
}
|
|
44
44
|
// 确保第一个字节是单播地址(最低位为0)
|
|
45
|
-
const firstByte = parseInt(mac.substring(0, 2), 16)
|
|
45
|
+
const firstByte = parseInt(mac.substring(0, 2), 16)
|
|
46
46
|
const unicastFirstByte = (firstByte & 0xfe)
|
|
47
47
|
.toString(16)
|
|
48
|
-
.padStart(2,
|
|
49
|
-
.toUpperCase()
|
|
50
|
-
return unicastFirstByte + mac.substring(2)
|
|
48
|
+
.padStart(2, '0')
|
|
49
|
+
.toUpperCase()
|
|
50
|
+
return unicastFirstByte + mac.substring(2)
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
54
|
* 获取MAC地址(优先真实,否则随机)
|
|
55
55
|
*/
|
|
56
56
|
static getMacAddress() {
|
|
57
|
-
const realMac = this.getRealMacAddress()
|
|
57
|
+
const realMac = this.getRealMacAddress()
|
|
58
58
|
if (realMac) {
|
|
59
|
-
return realMac
|
|
59
|
+
return realMac
|
|
60
60
|
}
|
|
61
|
-
console.warn(
|
|
62
|
-
return this.generateRandomMac()
|
|
61
|
+
console.warn('无法获取真实MAC地址,使用随机生成')
|
|
62
|
+
return this.generateRandomMac()
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* 字符串转HEX编码
|
|
67
67
|
*/
|
|
68
68
|
static stringToHex(str) {
|
|
69
|
-
return Buffer.from(str,
|
|
69
|
+
return Buffer.from(str, 'utf8').toString('hex').toUpperCase()
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* SHA-256哈希
|
|
74
74
|
*/
|
|
75
75
|
static sha256(data) {
|
|
76
|
-
return crypto.createHash(
|
|
76
|
+
return crypto.createHash('sha256').update(data, 'utf8').digest('hex')
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
80
|
* 生成随机设备ID
|
|
81
81
|
*/
|
|
82
82
|
static generateRandomDeviceId() {
|
|
83
|
-
const partLengths = [4, 4, 4, 4, 4, 4, 4, 5]
|
|
84
|
-
const chars =
|
|
83
|
+
const partLengths = [4, 4, 4, 4, 4, 4, 4, 5] // 各部分长度
|
|
84
|
+
const chars = '0123456789ABCDEF'
|
|
85
85
|
|
|
86
86
|
const parts = partLengths.map((length) => {
|
|
87
|
-
let part =
|
|
87
|
+
let part = ''
|
|
88
88
|
for (let i = 0; i < length; i++) {
|
|
89
|
-
part += chars[Math.floor(Math.random() * 16)]
|
|
89
|
+
part += chars[Math.floor(Math.random() * 16)]
|
|
90
90
|
}
|
|
91
|
-
return part
|
|
92
|
-
})
|
|
91
|
+
return part
|
|
92
|
+
})
|
|
93
93
|
|
|
94
|
-
return parts.join(
|
|
94
|
+
return parts.join('_')
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
98
|
* 生成随机clientSign(优先使用真实MAC,否则随机)
|
|
99
99
|
*/
|
|
100
|
-
static generateRandomClientSign(secretKey =
|
|
100
|
+
static generateRandomClientSign(secretKey = '') {
|
|
101
101
|
// 获取MAC地址(优先真实,否则随机)
|
|
102
|
-
const macAddress = this.getMacAddress()
|
|
102
|
+
const macAddress = this.getMacAddress()
|
|
103
103
|
|
|
104
104
|
// 生成随机设备ID
|
|
105
|
-
const deviceId = this.generateRandomDeviceId()
|
|
105
|
+
const deviceId = this.generateRandomDeviceId()
|
|
106
106
|
|
|
107
107
|
// 转换设备ID为HEX
|
|
108
|
-
const hexDeviceId = this.stringToHex(deviceId)
|
|
108
|
+
const hexDeviceId = this.stringToHex(deviceId)
|
|
109
109
|
|
|
110
110
|
// 构造签名字符串
|
|
111
|
-
const signString = `${macAddress}@@@${hexDeviceId}
|
|
111
|
+
const signString = `${macAddress}@@@${hexDeviceId}`
|
|
112
112
|
|
|
113
113
|
// 生成哈希
|
|
114
|
-
const hash = this.sha256(signString + secretKey)
|
|
114
|
+
const hash = this.sha256(signString + secretKey)
|
|
115
115
|
|
|
116
|
-
return `${signString}@@@@@@${hash}
|
|
116
|
+
return `${signString}@@@@@@${hash}`
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
120
|
* 批量生成多个随机签名
|
|
121
121
|
*/
|
|
122
|
-
static generateMultipleRandomSigns(count, secretKey =
|
|
123
|
-
const signs = []
|
|
122
|
+
static generateMultipleRandomSigns(count, secretKey = '') {
|
|
123
|
+
const signs = []
|
|
124
124
|
for (let i = 0; i < count; i++) {
|
|
125
|
-
signs.push(this.generateRandomClientSign(secretKey))
|
|
125
|
+
signs.push(this.generateRandomClientSign(secretKey))
|
|
126
126
|
}
|
|
127
|
-
return signs
|
|
127
|
+
return signs
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
/**
|
|
131
131
|
* 使用指定参数生成签名
|
|
132
132
|
*/
|
|
133
|
-
static generateWithCustomDeviceId(macAddress, deviceId, secretKey =
|
|
134
|
-
const hexDeviceId = this.stringToHex(deviceId)
|
|
135
|
-
const signString = `${macAddress}@@@${hexDeviceId}
|
|
136
|
-
const hash = this.sha256(signString + secretKey)
|
|
137
|
-
return `${signString}@@@@@@${hash}
|
|
133
|
+
static generateWithCustomDeviceId(macAddress, deviceId, secretKey = '') {
|
|
134
|
+
const hexDeviceId = this.stringToHex(deviceId)
|
|
135
|
+
const signString = `${macAddress}@@@${hexDeviceId}`
|
|
136
|
+
const hash = this.sha256(signString + secretKey)
|
|
137
|
+
return `${signString}@@@@@@${hash}`
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
/**
|
|
@@ -142,28 +142,28 @@ class AdvancedClientSignGenerator {
|
|
|
142
142
|
*/
|
|
143
143
|
static validateClientSign(clientSign) {
|
|
144
144
|
try {
|
|
145
|
-
const parts = clientSign.split(
|
|
146
|
-
if (parts.length !== 2) return false
|
|
145
|
+
const parts = clientSign.split('@@@@@@')
|
|
146
|
+
if (parts.length !== 2) return false
|
|
147
147
|
|
|
148
|
-
const [infoPart, hash] = parts
|
|
149
|
-
const infoParts = infoPart.split(
|
|
150
|
-
if (infoParts.length !== 2) return false
|
|
148
|
+
const [infoPart, hash] = parts
|
|
149
|
+
const infoParts = infoPart.split('@@@')
|
|
150
|
+
if (infoParts.length !== 2) return false
|
|
151
151
|
|
|
152
|
-
const [mac, hexDeviceId] = infoParts
|
|
152
|
+
const [mac, hexDeviceId] = infoParts
|
|
153
153
|
|
|
154
154
|
// 验证MAC地址格式
|
|
155
|
-
const macRegex = /^([0-9A-F]{2}:){5}[0-9A-F]{2}
|
|
156
|
-
if (!macRegex.test(mac)) return false
|
|
155
|
+
const macRegex = /^([0-9A-F]{2}:){5}[0-9A-F]{2}$/
|
|
156
|
+
if (!macRegex.test(mac)) return false
|
|
157
157
|
|
|
158
158
|
// 验证哈希格式
|
|
159
|
-
const hashRegex = /^[0-9a-f]{64}
|
|
160
|
-
if (!hashRegex.test(hash)) return false
|
|
159
|
+
const hashRegex = /^[0-9a-f]{64}$/
|
|
160
|
+
if (!hashRegex.test(hash)) return false
|
|
161
161
|
|
|
162
|
-
return true
|
|
162
|
+
return true
|
|
163
163
|
} catch (error) {
|
|
164
|
-
return false
|
|
164
|
+
return false
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
module.exports = AdvancedClientSignGenerator
|
|
169
|
+
module.exports = AdvancedClientSignGenerator
|
package/util/logger.js
CHANGED
|
@@ -1,29 +1,42 @@
|
|
|
1
1
|
// ANSI 颜色代码
|
|
2
2
|
const colors = {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
3
|
+
reset: '\x1b[0m',
|
|
4
|
+
bright: '\x1b[1m',
|
|
5
|
+
dim: '\x1b[2m',
|
|
6
|
+
black: '\x1b[30m',
|
|
7
|
+
red: '\x1b[31m',
|
|
8
|
+
green: '\x1b[32m',
|
|
9
|
+
yellow: '\x1b[33m',
|
|
10
|
+
blue: '\x1b[34m',
|
|
11
|
+
magenta: '\x1b[35m',
|
|
12
|
+
cyan: '\x1b[36m',
|
|
13
|
+
white: '\x1b[37m',
|
|
14
|
+
bgRed: '\x1b[41m',
|
|
15
|
+
bgGreen: '\x1b[42m',
|
|
16
|
+
bgYellow: '\x1b[43m',
|
|
17
|
+
}
|
|
18
18
|
|
|
19
19
|
const logger = {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
debug: (msg, ...args) =>
|
|
21
|
+
console.info(`${colors.cyan}[DEBUG]${colors.reset}`, msg, ...args),
|
|
22
|
+
info: (msg, ...args) =>
|
|
23
|
+
console.info(`${colors.green}[INFO]${colors.reset}`, msg, ...args),
|
|
24
|
+
warn: (msg, ...args) =>
|
|
25
|
+
console.info(`${colors.yellow}[WARN]${colors.reset}`, msg, ...args),
|
|
26
|
+
error: (msg, ...args) =>
|
|
27
|
+
console.error(`${colors.red}[ERROR]${colors.reset}`, msg, ...args),
|
|
28
|
+
success: (msg, ...args) =>
|
|
29
|
+
console.log(
|
|
30
|
+
`${colors.bright}${colors.green}[SUCCESS]${colors.reset}`,
|
|
31
|
+
msg,
|
|
32
|
+
...args,
|
|
33
|
+
),
|
|
34
|
+
critical: (msg, ...args) =>
|
|
35
|
+
console.error(
|
|
36
|
+
`${colors.bright}${colors.bgRed}[CRITICAL]${colors.reset}`,
|
|
37
|
+
msg,
|
|
38
|
+
...args,
|
|
39
|
+
),
|
|
40
|
+
}
|
|
27
41
|
|
|
28
|
-
|
|
29
|
-
module.exports = logger;
|
|
42
|
+
module.exports = logger
|
package/util/request.js
CHANGED
|
@@ -101,7 +101,7 @@ const SPECIAL_STATUS_CODES = new Set([201, 302, 400, 502, 800, 801, 802, 803])
|
|
|
101
101
|
|
|
102
102
|
// chooseUserAgent函数
|
|
103
103
|
const chooseUserAgent = (crypto, uaType = 'pc') => {
|
|
104
|
-
return userAgentMap[crypto]
|
|
104
|
+
return (userAgentMap[crypto] && userAgentMap[crypto][uaType]) || ''
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
// cookie处理
|
|
@@ -153,15 +153,17 @@ const createHeaderCookie = (header) => {
|
|
|
153
153
|
const generateRequestId = () => {
|
|
154
154
|
return `${now()}_${floor(random() * 1000)
|
|
155
155
|
.toString()
|
|
156
|
-
.padStart(4,
|
|
157
|
-
|
|
156
|
+
.padStart(4, '0')}`
|
|
158
157
|
}
|
|
159
158
|
|
|
160
159
|
const createRequest = (uri, data, options) => {
|
|
161
160
|
return new Promise((resolve, reject) => {
|
|
162
161
|
// 变量声明和初始化
|
|
163
162
|
const headers = options.headers ? { ...options.headers } : {}
|
|
164
|
-
const ip =
|
|
163
|
+
const ip =
|
|
164
|
+
options.realIP ||
|
|
165
|
+
options.ip ||
|
|
166
|
+
(options.randomCNIP ? generateRandomChineseIP() : '')
|
|
165
167
|
// IP头设置
|
|
166
168
|
if (ip) {
|
|
167
169
|
headers['X-Real-IP'] = ip
|
|
@@ -243,8 +245,8 @@ const createRequest = (uri, data, options) => {
|
|
|
243
245
|
options.e_r !== undefined
|
|
244
246
|
? options.e_r
|
|
245
247
|
: data.e_r !== undefined
|
|
246
|
-
|
|
247
|
-
|
|
248
|
+
? data.e_r
|
|
249
|
+
: ENCRYPT_RESPONSE,
|
|
248
250
|
)
|
|
249
251
|
encryptData = encrypt.eapi(uri, data)
|
|
250
252
|
url = API_DOMAIN + '/eapi/' + uri.substr(5)
|