@neteasecloudmusicapienhanced/api 4.30.1 → 4.30.2

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.
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2
+ <g>
3
+ <path fill="none" d="M0 0h24v24H0z"/>
4
+ <path d="M10.421 11.375c-.294 1.028.012 2.064.784 2.653 1.061.81 2.565.3 2.874-.995.08-.337.103-.722.027-1.056-.23-1.001-.52-1.988-.792-2.996-1.33.154-2.543 1.172-2.893 2.394zm5.548-.287c.273 1.012.285 2.017-.127 3-1.128 2.69-4.721 3.14-6.573.826-1.302-1.627-1.28-3.961.06-5.734.78-1.032 1.804-1.707 3.048-2.054l.379-.104c-.084-.415-.188-.816-.243-1.224-.176-1.317.512-2.503 1.744-3.04 1.226-.535 2.708-.216 3.53.76.406.479.395 1.08-.025 1.464-.412.377-.996.346-1.435-.09-.247-.246-.51-.44-.877-.436-.525.006-.987.418-.945.937.037.468.173.93.3 1.386.022.078.216.135.338.153 1.334.197 2.504.731 3.472 1.676 2.558 2.493 2.861 6.531.672 9.44-1.529 2.032-3.61 3.168-6.127 3.409-4.621.44-8.664-2.53-9.7-7.058C2.515 10.255 4.84 5.831 8.795 4.25c.586-.234 1.143-.031 1.371.498.232.537-.019 1.086-.61 1.35-2.368 1.06-3.817 2.855-4.215 5.424-.533 3.433 1.656 6.776 5 7.72 2.723.77 5.658-.166 7.308-2.33 1.586-2.08 1.4-5.099-.427-6.873a3.979 3.979 0 0 0-1.823-1.013c.198.716.389 1.388.57 2.062z"/>
5
+ </g>
6
+ </svg>
Binary file
package/public/index.html CHANGED
@@ -4,6 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
7
+ <link rel="icon" href="docs/netease.png">
7
8
  <title>网易云音乐 API Enhanced</title>
8
9
  <style>
9
10
  :root {
@@ -18,20 +19,33 @@
18
19
  html, body { height: 100%; }
19
20
  body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: var(--fg); background: var(--bg); line-height: 1.6; }
20
21
  .container { max-width: 960px; margin: 40px auto; padding: 0 20px; }
22
+ @media (max-width: 480px) {
23
+ .container { margin: 20px auto; padding: 0 16px; }
24
+ header.site-header h1 { font-size: 22px; }
25
+ .block { padding: 16px; }
26
+ }
21
27
  header.site-header { margin-bottom: 24px; }
22
28
  header.site-header h1 { font-size: 28px; font-weight: 600; margin: 0; }
23
29
  .badge { display: inline-block; margin-left: 8px; padding: 4px 10px; border: 1px solid var(--border); border-radius: 12px; font-size: 12px; color: var(--muted); }
24
30
  .sub { margin-top: 8px; color: var(--muted); font-size: 14px; }
25
31
  .block { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); }
26
32
  .block h2 { margin: 0 0 12px; font-size: 18px; font-weight: 600; }
27
- .kvs { display: grid; grid-template-columns: 140px 1fr; gap: 8px 16px; align-items: center; }
28
- .kvs div:first-child { color: var(--muted); }
33
+ .kvs { display: grid; grid-template-columns: 100px 1fr; gap: 8px 12px; align-items: start; }
34
+ .kvs div:first-child { color: var(--muted); flex-shrink: 0; }
35
+ .kvs div:last-child { word-break: break-all; overflow-wrap: anywhere; min-width: 0; overflow: hidden; }
36
+ @media (max-width: 480px) {
37
+ .kvs { grid-template-columns: 1fr; gap: 4px 12px; }
38
+ .kvs div:first-child { font-weight: 500; }
39
+ }
29
40
  ul.links { list-style: none; padding: 0; margin: 0; }
30
41
  ul.links li { margin: 8px 0; }
31
42
  ul.links a { color: var(--fg); text-decoration: none; border-bottom: 1px dotted var(--border); transition: all 0.2s ease; }
32
43
  ul.links a:hover { color: var(--accent); border-bottom-color: var(--accent); }
