@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 CHANGED
@@ -175,4 +175,4 @@ pnpm test
175
175
 
176
176
  ## License
177
177
 
178
- [MIT License](https://github.com/IamFurina/NeteaseCloudMusicApiReborn/blob/main/LICENSE)
178
+ [MIT License](https://github.com/MoeFurina/NeteaseCloudMusicApiEnhanced/blob/main/LICENSE)
@@ -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
- try {
9
- const match = require("@unblockneteasemusic/server")
10
- const source = query.source
11
- ? query.source.split(',') : ['pyncmd', 'bodian','kuwo', 'qq', 'migu', 'kugou']
12
- const server = query.server ? query.server.split(',') : query.server
13
- const result = await match(query.id, !server? source : server)
14
- const proxy = process.env.PROXY_URL;
15
- logger.info("开始解灰", query.id, result)
16
- const useProxy = process.env.ENABLE_PROXY || "false"
17
- if (result.url.includes('kuwo')) { result.proxyUrl = useProxy === 'true' ? proxy + result.url : result.url }
18
- return {
19
- status: 200,
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
- 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
- }
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
- 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);
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
- const response = await fetch(apiUrl.toString());
40
- if (!response.ok) throw new Error(`API 响应状态: ${response.status}`);
41
- const result = await response.json();
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
- 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
- }
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
- 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" ? { error: error.message } : {}),
71
- data: [],
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
- try {
9
- const match = require("@unblockneteasemusic/server")
10
- const source = query.source
11
- ? query.source.split(',') : ['pyncmd', 'bodian', 'kuwo', 'qq', 'migu', 'kugou']
12
- const server = query.server ? query.server.split(',') : query.server
13
- const result = await match(query.id, !server? source : server)
14
- const proxy = process.env.PROXY_URL;
15
- logger.info("开始解灰", query.id, result)
16
- const useProxy = process.env.ENABLE_PROXY || "false"
17
- if (result.url.includes('kuwo') && useProxy === "true") { result.proxyUrl = proxy + result.url }
18
- return {
19
- status: 200,
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
+ }
@@ -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
- if (result.url.includes('kuwo')) {
22
- const useProxy = process.env.ENABLE_PROXY || 'false'
23
- var proxyUrl = useProxy === 'true' ? process.env.PROXY_URL + result.url : result.url
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
- let url = Array.isArray(result) ? (result[0]?.url || result[0]) : (result.url || result)
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.11",
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.12.2",
70
+ "axios": "^1.13.1",
70
71
  "crypto-js": "^4.2.0",
71
- "dotenv": "^16.6.1",
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": "^17.7.2"
83
+ "yargs": "^18.0.0"
83
84
  },
84
85
  "devDependencies": {
85
- "@types/express": "^5.0.3",
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": "^9.1.1",
88
- "@types/node": "24.6.1",
89
- "@typescript-eslint/eslint-plugin": "5.0.0",
90
- "@typescript-eslint/parser": "5.0.0",
91
- "eslint": "8.7.0",
92
- "eslint-config-prettier": "8.5.0",
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": "4.0.0",
95
- "husky": "7.0.4",
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": "12.1.7",
98
- "mocha": "11.7.3",
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": "4.5.2"
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="ie=edge">
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
- --primary-color: #2d8cf0;
12
- --secondary-color: #42b983;
13
- --text-color: #333;
14
- --text-secondary: #666;
15
- --bg-color: #f5f7fa;
16
- --card-bg: rgba(255,255,255,0.95);
17
- --hover-bg: #eaf4fb;
18
- --border-radius: 12px;
19
- --transition: all 0.3s ease;
20
- --shadow: 0 8px 24px rgba(31, 38, 135, 0.12);
21
- --container-width: 1200px;
22
- }
23
-
24
- * {
25
- margin: 0;
26
- padding: 0;
27
- box-sizing: border-box;
28
- }
29
-
30
- html, body {
31
- height: 100%;
32
- font-family: 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', Arial, sans-serif;
33
- background: linear-gradient(135deg, #e0eafc 0%, #cfdef3 100%);
34
- color: var(--text-color);
35
- line-height: 1.6;
36
- }
37
-
38
- .layout {
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
- <div class="layout">
198
- <div class="container">
199
- <header class="header">
200
- <h1>网易云音乐 API Enhanced <span id="api-version" class="version"></span></h1>
201
- <p>🔍 A revival project for NeteaseCloudMusicApi Node.js Services</p>
202
- </header>
203
-
204
- <div class="card">
205
- <h2><i class="mdi mdi-book-open-page-variant"></i>文档与演示</h2>
206
- <ul class="feature-list">
207
- <li class="feature-item">
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
- // 显示当前URL
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
- // 获取API版本号
302
- axios({
303
- url: '/inner/version',
304
- method: 'post',
305
- data: {},
306
- }).then((res) => {
307
- const version = res.data.data.version;
308
- document.getElementById('api-version').textContent = `v${version}`;
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("dotenv").config();
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['body']['data'][0]
252
- if (song.freeTrialInfo !== null || !song.url || [1, 4].includes(song.fee)) {
253
- const match = require('@unblockneteasemusic/server')
254
- const source = process.env.UNBLOCK_SOURCE ? process.env.UNBLOCK_SOURCE.split(',') : ['pyncmd', 'bodian', 'kuwo', 'qq', 'migu', 'kugou']
255
- logger.info("开始解灰", source)
256
- const { url } = await match(req.query.id, source)
257
- song.url = url
258
- song.freeTrialInfo = 'null'
259
- logger.info("解灰成功!")
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) {song.proxyUrl = proxy + song.url}
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(function () {
159
- instance.clear(key, true)
160
- }, Math.min(duration, 2147483647))
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) {
@@ -1,5 +1,5 @@
1
- const crypto = require("crypto");
2
- const os = require("os");
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 interface = interfaces[interfaceName];
13
- for (let i = 0; i < interface.length; i++) {
14
- const alias = interface[i];
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 !== "00:00:00:00:00:00" &&
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("获取MAC地址失败:", error.message);
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 = "0123456789ABCDEF";
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, "0")
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("无法获取真实MAC地址,使用随机生成");
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, "utf8").toString("hex").toUpperCase();
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("sha256").update(data, "utf8").digest("hex");
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 = "0123456789ABCDEF";
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
- 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
- };
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
- debug: (msg, ...args) => console.info(`${colors.cyan}[DEBUG]${colors.reset}`, msg, ...args),
21
- info: (msg, ...args) => console.info(`${colors.green}[INFO]${colors.reset}`, msg, ...args),
22
- warn: (msg, ...args) => console.info(`${colors.yellow}[WARN]${colors.reset}`, msg, ...args),
23
- error: (msg, ...args) => console.error(`${colors.red}[ERROR]${colors.reset}`, msg, ...args),
24
- success: (msg, ...args) => console.log(`${colors.bright}${colors.green}[SUCCESS]${colors.reset}`, msg, ...args),
25
- critical: (msg, ...args) => console.error(`${colors.bright}${colors.bgRed}[CRITICAL]${colors.reset}`, msg, ...args)
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
- // 导出logger
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]?.[uaType] || ''
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, "0")}`;
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 = options.realIP || options.ip || (options.randomCNIP ? generateRandomChineseIP() : '')
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
- ? data.e_r
247
- : ENCRYPT_RESPONSE,
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)