@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.
- package/README.MD +47 -23
- package/data/china_ip_ranges.txt +4147 -0
- package/module/cloud.js +136 -119
- package/module/cloud_upload_complete.js +72 -0
- package/module/cloud_upload_token.js +111 -0
- package/module/comment_info_list.js +30 -0
- package/module/song_url_ncmget.js +2 -74
- package/module/voice_upload.js +36 -22
- package/package.json +19 -25
- package/plugins/songUpload.js +67 -19
- package/plugins/upload.js +5 -7
- package/public/cloud.html +406 -39
- package/public/docs/home.md +134 -4
- package/public/docs/index.html +1 -1
- package/public/docs/logo.svg +6 -0
- package/public/docs/netease.png +0 -0
- package/public/index.html +29 -4
- package/public/static/docs.png +0 -0
- package/server.js +37 -17
- package/util/fileHelper.js +88 -0
- package/util/index.js +55 -52
- package/data/ChineseIPGenerate.csv +0 -26
- package/public/docs/ncmapireborn.png +0 -0
- /package/public/static/{screenshot1.png → module_test.png} +0 -0
package/public/cloud.html
CHANGED
|
@@ -47,6 +47,59 @@
|
|
|
47
47
|
text-decoration: underline;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
.mode-section {
|
|
51
|
+
margin-bottom: 24px;
|
|
52
|
+
padding: 16px;
|
|
53
|
+
background: #f9f9f9;
|
|
54
|
+
border-radius: 8px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.mode-section label {
|
|
58
|
+
display: block;
|
|
59
|
+
font-size: 14px;
|
|
60
|
+
font-weight: 500;
|
|
61
|
+
color: #333;
|
|
62
|
+
margin-bottom: 12px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.mode-options {
|
|
66
|
+
display: flex;
|
|
67
|
+
gap: 16px;
|
|
68
|
+
flex-wrap: wrap;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.mode-option {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: flex-start;
|
|
74
|
+
gap: 8px;
|
|
75
|
+
cursor: pointer;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.mode-option input[type="radio"] {
|
|
79
|
+
margin-top: 3px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.mode-option-text {
|
|
83
|
+
display: flex;
|
|
84
|
+
flex-direction: column;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.mode-option-title {
|
|
88
|
+
font-size: 14px;
|
|
89
|
+
color: #333;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.mode-option-desc {
|
|
93
|
+
font-size: 12px;
|
|
94
|
+
color: #999;
|
|
95
|
+
margin-top: 2px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.mode-option input[type="radio"]:checked + .mode-option-text .mode-option-title {
|
|
99
|
+
color: #333;
|
|
100
|
+
font-weight: 500;
|
|
101
|
+
}
|
|
102
|
+
|
|
50
103
|
.upload-section {
|
|
51
104
|
margin-bottom: 32px;
|
|
52
105
|
}
|
|
@@ -72,6 +125,11 @@
|
|
|
72
125
|
display: none;
|
|
73
126
|
}
|
|
74
127
|
|
|
128
|
+
.upload-btn.disabled {
|
|
129
|
+
background: #ccc;
|
|
130
|
+
cursor: not-allowed;
|
|
131
|
+
}
|
|
132
|
+
|
|
75
133
|
.songs-list {
|
|
76
134
|
list-style: none;
|
|
77
135
|
}
|
|
@@ -99,6 +157,74 @@
|
|
|
99
157
|
padding: 20px;
|
|
100
158
|
color: #666;
|
|
101
159
|
}
|
|
160
|
+
|
|
161
|
+
.progress-section {
|
|
162
|
+
margin-bottom: 24px;
|
|
163
|
+
display: none;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.progress-section.active {
|
|
167
|
+
display: block;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.progress-item {
|
|
171
|
+
margin-bottom: 12px;
|
|
172
|
+
padding: 12px;
|
|
173
|
+
background: #f9f9f9;
|
|
174
|
+
border-radius: 6px;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.progress-item .name {
|
|
178
|
+
font-size: 14px;
|
|
179
|
+
color: #333;
|
|
180
|
+
margin-bottom: 8px;
|
|
181
|
+
word-break: break-all;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.progress-item .status {
|
|
185
|
+
font-size: 12px;
|
|
186
|
+
color: #666;
|
|
187
|
+
margin-bottom: 6px;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.progress-bar {
|
|
191
|
+
height: 6px;
|
|
192
|
+
background: #e0e0e0;
|
|
193
|
+
border-radius: 3px;
|
|
194
|
+
overflow: hidden;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.progress-bar .fill {
|
|
198
|
+
height: 100%;
|
|
199
|
+
background: #333;
|
|
200
|
+
border-radius: 3px;
|
|
201
|
+
transition: width 0.3s ease;
|
|
202
|
+
width: 0%;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.progress-item.success .fill {
|
|
206
|
+
background: #4caf50;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.progress-item.error .fill {
|
|
210
|
+
background: #f44336;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.progress-item.error .status {
|
|
214
|
+
color: #f44336;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.info-text {
|
|
218
|
+
font-size: 12px;
|
|
219
|
+
color: #999;
|
|
220
|
+
margin-top: 8px;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.warning-text {
|
|
224
|
+
font-size: 12px;
|
|
225
|
+
color: #e65100;
|
|
226
|
+
margin-top: 8px;
|
|
227
|
+
}
|
|
102
228
|
</style>
|
|
103
229
|
</head>
|
|
104
230
|
|
|
@@ -107,13 +233,36 @@
|
|
|
107
233
|
<h1>云盘上传</h1>
|
|
108
234
|
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
|
|
109
235
|
|
|
236
|
+
<div class="mode-section">
|
|
237
|
+
<label>上传模式</label>
|
|
238
|
+
<div class="mode-options">
|
|
239
|
+
<label class="mode-option">
|
|
240
|
+
<input type="radio" name="uploadMode" value="direct" checked />
|
|
241
|
+
<span class="mode-option-text">
|
|
242
|
+
<span class="mode-option-title">客户端直传</span>
|
|
243
|
+
<span class="mode-option-desc">文件直接上传到云存储,支持大文件,适合 Vercel 等平台</span>
|
|
244
|
+
</span>
|
|
245
|
+
</label>
|
|
246
|
+
<label class="mode-option">
|
|
247
|
+
<input type="radio" name="uploadMode" value="proxy" />
|
|
248
|
+
<span class="mode-option-text">
|
|
249
|
+
<span class="mode-option-title">后端代理</span>
|
|
250
|
+
<span class="mode-option-desc">文件通过服务器转发,更简洁,需要服务器支持大文件</span>
|
|
251
|
+
</span>
|
|
252
|
+
</label>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
|
|
110
256
|
<div class="upload-section">
|
|
111
|
-
<label class="upload-btn">
|
|
257
|
+
<label class="upload-btn" id="uploadBtn">
|
|
112
258
|
选择文件(支持多选)
|
|
113
259
|
<input id="file" type="file" multiple accept="audio/*" />
|
|
114
260
|
</label>
|
|
261
|
+
<p class="info-text" id="modeInfo">支持大文件上传,文件将直接传输到云存储服务器</p>
|
|
115
262
|
</div>
|
|
116
263
|
|
|
264
|
+
<div id="progressSection" class="progress-section"></div>
|
|
265
|
+
|
|
117
266
|
<div id="app">
|
|
118
267
|
<div v-if="loading" class="loading">加载中...</div>
|
|
119
268
|
<ul v-else-if="songs.length > 0" class="songs-list">
|
|
@@ -127,6 +276,7 @@
|
|
|
127
276
|
|
|
128
277
|
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
|
129
278
|
<script src="https://fastly.jsdelivr.net/npm/vue@3"></script>
|
|
279
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsmediatags/3.9.5/jsmediatags.min.js"></script>
|
|
130
280
|
<script>
|
|
131
281
|
const app = Vue.createApp({
|
|
132
282
|
data() {
|
|
@@ -157,55 +307,272 @@
|
|
|
157
307
|
},
|
|
158
308
|
}).mount('#app')
|
|
159
309
|
|
|
160
|
-
|
|
161
|
-
let
|
|
310
|
+
let isUploading = false
|
|
311
|
+
let uploadMode = 'direct'
|
|
312
|
+
const progressSection = document.getElementById('progressSection')
|
|
313
|
+
const uploadBtn = document.getElementById('uploadBtn')
|
|
314
|
+
const fileInput = document.querySelector('input[type="file"]')
|
|
315
|
+
const modeInfo = document.getElementById('modeInfo')
|
|
316
|
+
|
|
317
|
+
document.querySelectorAll('input[name="uploadMode"]').forEach(radio => {
|
|
318
|
+
radio.addEventListener('change', function() {
|
|
319
|
+
uploadMode = this.value
|
|
320
|
+
if (uploadMode === 'direct') {
|
|
321
|
+
modeInfo.textContent = '支持大文件上传,文件将直接传输到云存储服务器'
|
|
322
|
+
modeInfo.className = 'info-text'
|
|
323
|
+
} else {
|
|
324
|
+
modeInfo.textContent = '文件将通过服务器转发,服务器需支持大文件上传(Vercel 限制 4.5MB)'
|
|
325
|
+
modeInfo.className = 'warning-text'
|
|
326
|
+
}
|
|
327
|
+
})
|
|
328
|
+
})
|
|
162
329
|
|
|
163
330
|
function main() {
|
|
164
|
-
|
|
165
|
-
.
|
|
166
|
-
.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
upload(files[i], i + 1)
|
|
173
|
-
}
|
|
174
|
-
})
|
|
331
|
+
fileInput.addEventListener('change', function (e) {
|
|
332
|
+
const files = this.files
|
|
333
|
+
if (files.length === 0) return
|
|
334
|
+
if (isUploading) return
|
|
335
|
+
|
|
336
|
+
uploadFilesSequentially(Array.from(files))
|
|
337
|
+
this.value = ''
|
|
338
|
+
})
|
|
175
339
|
}
|
|
176
340
|
main()
|
|
177
341
|
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|
|
342
|
+
async function uploadFilesSequentially(files) {
|
|
343
|
+
isUploading = true
|
|
344
|
+
uploadBtn.classList.add('disabled')
|
|
345
|
+
progressSection.classList.add('active')
|
|
346
|
+
progressSection.innerHTML = ''
|
|
181
347
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
348
|
+
for (let i = 0; i < files.length; i++) {
|
|
349
|
+
if (uploadMode === 'direct') {
|
|
350
|
+
await uploadFileDirect(files[i], i + 1, files.length)
|
|
351
|
+
} else {
|
|
352
|
+
await uploadFileProxy(files[i], i + 1, files.length)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
isUploading = false
|
|
357
|
+
uploadBtn.classList.remove('disabled')
|
|
358
|
+
app.getData()
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function createProgressItem(file, index, total) {
|
|
362
|
+
const item = document.createElement('div')
|
|
363
|
+
item.className = 'progress-item'
|
|
364
|
+
item.id = `progress-${index}`
|
|
365
|
+
item.innerHTML = `
|
|
366
|
+
<div class="name">${file.name} (${formatSize(file.size)})</div>
|
|
367
|
+
<div class="status">准备中...</div>
|
|
368
|
+
<div class="progress-bar"><div class="fill"></div></div>
|
|
369
|
+
`
|
|
370
|
+
progressSection.appendChild(item)
|
|
371
|
+
return item
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function updateProgress(index, status, percent, isError = false) {
|
|
375
|
+
const item = document.getElementById(`progress-${index}`)
|
|
376
|
+
if (!item) return
|
|
377
|
+
item.querySelector('.status').textContent = status
|
|
378
|
+
item.querySelector('.fill').style.width = `${percent}%`
|
|
379
|
+
if (isError) {
|
|
380
|
+
item.classList.add('error')
|
|
381
|
+
} else if (percent >= 100) {
|
|
382
|
+
item.classList.add('success')
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function formatSize(bytes) {
|
|
387
|
+
if (bytes < 1024) return bytes + ' B'
|
|
388
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
|
389
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function uploadFileProxy(file, index, total) {
|
|
393
|
+
createProgressItem(file, index, total)
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
updateProgress(index, '上传中...', 10)
|
|
397
|
+
|
|
398
|
+
const formData = new FormData()
|
|
399
|
+
formData.append('songFile', file)
|
|
400
|
+
|
|
401
|
+
await axios({
|
|
402
|
+
method: 'post',
|
|
403
|
+
url: `/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
|
|
404
|
+
headers: {
|
|
405
|
+
'Content-Type': 'multipart/form-data',
|
|
406
|
+
},
|
|
407
|
+
data: formData,
|
|
408
|
+
onUploadProgress: (progressEvent) => {
|
|
409
|
+
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 90) + 10
|
|
410
|
+
updateProgress(index, `上传中... ${Math.round(progressEvent.loaded / progressEvent.total * 100)}%`, Math.min(percent, 100))
|
|
411
|
+
},
|
|
412
|
+
timeout: 600000,
|
|
196
413
|
})
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
414
|
+
|
|
415
|
+
updateProgress(index, '上传完成!', 100)
|
|
416
|
+
|
|
417
|
+
} catch (err) {
|
|
418
|
+
console.error(`${file.name} 上传失败:`, err)
|
|
419
|
+
const errorMsg = err.response?.data?.msg || err.message || '未知错误'
|
|
420
|
+
if (err.response?.status === 413 || errorMsg.includes('PAYLOAD_TOO_LARGE')) {
|
|
421
|
+
updateProgress(index, '文件过大,请切换到客户端直传模式', 0, true)
|
|
422
|
+
} else {
|
|
423
|
+
updateProgress(index, `上传失败: ${errorMsg}`, 0, true)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function calculateMD5(file) {
|
|
429
|
+
return new Promise((resolve, reject) => {
|
|
430
|
+
const chunkSize = 2 * 1024 * 1024
|
|
431
|
+
const chunks = Math.ceil(file.size / chunkSize)
|
|
432
|
+
let currentChunk = 0
|
|
433
|
+
const spark = new SparkMD5.ArrayBuffer()
|
|
434
|
+
const reader = new FileReader()
|
|
435
|
+
|
|
436
|
+
reader.onload = (e) => {
|
|
437
|
+
spark.append(e.target.result)
|
|
438
|
+
currentChunk++
|
|
439
|
+
if (currentChunk < chunks) {
|
|
440
|
+
loadNext()
|
|
203
441
|
} else {
|
|
204
|
-
|
|
205
|
-
|
|
442
|
+
resolve(spark.end())
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
reader.onerror = () => reject(reader.error)
|
|
447
|
+
|
|
448
|
+
function loadNext() {
|
|
449
|
+
const start = currentChunk * chunkSize
|
|
450
|
+
const end = Math.min(start + chunkSize, file.size)
|
|
451
|
+
reader.readAsArrayBuffer(file.slice(start, end))
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
loadNext()
|
|
455
|
+
})
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function parseMediaTags(file) {
|
|
459
|
+
return new Promise((resolve) => {
|
|
460
|
+
jsmediatags.read(file, {
|
|
461
|
+
onSuccess: function(tag) {
|
|
462
|
+
resolve({
|
|
463
|
+
title: tag.tags.title || null,
|
|
464
|
+
artist: tag.tags.artist || null,
|
|
465
|
+
album: tag.tags.album || null,
|
|
466
|
+
})
|
|
467
|
+
},
|
|
468
|
+
onError: function() {
|
|
469
|
+
resolve({ title: null, artist: null, album: null })
|
|
206
470
|
}
|
|
207
471
|
})
|
|
472
|
+
})
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function uploadFileDirect(file, index, total) {
|
|
476
|
+
createProgressItem(file, index, total)
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
updateProgress(index, '计算文件MD5...', 5)
|
|
480
|
+
|
|
481
|
+
const md5 = await calculateMD5(file)
|
|
482
|
+
const fileSize = file.size
|
|
483
|
+
const filename = file.name
|
|
484
|
+
|
|
485
|
+
updateProgress(index, '解析音频元数据...', 8)
|
|
486
|
+
|
|
487
|
+
const mediaTags = await parseMediaTags(file)
|
|
488
|
+
|
|
489
|
+
updateProgress(index, '获取上传凭证...', 10)
|
|
490
|
+
|
|
491
|
+
const tokenRes = await axios({
|
|
492
|
+
method: 'post',
|
|
493
|
+
url: `/cloud/upload/token?time=${Date.now()}`,
|
|
494
|
+
data: {
|
|
495
|
+
cookie: localStorage.getItem('cookie'),
|
|
496
|
+
md5: md5,
|
|
497
|
+
fileSize: fileSize,
|
|
498
|
+
filename: filename,
|
|
499
|
+
},
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
if (tokenRes.data.code !== 200) {
|
|
503
|
+
throw new Error(tokenRes.data.msg || '获取上传凭证失败')
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const tokenData = tokenRes.data.data
|
|
507
|
+
|
|
508
|
+
if (!tokenData.needUpload) {
|
|
509
|
+
updateProgress(index, '文件已存在,直接导入云盘...', 80)
|
|
510
|
+
await completeUpload(tokenData, file, mediaTags)
|
|
511
|
+
updateProgress(index, '上传完成!', 100)
|
|
512
|
+
return
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
updateProgress(index, '开始上传到云存储...', 15)
|
|
516
|
+
|
|
517
|
+
await axios({
|
|
518
|
+
method: 'post',
|
|
519
|
+
url: tokenData.uploadUrl,
|
|
520
|
+
headers: {
|
|
521
|
+
'x-nos-token': tokenData.uploadToken,
|
|
522
|
+
'Content-MD5': md5,
|
|
523
|
+
'Content-Type': 'audio/mpeg',
|
|
524
|
+
'Content-Length': String(fileSize),
|
|
525
|
+
},
|
|
526
|
+
data: file,
|
|
527
|
+
onUploadProgress: (progressEvent) => {
|
|
528
|
+
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 70) + 15
|
|
529
|
+
updateProgress(index, `上传中... ${Math.round(progressEvent.loaded / progressEvent.total * 100)}%`, Math.min(percent, 85))
|
|
530
|
+
},
|
|
531
|
+
maxContentLength: Infinity,
|
|
532
|
+
maxBodyLength: Infinity,
|
|
533
|
+
timeout: 600000,
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
updateProgress(index, '上传完成,正在导入云盘...', 90)
|
|
537
|
+
|
|
538
|
+
await completeUpload(tokenData, file, mediaTags)
|
|
539
|
+
|
|
540
|
+
updateProgress(index, '上传完成!', 100)
|
|
541
|
+
|
|
542
|
+
} catch (err) {
|
|
543
|
+
console.error(`${file.name} 上传失败:`, err)
|
|
544
|
+
const errorMsg = err.response?.data?.msg || err.message || '未知错误'
|
|
545
|
+
updateProgress(index, `上传失败: ${errorMsg}`, 0, true)
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function completeUpload(tokenData, file, mediaTags = {}) {
|
|
550
|
+
const songName = mediaTags.title || file.name.replace(/\.[^.]+$/, '')
|
|
551
|
+
const artist = mediaTags.artist || '未知艺术家'
|
|
552
|
+
const album = mediaTags.album || '未知专辑'
|
|
553
|
+
|
|
554
|
+
const completeRes = await axios({
|
|
555
|
+
method: 'post',
|
|
556
|
+
url: `/cloud/upload/complete?time=${Date.now()}`,
|
|
557
|
+
data: {
|
|
558
|
+
cookie: localStorage.getItem('cookie'),
|
|
559
|
+
songId: tokenData.songId,
|
|
560
|
+
resourceId: tokenData.resourceId,
|
|
561
|
+
md5: tokenData.md5,
|
|
562
|
+
filename: file.name,
|
|
563
|
+
song: songName,
|
|
564
|
+
artist: artist,
|
|
565
|
+
album: album,
|
|
566
|
+
},
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
if (completeRes.data.code !== 200) {
|
|
570
|
+
throw new Error(completeRes.data.msg || '导入云盘失败')
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return completeRes.data
|
|
208
574
|
}
|
|
209
575
|
</script>
|
|
576
|
+
<script src="https://fastly.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js"></script>
|
|
210
577
|
</body>
|
|
211
578
|
</html>
|
package/public/docs/home.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
网易云音乐 NodeJS API Enhanced
|
|
4
4
|
|
|
5
|
+
最后更新于: 2026.2.15
|
|
6
|
+
|
|
5
7
|
## 灵感来自
|
|
6
8
|
|
|
7
9
|
[disoul/electron-cloud-music](https://github.com/disoul/electron-cloud-music)
|
|
@@ -1684,6 +1686,62 @@ tags: 歌单标签
|
|
|
1684
1686
|
|
|
1685
1687
|
**调用例子 :** `/comment/video?id=89ADDE33C0AAE8EC14B99F6750DB954D`
|
|
1686
1688
|
|
|
1689
|
+
### 评论统计数据
|
|
1690
|
+
|
|
1691
|
+
说明 : 调用此接口 , 传入资源类型和资源 id 列表 , 可批量获取对应资源的评论统计数据 ( 不需要登录 )
|
|
1692
|
+
|
|
1693
|
+
**必选参数 :**
|
|
1694
|
+
|
|
1695
|
+
`type`: 数字 , 资源类型 , 对应以下类型
|
|
1696
|
+
|
|
1697
|
+
```
|
|
1698
|
+
0: 歌曲
|
|
1699
|
+
|
|
1700
|
+
1: mv
|
|
1701
|
+
|
|
1702
|
+
2: 歌单
|
|
1703
|
+
|
|
1704
|
+
3: 专辑
|
|
1705
|
+
|
|
1706
|
+
4: 电台节目
|
|
1707
|
+
|
|
1708
|
+
5: 视频
|
|
1709
|
+
|
|
1710
|
+
6: 动态
|
|
1711
|
+
|
|
1712
|
+
7: 电台
|
|
1713
|
+
```
|
|
1714
|
+
|
|
1715
|
+
`ids`: 资源 id 列表 , 多个 id 用逗号分隔 , 如 `186016,347230`
|
|
1716
|
+
|
|
1717
|
+
**接口地址 :** `/comment/info/list`
|
|
1718
|
+
|
|
1719
|
+
**调用例子 :** `/comment/info/list?type=0&ids=186016,347230`
|
|
1720
|
+
|
|
1721
|
+
**返回数据 :**
|
|
1722
|
+
|
|
1723
|
+
```json
|
|
1724
|
+
{
|
|
1725
|
+
"data": [
|
|
1726
|
+
{
|
|
1727
|
+
"latestLikedUsers": null,
|
|
1728
|
+
"liked": false,
|
|
1729
|
+
"comments": null,
|
|
1730
|
+
"resourceType": 4,
|
|
1731
|
+
"resourceId": 186016,
|
|
1732
|
+
"commentUpgraded": false,
|
|
1733
|
+
"musicianSaidCount": 0,
|
|
1734
|
+
"commentCountDesc": "100w+",
|
|
1735
|
+
"likedCount": 347,
|
|
1736
|
+
"commentCount": 1970844,
|
|
1737
|
+
"shareCount": 109721,
|
|
1738
|
+
"threadId": "R_SO_4_186016"
|
|
1739
|
+
}
|
|
1740
|
+
],
|
|
1741
|
+
"code": 200
|
|
1742
|
+
}
|
|
1743
|
+
```
|
|
1744
|
+
|
|
1687
1745
|
### 热门评论
|
|
1688
1746
|
|
|
1689
1747
|
说明 : 调用此接口 , 传入 type, 资源 id 可获得对应资源热门评论 ( 不需要登录 )
|
|
@@ -2766,7 +2824,7 @@ type : 地区
|
|
|
2766
2824
|
|
|
2767
2825
|
参考: https://github.com/neteasecloudmusicapienhanced/api-enhanced/blob/main/public/cloud.html
|
|
2768
2826
|
|
|
2769
|
-
访问地址: http://localhost:3000/cloud.html
|
|
2827
|
+
访问地址: http://localhost:3000/cloud.html
|
|
2770
2828
|
|
|
2771
2829
|
支持命令行调用,参考 module_example 目录下`song_upload.js`
|
|
2772
2830
|
|
|
@@ -2774,6 +2832,78 @@ type : 地区
|
|
|
2774
2832
|
|
|
2775
2833
|
**调用例子 :** `/cloud`
|
|
2776
2834
|
|
|
2835
|
+
#### 上传模式说明
|
|
2836
|
+
|
|
2837
|
+
云盘上传支持两种模式:
|
|
2838
|
+
|
|
2839
|
+
**1. 后端代理模式 (默认)**
|
|
2840
|
+
|
|
2841
|
+
文件通过服务器转发到云存储,调用简单,但受服务器限制:
|
|
2842
|
+
|
|
2843
|
+
- Vercel Serverless Functions 限制请求体大小为 4.5MB
|
|
2844
|
+
- 自建服务器需配置足够大的请求体限制
|
|
2845
|
+
|
|
2846
|
+
**2. 客户端直传模式 (推荐用于 Vercel)**
|
|
2847
|
+
|
|
2848
|
+
文件直接从客户端上传到云存储服务器,绕过服务器限制:
|
|
2849
|
+
|
|
2850
|
+
- 支持大文件上传
|
|
2851
|
+
- 适合 Vercel、Netlify 等有请求体限制的平台
|
|
2852
|
+
- 需要前端配合实现
|
|
2853
|
+
|
|
2854
|
+
#### 客户端直传相关接口
|
|
2855
|
+
|
|
2856
|
+
**获取上传凭证**
|
|
2857
|
+
|
|
2858
|
+
**接口地址 :** `/cloud/upload/token`
|
|
2859
|
+
|
|
2860
|
+
**必选参数 :**
|
|
2861
|
+
|
|
2862
|
+
- `cookie`: 网易云音乐 Cookie (在请求体中传递)
|
|
2863
|
+
- `md5`: 文件 MD5 值
|
|
2864
|
+
- `fileSize`: 文件大小(字节)
|
|
2865
|
+
- `filename`: 文件名
|
|
2866
|
+
|
|
2867
|
+
**返回数据 :**
|
|
2868
|
+
|
|
2869
|
+
```json
|
|
2870
|
+
{
|
|
2871
|
+
"code": 200,
|
|
2872
|
+
"data": {
|
|
2873
|
+
"needUpload": true,
|
|
2874
|
+
"songId": "...",
|
|
2875
|
+
"uploadToken": "...",
|
|
2876
|
+
"uploadUrl": "...",
|
|
2877
|
+
"resourceId": "..."
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
```
|
|
2881
|
+
|
|
2882
|
+
**完成上传导入**
|
|
2883
|
+
|
|
2884
|
+
**接口地址 :** `/cloud/upload/complete`
|
|
2885
|
+
|
|
2886
|
+
**必选参数 :**
|
|
2887
|
+
|
|
2888
|
+
- `cookie`: 网易云音乐 Cookie (在请求体中传递)
|
|
2889
|
+
- `songId`: 歌曲 ID
|
|
2890
|
+
- `resourceId`: 资源 ID
|
|
2891
|
+
- `md5`: 文件 MD5
|
|
2892
|
+
- `filename`: 文件名
|
|
2893
|
+
|
|
2894
|
+
**可选参数 :**
|
|
2895
|
+
|
|
2896
|
+
- `song`: 歌曲名
|
|
2897
|
+
- `artist`: 艺术家
|
|
2898
|
+
- `album`: 专辑名
|
|
2899
|
+
|
|
2900
|
+
#### 客户端直传流程
|
|
2901
|
+
|
|
2902
|
+
1. 客户端计算文件 MD5
|
|
2903
|
+
2. 调用 `/cloud/upload/token` 获取上传凭证
|
|
2904
|
+
3. 如果 `needUpload` 为 true,直接 PUT 文件到 `uploadUrl`
|
|
2905
|
+
4. 调用 `/cloud/upload/complete` 完成导入
|
|
2906
|
+
|
|
2777
2907
|
### 云盘歌曲信息匹配纠正
|
|
2778
2908
|
|
|
2779
2909
|
说明 : 登录后调用此接口,可对云盘歌曲信息匹配纠正,如需取消匹配,asid 需要传 0
|
|
@@ -2790,6 +2920,7 @@ type : 地区
|
|
|
2790
2920
|
**调用例子 :** `/cloud/match?uid=32953014&sid=aaa&asid=bbb` `/cloud/match?uid=32953014&sid=bbb&asid=0`
|
|
2791
2921
|
|
|
2792
2922
|
### 获取云盘歌词
|
|
2923
|
+
|
|
2793
2924
|
说明: 调用此接口, 获取云盘歌曲的歌词,歌词来自此文件的音乐元数据`LYRICS`标签。
|
|
2794
2925
|
|
|
2795
2926
|
**可选参数 :**
|
|
@@ -4883,12 +5014,11 @@ let data = encodeURIComponent(
|
|
|
4883
5014
|
|
|
4884
5015
|
**调用例子:** `/vip/sign/info`
|
|
4885
5016
|
|
|
4886
|
-
|
|
4887
5017
|
### 用户的创建歌单列表
|
|
4888
5018
|
|
|
4889
5019
|
说明 : 调用此接口, 传入用户id, 获取用户的创建歌单列表
|
|
4890
5020
|
|
|
4891
|
-
**必选参数 :**
|
|
5021
|
+
**必选参数 :**
|
|
4892
5022
|
|
|
4893
5023
|
`uid`: 用户 id
|
|
4894
5024
|
|
|
@@ -4906,7 +5036,7 @@ let data = encodeURIComponent(
|
|
|
4906
5036
|
|
|
4907
5037
|
说明 : 调用此接口, 传入用户id, 获取用户的收藏歌单列表
|
|
4908
5038
|
|
|
4909
|
-
**必选参数 :**
|
|
5039
|
+
**必选参数 :**
|
|
4910
5040
|
|
|
4911
5041
|
`uid`: 用户 id
|
|
4912
5042
|
|
package/public/docs/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="KEYWords" contect="网易云音乐,网易云音乐 api,网易云音乐 nodejs,网易云音乐 node.js">
|
|
6
6
|
<meta name="description" content="网易云音乐 NodeJS API Enhanced">
|
|
7
7
|
<title>网易云音乐 NodeJS API Enhanced</title>
|
|
8
|
-
<link rel="icon" href="
|
|
8
|
+
<link rel="icon" href="netease.png">
|
|
9
9
|
<meta name="description" content="Description">
|
|
10
10
|
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
|
11
11
|
<meta name="referrer" content="never">
|