33
- pre { margin: 0; background: #f9f9f9; border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow: auto; }
44
+ pre { margin: 0; background: #f9f9f9; border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow-x: auto; white-space: pre-wrap; word-break: break-all; }
34
45
  code { font-family: 'Courier New', monospace; font-size: 13px; }
46
+ @media (max-width: 480px) {
47
+ code { font-size: 12px; }
48
+ }
35
49
  footer.site-footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid var(--border); color: var(--muted); text-align: center; }
36
50
  footer.site-footer a { color: var(--fg); text-decoration: none; transition: color 0.2s ease; }
37
51
  footer.site-footer a:hover { color: var(--accent); }
@@ -71,7 +85,18 @@
71
85
  <h2>调试部分</h2>
72
86
  <pre><code>curl -s {origin}/inner/version
73
87
  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> · <a href="/listen_together_host.html">一起听示例</p> · <a href="/playlist_cover_update.html">更新歌单封面示例</p> · <a href="/avatar_update.html">头像更新示例</p>
88
+ <div style="margin-top:10px; line-height:2;">
89
+ <a href="/api.html">交互式调试</a> ·
90
+ <a href="/qrlogin.html">二维码登录示例</a> ·
91
+ <a href="/unblock_test.html">解灰测试</a> ·
92
+ <a href="/audio_match_demo/index.html">听歌识曲 Demo</a> ·
93
+ <a href="/cloud.html">云盘上传</a> ·
94
+ <a href="/playlist_import.html">歌单导入</a> ·
95
+ <a href="/eapi_decrypt.html">EAPI 解密</a> ·
96
+ <a href="/listen_together_host.html">一起听示例</a> ·
97
+ <a href="/playlist_cover_update.html">更新歌单封面示例</a> ·
98
+ <a href="/avatar_update.html">头像更新示例</a>
99
+ </div>
75
100
  </section>
76
101
 
77
102
  <footer class="site-footer">
Binary file
package/server.js CHANGED
@@ -178,10 +178,25 @@ async function consturctServer(moduleDefs) {
178
178
  /**
179
179
  * Body Parser and File Upload
180
180
  */
181
- app.use(express.json({ limit: '50mb' }))
182
- app.use(express.urlencoded({ extended: false, limit: '50mb' }))
183
-
184
- app.use(fileUpload())
181
+ const MAX_UPLOAD_SIZE_MB = 500
182
+ const MAX_UPLOAD_SIZE_BYTES = MAX_UPLOAD_SIZE_MB * 1024 * 1024
183
+
184
+ app.use(express.json({ limit: `${MAX_UPLOAD_SIZE_MB}mb` }))
185
+ app.use(
186
+ express.urlencoded({ extended: false, limit: `${MAX_UPLOAD_SIZE_MB}mb` }),
187
+ )
188
+
189
+ app.use(
190
+ fileUpload({
191
+ limits: {
192
+ fileSize: MAX_UPLOAD_SIZE_BYTES,
193
+ },
194
+ useTempFiles: true,
195
+ tempFileDir: require('os').tmpdir(),
196
+ abortOnLimit: true,
197
+ parseNested: true,
198
+ }),
199
+ )
185
200
 
186
201
  /**
187
202
  * Cache
@@ -227,25 +242,30 @@ async function consturctServer(moduleDefs) {
227
242
  const moduleResponse = await moduleDef.module(query, (...params) => {
228
243
  // 参数注入客户端IP
229
244
  const obj = [...params]
230
- let ip = req.ip
245
+ const options = obj[2] || {}
246
+ if (!options.randomCNIP) {
247
+ let ip = req.ip
231
248
 
232
- if (ip.substring(0, 7) == '::ffff:') {
233
- ip = ip.substring(7)
234
- }
235
- if (ip == '::1') {
236
- ip = global.cnIp
237
- }
238
- // logger.info('Requested from ip:', ip)
239
- obj[3] = {
240
- ...obj[3],
241
- ip,
249
+ if (ip.substring(0, 7) == '::ffff:') {
250
+ ip = ip.substring(7)
251
+ }
252
+ if (ip == '::1') {
253
+ ip = global.cnIp
254
+ }
255
+ // logger.info('Requested from ip:', ip)
256
+ obj[2] = {
257
+ ...options,
258
+ ip,
259
+ }
242
260
  }
261
+
243
262
  return request(...obj)
244
263
  })
245
264
  logger.info(`Request Success: ${decode(req.originalUrl)}`)
246
265
 
266
+ // 夹带私货部分:如果开启了通用解锁,并且是获取歌曲URL的接口,则尝试解锁(如果需要的话)ヾ(≧▽≦*)o
247
267
  if (
248
- (req.baseUrl === '/song/url/v1' || req.baseUrl === '/song/url') &&
268
+ req.baseUrl === '/song/url/v1' &&
249
269
  process.env.ENABLE_GENERAL_UNBLOCK === 'true'
250
270
  ) {
251
271
  const song = moduleResponse.body.data[0]
@@ -260,7 +280,7 @@ async function consturctServer(moduleDefs) {
260
280
  logger.info('Starting unblock(uses general unblock):', req.query.id)
261
281
  const result = await matchID(req.query.id)
262
282
  song.url = result.data.url
263
- song.freeTrialInfo = 'null'
283
+ song.freeTrialInfo = null
264
284
  logger.info('Unblock success! url:', song.url)
265
285
  }
266
286
  if (song.url && song.url.includes('kuwo')) {
@@ -0,0 +1,88 @@
1
+ const fs = require('fs')
2
+ const crypto = require('crypto')
3
+ const logger = require('./logger')
4
+
5
+ function isTempFile(file) {
6
+ return !!(file && file.tempFilePath)
7
+ }
8
+
9
+ async function getFileSize(file) {
10
+ if (isTempFile(file)) {
11
+ const stats = await fs.promises.stat(file.tempFilePath)
12
+ return stats.size
13
+ }
14
+ return file.data ? file.data.byteLength : file.size || 0
15
+ }
16
+
17
+ async function getFileMd5(file) {
18
+ if (file.md5) {
19
+ return file.md5
20
+ }
21
+
22
+ if (isTempFile(file)) {
23
+ return new Promise((resolve, reject) => {
24
+ const hash = crypto.createHash('md5')
25
+ const stream = fs.createReadStream(file.tempFilePath)
26
+ stream.on('data', (chunk) => hash.update(chunk))
27
+ stream.on('end', () => resolve(hash.digest('hex')))
28
+ stream.on('error', reject)
29
+ })
30
+ }
31
+
32
+ if (file.data) {
33
+ return crypto.createHash('md5').update(file.data).digest('hex')
34
+ }
35
+
36
+ throw new Error('无法计算文件MD5: 缺少文件数据')
37
+ }
38
+
39
+ function getUploadData(file) {
40
+ if (isTempFile(file)) {
41
+ return fs.createReadStream(file.tempFilePath)
42
+ }
43
+ return file.data
44
+ }
45
+
46
+ async function cleanupTempFile(filePath) {
47
+ if (!filePath) return
48
+ try {
49
+ await fs.promises.unlink(filePath)
50
+ } catch (e) {
51
+ logger.info('临时文件清理失败:', e.message)
52
+ }
53
+ }
54
+
55
+ async function readFileChunk(filePath, offset, length) {
56
+ const fd = await fs.promises.open(filePath, 'r')
57
+ const buffer = Buffer.alloc(length)
58
+ await fd.read(buffer, 0, length, offset)
59
+ await fd.close()
60
+ return buffer
61
+ }
62
+
63
+ function getFileExtension(filename) {
64
+ if (!filename) return 'mp3'
65
+ if (filename.includes('.')) {
66
+ return filename.split('.').pop().toLowerCase()
67
+ }
68
+ return 'mp3'
69
+ }
70
+
71
+ function sanitizeFilename(filename) {
72
+ if (!filename) return 'unknown'
73
+ return filename
74
+ .replace(/\.[^.]+$/, '')
75
+ .replace(/\s/g, '')
76
+ .replace(/\./g, '_')
77
+ }
78
+
79
+ module.exports = {
80
+ isTempFile,
81
+ getFileSize,
82
+ getFileMd5,
83
+ getUploadData,
84
+ cleanupTempFile,
85
+ readFileChunk,
86
+ getFileExtension,
87
+ sanitizeFilename,
88
+ }
package/util/index.js CHANGED
@@ -1,36 +1,8 @@
1
1
  const logger = require('./logger')
2
- // 预先定义常量和函数引用
3
- // 中国 IP 段(来源:data/ChineseIPGenerate.csv)
4
- const chinaIPRangesRaw = [
5
- // 开始IP, 结束IP, IP个数, 位置
6
- ['1.0.1.0', '1.0.3.255', 768, '福州'],
7
- ['1.0.8.0', '1.0.15.255', 2048, '广州'],
8
- ['1.0.32.0', '1.0.63.255', 8192, '广州'],
9
- ['1.1.0.0', '1.1.0.255', 256, '福州'],
10
- ['1.1.2.0', '1.1.63.255', 15872, '广州'],
11
- ['1.2.0.0', '1.2.2.255', 768, '北京'],
12
- ['1.2.4.0', '1.2.127.255', 31744, '广州'],
13
- ['1.3.0.0', '1.3.255.255', 65536, '广州'],
14
- ['1.4.1.0', '1.4.127.255', 32512, '广州'],
15
- ['1.8.0.0', '1.8.255.255', 65536, '北京'],
16
- ['1.10.0.0', '1.10.9.255', 2560, '福州'],
17
- ['1.10.11.0', '1.10.127.255', 29952, '广州'],
18
- ['1.12.0.0', '1.15.255.255', 262144, '上海'],
19
- ['1.18.128.0', '1.18.128.255', 256, '北京'],
20
- ['1.24.0.0', '1.31.255.255', 524288, '赤峰'],
21
- ['1.45.0.0', '1.45.255.255', 65536, '北京'],
22
- ['1.48.0.0', '1.51.255.255', 262144, '济南'],
23
- ['1.56.0.0', '1.63.255.255', 524288, '伊春'],
24
- ['1.68.0.0', '1.71.255.255', 262144, '忻州'],
25
- ['1.80.0.0', '1.95.255.255', 1048576, '北京'],
26
- ['1.116.0.0', '1.117.255.255', 131072, '上海'],
27
- ['1.119.0.0', '1.119.255.255', 65536, '北京'],
28
- ['1.180.0.0', '1.185.255.255', 393216, '桂林'],
29
- ['1.188.0.0', '1.199.255.255', 786432, '洛阳'],
30
- ['1.202.0.0', '1.207.255.255', 393216, '铜仁'],
31
- ]
32
-
33
- // 将原始字符串段转换为数值段并计算总数(在模块初始化时完成一次)
2
+ const fs = require('fs')
3
+ const path = require('path')
4
+
5
+ // IP地址转换函数
34
6
  function ipToInt(ip) {
35
7
  const parts = ip.split('.').map(Number)
36
8
  const a = (parts[0] << 24) >>> 0
@@ -49,20 +21,56 @@ function intToIp(int) {
49
21
  ].join('.')
50
22
  }
51
23
 
52
- const chinaIPRanges = (function buildRanges() {
53
- const arr = []
54
- let total = 0
55
- for (let i = 0; i < chinaIPRangesRaw.length; i++) {
56
- const r = chinaIPRangesRaw[i]
57
- const start = ipToInt(r[0])
58
- const end = ipToInt(r[1])
59
- const count = r[2] || end - start + 1
60
- arr.push({ start, end, count, location: r[3] || '' })
61
- total += count
24
+ // 解析CIDR格式的IP段
25
+ function parseCIDR(cidr) {
26
+ const [ipStr, prefixLengthStr] = cidr.split('/')
27
+ const prefixLength = parseInt(prefixLengthStr, 10)
28
+
29
+ const ipInt = ipToInt(ipStr)
30
+ const mask = (0xffffffff << (32 - prefixLength)) >>> 0
31
+ const start = (ipInt & mask) >>> 0
32
+ const end = (start | (~mask >>> 0)) >>> 0
33
+ const count = end - start + 1
34
+
35
+ return { start, end, count, cidr }
36
+ }
37
+
38
+ // 从china_ip_ranges.txt加载中国IP段(CIDR格式)
39
+ const chinaIPRanges = (function loadChinaIPRanges() {
40
+ try {
41
+ const filePath = path.join(__dirname, '../data/china_ip_ranges.txt')
42
+ const content = fs.readFileSync(filePath, 'utf-8')
43
+ const lines = content
44
+ .split('\n')
45
+ .filter((line) => line.trim() && !line.startsWith('#'))
46
+
47
+ const arr = []
48
+ let total = 0
49
+
50
+ for (let i = 0; i < lines.length; i++) {
51
+ const line = lines[i].trim()
52
+ if (!line) continue
53
+
54
+ const range = parseCIDR(line)
55
+ arr.push(range)
56
+ total += range.count
57
+ }
58
+
59
+ // 按IP段大小排序,提高随机选择效率
60
+ arr.sort((a, b) => b.count - a.count)
61
+
62
+ // attach total for convenience
63
+ arr.totalCount = total
64
+
65
+ logger.info(
66
+ `Loaded ${arr.length} Chinese IP ranges from china_ip_ranges.txt, total ${total} IPs`,
67
+ )
68
+ return arr
69
+ } catch (error) {
70
+ logger.error('Failed to load china_ip_ranges.txt:', error.message)
71
+ // 返回空数组,generateRandomChineseIP会使用兜底逻辑
72
+ return { totalCount: 0 }
62
73
  }
63
- // attach total for convenience
64
- arr.totalCount = total
65
- return arr
66
74
  })()
67
75
  const floor = Math.floor
68
76
  const random = Math.random
@@ -144,16 +152,11 @@ module.exports = {
144
152
  // 如果没有选中(理论上不应该发生),回退到最后一个段
145
153
  if (!chosen) chosen = chinaIPRanges[chinaIPRanges.length - 1]
146
154
 
147
- // 在段内随机生成一个 IP(使用段真实的数值范围,而非 csv 中的 count)
155
+ // 在段内随机生成一个 IP(使用段真实的数值范围)
148
156
  const segSize = chosen.end - chosen.start + 1
149
157
  const ipInt = chosen.start + Math.floor(random() * segSize)
150
158
  const ip = intToIp(ipInt)
151
- logger.info(
152
- 'Generated Random Chinese IP:',
153
- ip,
154
- 'location:',
155
- chosen.location,
156
- )
159
+ logger.info('Generated Random Chinese IP:', ip, 'from CIDR:', chosen.cidr)
157
160
  return ip
158
161
  },
159
162
  // 生成chainId的函数
@@ -1,26 +0,0 @@
1
- 开始IP,结束IP,IP个数,位置
2
- 1.0.1.0 ,1.0.3.255 ,768,福州
3
- 1.0.8.0 ,1.0.15.255 ,2048,广州
4
- 1.0.32.0 ,1.0.63.255 ,8192,广州
5
- 1.1.0.0 ,1.1.0.255 ,256,福州
6
- 1.1.2.0 ,1.1.63.255 ,15872,广州
7
- 1.2.0.0 ,1.2.2.255 ,768,北京
8
- 1.2.4.0 ,1.2.127.255 ,31744,广州
9
- 1.3.0.0 ,1.3.255.255 ,65536,广州
10
- 1.4.1.0 ,1.4.127.255 ,32512,广州
11
- 1.8.0.0 ,1.8.255.255 ,65536,北京
12
- 1.10.0.0 ,1.10.9.255 ,2560,福州
13
- 1.10.11.0 ,1.10.127.255 ,29952,广州
14
- 1.12.0.0 ,1.15.255.255 ,262144,上海
15
- 1.18.128.0 ,1.18.128.255 ,256,北京
16
- 1.24.0.0 ,1.31.255.255 ,524288,赤峰
17
- 1.45.0.0 ,1.45.255.255 ,65536,北京
18
- 1.48.0.0 ,1.51.255.255 ,262144,济南
19
- 1.56.0.0 ,1.63.255.255 ,524288,伊春
20
- 1.68.0.0 ,1.71.255.255 ,262144,忻州
21
- 1.80.0.0 ,1.95.255.255 ,1048576,北京
22
- 1.116.0.0 ,1.117.255.255 ,131072,上海
23
- 1.119.0.0 ,1.119.255.255 ,65536,北京
24
- 1.180.0.0 ,1.185.255.255 ,393216,桂林
25
- 1.188.0.0 ,1.199.255.255 ,786432,洛阳
26
- 1.202.0.0 ,1.207.255.255 ,393216,铜仁
Binary file