@neteasecloudmusicapienhanced/api 4.30.1 → 4.30.3

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/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
- const fileUpdateTime = {}
161
- let fileLength = 0
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
- document
165
- .querySelector('input[type="file"]')
166
- .addEventListener('change', function (e) {
167
- const files = this.files
168
- if (files.length === 0) return
169
-
170
- fileLength = files.length
171
- for (let i = 0; i < files.length; i++) {
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 upload(file, currentIndex) {
179
- var formData = new FormData()
180
- formData.append('songFile', file)
342
+ async function uploadFilesSequentially(files) {
343
+ isUploading = true
344
+ uploadBtn.classList.add('disabled')
345
+ progressSection.classList.add('active')
346
+ progressSection.innerHTML = ''
181
347
 
182
- axios({
183
- method: 'post',
184
- url: `/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
185
- headers: {
186
- 'Content-Type': 'multipart/form-data',
187
- },
188
- data: formData,
189
- })
190
- .then((res) => {
191
- console.log(`${file.name} 上传成功`)
192
- if (currentIndex >= fileLength) {
193
- console.log('所有文件上传完毕')
194
- }
195
- app.getData()
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
- .catch((err) => {
198
- console.error(`${file.name} 上传失败:`, err)
199
- fileUpdateTime[file.name] = (fileUpdateTime[file.name] || 0) + 1
200
- if (fileUpdateTime[file.name] >= 4) {
201
- console.error(`文件 ${file.name} 上传失败次数过多,已停止重试`)
202
- return
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
- console.error(`${file.name} 上传失败 ${fileUpdateTime[file.name]} 次,正在重试...`)
205
- upload(file, currentIndex)
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>