@neteasecloudmusicapienhanced/api 4.29.11 → 4.29.13
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/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 +21 -16
- package/public/index.html +82 -295
- 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
package/README.MD
CHANGED
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.13",
|
|
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,9 +67,9 @@
|
|
|
66
67
|
],
|
|
67
68
|
"dependencies": {
|
|
68
69
|
"@unblockneteasemusic/server": "^0.28.0",
|
|
69
|
-
"axios": "^1.
|
|
70
|
+
"axios": "^1.13.1",
|
|
70
71
|
"crypto-js": "^4.2.0",
|
|
71
|
-
"dotenv": "^
|
|
72
|
+
"dotenv": "^17.2.3",
|
|
72
73
|
"express": "^5.1.0",
|
|
73
74
|
"express-fileupload": "^1.5.2",
|
|
74
75
|
"md5": "^2.3.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.0",
|
|
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.4.0",
|
|
99
|
+
"husky": "9.1.7",
|
|
96
100
|
"intelli-espower-loader": "1.1.0",
|
|
97
|
-
"lint-staged": "
|
|
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/index.html
CHANGED
|
@@ -1,312 +1,99 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
|
-
<html lang="zh">
|
|
2
|
+
<html lang="zh-CN">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<meta http-equiv="X-UA-Compatible" content="
|
|
6
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
7
7
|
<title>网易云音乐 API Enhanced</title>
|
|
8
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@6.5.95/css/materialdesignicons.min.css">
|
|
9
8
|
<style>
|
|
10
9
|
:root {
|
|
11
|
-
--
|
|
12
|
-
--
|
|
13
|
-
--
|
|
14
|
-
--
|
|
15
|
-
--
|
|
16
|
-
--
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
.
|
|
39
|
-
min-height: 100%;
|
|
40
|
-
padding: 2rem 1rem;
|
|
41
|
-
display: flex;
|
|
42
|
-
flex-direction: column;
|
|
43
|
-
align-items: center;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
.container {
|
|
47
|
-
width: 100%;
|
|
48
|
-
max-width: var(--container-width);
|
|
49
|
-
margin: 0 auto;
|
|
50
|
-
display: grid;
|
|
51
|
-
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
52
|
-
gap: 2rem;
|
|
53
|
-
animation: fadeIn 0.8s ease-out;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
@keyframes fadeIn {
|
|
57
|
-
from { opacity: 0; transform: translateY(20px); }
|
|
58
|
-
to { opacity: 1; transform: translateY(0); }
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.header {
|
|
62
|
-
grid-column: 1 / -1;
|
|
63
|
-
text-align: center;
|
|
64
|
-
padding: 2rem;
|
|
65
|
-
background: var(--card-bg);
|
|
66
|
-
border-radius: var(--border-radius);
|
|
67
|
-
box-shadow: var(--shadow);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
.header h1 {
|
|
71
|
-
font-size: 2.5rem;
|
|
72
|
-
color: var(--primary-color);
|
|
73
|
-
margin-bottom: 1rem;
|
|
74
|
-
text-shadow: 0 2px 8px #e0eafc;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.header p {
|
|
78
|
-
color: var(--text-secondary);
|
|
79
|
-
font-size: 1.1rem;
|
|
80
|
-
max-width: 800px;
|
|
81
|
-
margin: 0 auto;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.card {
|
|
85
|
-
background: var(--card-bg);
|
|
86
|
-
border-radius: var(--border-radius);
|
|
87
|
-
padding: 1.5rem;
|
|
88
|
-
box-shadow: var(--shadow);
|
|
89
|
-
transition: var(--transition);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
.card:hover {
|
|
93
|
-
transform: translateY(-5px);
|
|
94
|
-
box-shadow: 0 12px 32px rgba(31, 38, 135, 0.15);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
.card h2 {
|
|
98
|
-
color: var(--primary-color);
|
|
99
|
-
font-size: 1.5rem;
|
|
100
|
-
margin-bottom: 1rem;
|
|
101
|
-
display: flex;
|
|
102
|
-
align-items: center;
|
|
103
|
-
gap: 0.5rem;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
.card h2 i {
|
|
107
|
-
font-size: 1.8rem;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
.feature-list {
|
|
111
|
-
list-style: none;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
.feature-item {
|
|
115
|
-
display: flex;
|
|
116
|
-
align-items: center;
|
|
117
|
-
padding: 0.8rem;
|
|
118
|
-
margin: 0.5rem 0;
|
|
119
|
-
background: var(--bg-color);
|
|
120
|
-
border-radius: 8px;
|
|
121
|
-
transition: var(--transition);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
.feature-item:hover {
|
|
125
|
-
background: var(--hover-bg);
|
|
126
|
-
transform: translateX(5px);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
.feature-item a {
|
|
130
|
-
color: var(--text-color);
|
|
131
|
-
text-decoration: none;
|
|
132
|
-
flex: 1;
|
|
133
|
-
display: flex;
|
|
134
|
-
align-items: center;
|
|
135
|
-
gap: 0.5rem;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
.feature-item i {
|
|
139
|
-
color: var(--primary-color);
|
|
140
|
-
font-size: 1.2rem;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
.status {
|
|
144
|
-
background: var(--card-bg);
|
|
145
|
-
padding: 1rem;
|
|
146
|
-
border-radius: var(--border-radius);
|
|
147
|
-
margin-top: 2rem;
|
|
148
|
-
text-align: center;
|
|
149
|
-
font-size: 0.9rem;
|
|
150
|
-
color: var(--text-secondary);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
.version {
|
|
154
|
-
display: inline-block;
|
|
155
|
-
padding: 0.2rem 0.8rem;
|
|
156
|
-
background: var(--primary-color);
|
|
157
|
-
color: white;
|
|
158
|
-
border-radius: 20px;
|
|
159
|
-
font-size: 0.9rem;
|
|
160
|
-
margin-left: 1rem;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
.footer {
|
|
164
|
-
grid-column: 1 / -1;
|
|
165
|
-
text-align: center;
|
|
166
|
-
margin-top: 2rem;
|
|
167
|
-
padding: 1rem;
|
|
168
|
-
color: var(--text-secondary);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
.footer a {
|
|
172
|
-
color: var(--primary-color);
|
|
173
|
-
text-decoration: none;
|
|
174
|
-
transition: var(--transition);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
.footer a:hover {
|
|
178
|
-
color: var(--secondary-color);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
@media (max-width: 768px) {
|
|
182
|
-
.container {
|
|
183
|
-
grid-template-columns: 1fr;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
.header h1 {
|
|
187
|
-
font-size: 2rem;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
.card {
|
|
191
|
-
margin: 0 1rem;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
10
|
+
--fg: #111827; /* gray-900 */
|
|
11
|
+
--muted: #6b7280; /* gray-500 */
|
|
12
|
+
--border: #e5e7eb; /* gray-200 */
|
|
13
|
+
--bg: #ffffff;
|
|
14
|
+
--panel: #f9fafb; /* gray-50 */
|
|
15
|
+
--accent: #2563eb; /* blue-600 */
|
|
16
|
+
}
|
|
17
|
+
* { box-sizing: border-box; }
|
|
18
|
+
html, body { height: 100%; }
|
|
19
|
+
body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, PingFang SC, Helvetica, Arial, sans-serif; color: var(--fg); background: var(--bg); line-height: 1.6; }
|
|
20
|
+
.container { max-width: 960px; margin: 40px auto; padding: 0 20px; }
|
|
21
|
+
header.site-header { margin-bottom: 24px; }
|
|
22
|
+
header.site-header h1 { font-size: 28px; font-weight: 700; margin: 0; }
|
|
23
|
+
.badge { display: inline-block; margin-left: 8px; padding: 2px 8px; border: 1px solid var(--border); border-radius: 14px; font-size: 12px; color: var(--muted); }
|
|
24
|
+
.sub { margin-top: 6px; color: var(--muted); }
|
|
25
|
+
.block { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 16px; }
|
|
26
|
+
.block h2 { margin: 0 0 10px; font-size: 18px; }
|
|
27
|
+
.kvs { display: grid; grid-template-columns: 140px 1fr; gap: 8px 16px; align-items: center; }
|
|
28
|
+
.kvs div:first-child { color: var(--muted); }
|
|
29
|
+
ul.links { list-style: none; padding: 0; margin: 0; }
|
|
30
|
+
ul.links li { margin: 6px 0; }
|
|
31
|
+
ul.links a { color: var(--fg); text-decoration: none; border-bottom: 1px dotted var(--border); }
|
|
32
|
+
ul.links a:hover { color: var(--accent); border-bottom-color: var(--accent); }
|
|
33
|
+
pre { margin: 0; background: #fff; border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow: auto; }
|
|
34
|
+
code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; font-size: 13px; }
|
|
35
|
+
footer.site-footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid var(--border); color: var(--muted); }
|
|
36
|
+
footer.site-footer a { color: var(--fg); text-decoration: none; }
|
|
37
|
+
footer.site-footer a:hover { color: var(--accent); }
|
|
194
38
|
</style>
|
|
195
39
|
</head>
|
|
196
40
|
<body>
|
|
197
|
-
<
|
|
198
|
-
<
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
<
|
|
207
|
-
|
|
208
|
-
<a href="/docs" target="_blank">
|
|
209
|
-
<i class="mdi mdi-file-document"></i>
|
|
210
|
-
API 文档
|
|
211
|
-
</a>
|
|
212
|
-
</li>
|
|
213
|
-
<li class="feature-item">
|
|
214
|
-
<a href="./api.html">
|
|
215
|
-
<i class="mdi mdi-console"></i>
|
|
216
|
-
API 调试界面
|
|
217
|
-
</a>
|
|
218
|
-
</li>
|
|
219
|
-
<li class="feature-item">
|
|
220
|
-
<a href="./qrlogin.html">
|
|
221
|
-
<i class="mdi mdi-qrcode"></i>
|
|
222
|
-
二维码登录演示
|
|
223
|
-
</a>
|
|
224
|
-
</li>
|
|
225
|
-
</ul>
|
|
226
|
-
</div>
|
|
227
|
-
|
|
228
|
-
<div class="card">
|
|
229
|
-
<h2><i class="mdi mdi-music"></i>音乐功能</h2>
|
|
230
|
-
<ul class="feature-list">
|
|
231
|
-
<li class="feature-item">
|
|
232
|
-
<a href="./search?keywords=这么可爱真是抱歉">
|
|
233
|
-
<i class="mdi mdi-magnify"></i>
|
|
234
|
-
搜索歌曲
|
|
235
|
-
</a>
|
|
236
|
-
</li>
|
|
237
|
-
<li class="feature-item">
|
|
238
|
-
<a href="./unblock_test.html">
|
|
239
|
-
<i class="mdi mdi-lock-open"></i>
|
|
240
|
-
解灰测试
|
|
241
|
-
</a>
|
|
242
|
-
</li>
|
|
243
|
-
<li class="feature-item">
|
|
244
|
-
<a href="./comment/music?id=1969519579&limit=1">
|
|
245
|
-
<i class="mdi mdi-comment"></i>
|
|
246
|
-
获取评论
|
|
247
|
-
</a>
|
|
248
|
-
</li>
|
|
249
|
-
</ul>
|
|
250
|
-
</div>
|
|
251
|
-
|
|
252
|
-
<div class="card">
|
|
253
|
-
<h2><i class="mdi mdi-tools"></i>实用工具</h2>
|
|
254
|
-
<ul class="feature-list">
|
|
255
|
-
<li class="feature-item">
|
|
256
|
-
<a href="./audio_match_demo/index.html">
|
|
257
|
-
<i class="mdi mdi-music-note-search"></i>
|
|
258
|
-
听歌识曲
|
|
259
|
-
</a>
|
|
260
|
-
</li>
|
|
261
|
-
<li class="feature-item">
|
|
262
|
-
<a href="./cloud.html">
|
|
263
|
-
<i class="mdi mdi-cloud-upload"></i>
|
|
264
|
-
云盘上传
|
|
265
|
-
</a>
|
|
266
|
-
</li>
|
|
267
|
-
<li class="feature-item">
|
|
268
|
-
<a href="./playlist_import.html">
|
|
269
|
-
<i class="mdi mdi-playlist-plus"></i>
|
|
270
|
-
歌单导入
|
|
271
|
-
</a>
|
|
272
|
-
</li>
|
|
273
|
-
<li class="feature-item">
|
|
274
|
-
<a href="./eapi_decrypt.html">
|
|
275
|
-
<i class="mdi mdi-decode"></i>
|
|
276
|
-
EAPI 解析
|
|
277
|
-
</a>
|
|
278
|
-
</li>
|
|
279
|
-
</ul>
|
|
41
|
+
<main class="container">
|
|
42
|
+
<header class="site-header">
|
|
43
|
+
<h1>网易云音乐 API Enhanced <span id="api-version" class="badge"></span></h1>
|
|
44
|
+
<p class="sub">🔍 A revival project for NeteaseCloudMusicApi Node.js Api Services || 网易云音乐 API 备份 + 增强 || 本项目自原版v4.28.0版本后开始自行维护</p>
|
|
45
|
+
</header>
|
|
46
|
+
|
|
47
|
+
<section class="block">
|
|
48
|
+
<h2>状态</h2>
|
|
49
|
+
<div class="kvs">
|
|
50
|
+
<div>Base URL</div><div id="base-url">—</div>
|
|
51
|
+
<div>当前页</div><div id="current-url">—</div>
|
|
280
52
|
</div>
|
|
53
|
+
</section>
|
|
54
|
+
|
|
55
|
+
<section class="block">
|
|
56
|
+
<h2>文档</h2>
|
|
57
|
+
<p><a href="/docs" target="_blank">查看在线文档</a></p>
|
|
58
|
+
</section>
|
|
59
|
+
|
|
60
|
+
<section class="block">
|
|
61
|
+
<h2>常用接口</h2>
|
|
62
|
+
<ul class="links">
|
|
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
|
+
</ul>
|
|
68
|
+
</section>
|
|
69
|
+
|
|
70
|
+
<section class="block">
|
|
71
|
+
<h2>调试部分</h2>
|
|
72
|
+
<pre><code>curl -s {origin}/inner/version
|
|
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="/cloud.html">云盘上传</a></p> · <a href="/playlist_import.html">歌单导入</a></p> · <a href="/eapi_decrypt.html">EAPI 解密</p>
|
|
75
|
+
</section>
|
|
76
|
+
|
|
77
|
+
<footer class="site-footer">
|
|
78
|
+
<a href="https://github.com/neteasecloudmusicapienhanced/api-enhanced" target="_blank">GitHub</a>
|
|
79
|
+
</footer>
|
|
80
|
+
</main>
|
|
281
81
|
|
|
282
|
-
<div class="status">
|
|
283
|
-
当前访问地址:<span id="current-url"></span>
|
|
284
|
-
</div>
|
|
285
|
-
|
|
286
|
-
<footer class="footer">
|
|
287
|
-
<span>© 2025 网易云音乐 API Enhanced(Reborn) | </span>
|
|
288
|
-
<a href="https://github.com/neteasecloudmusicapienhanced/api-enhanced" target="_blank">
|
|
289
|
-
<i class="mdi mdi-github"></i> GitHub
|
|
290
|
-
</a>
|
|
291
|
-
</footer>
|
|
292
|
-
</div>
|
|
293
|
-
</div>
|
|
294
|
-
|
|
295
|
-
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
|
296
82
|
<script>
|
|
297
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
298
|
-
|
|
83
|
+
document.addEventListener('DOMContentLoaded', function () {
|
|
84
|
+
var origin = window.location.origin;
|
|
85
|
+
document.getElementById('base-url').textContent = origin;
|
|
299
86
|
document.getElementById('current-url').textContent = window.location.href;
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
87
|
+
|
|
88
|
+
fetch('/inner/version', { method: 'POST' })
|
|
89
|
+
.then(function (r) { return r.json(); })
|
|
90
|
+
.then(function (data) {
|
|
91
|
+
var v = data && data.data && data.data.version;
|
|
92
|
+
if (v) document.getElementById('api-version').textContent = 'v' + v;
|
|
93
|
+
var pre = document.querySelector('pre code');
|
|
94
|
+
if (pre) pre.textContent = pre.textContent.replace(/\{origin\}/g, origin);
|
|
95
|
+
})
|
|
96
|
+
.catch(function () {});
|
|
310
97
|
});
|
|
311
98
|
</script>
|
|
312
99
|
</body>
|
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)
|