mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-03-21 19:13:10 +00:00
- 将Cookie参数从URL查询字符串改为请求体传递 - 更新token接口调用以将cookie作为data参数发送 - 更新complete接口调用以将cookie作为data参数发送 - 修改API文档说明Cookie参数传递方式变更 - 确保Cookie值通过POST请求体安全传输
579 lines
16 KiB
HTML
579 lines
16 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>云盘上传</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
min-height: 100vh;
|
||
background: #f5f5f5;
|
||
padding: 20px;
|
||
}
|
||
|
||
.container {
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 32px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
h1 {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.login-link {
|
||
display: block;
|
||
margin-bottom: 24px;
|
||
color: #666;
|
||
font-size: 14px;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.login-link:hover {
|
||
color: #333;
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.mode-section {
|
||
margin-bottom: 24px;
|
||
padding: 16px;
|
||
background: #f9f9f9;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.mode-section label {
|
||
display: block;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.mode-options {
|
||
display: flex;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.mode-option {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.mode-option input[type="radio"] {
|
||
margin-top: 3px;
|
||
}
|
||
|
||
.mode-option-text {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.mode-option-title {
|
||
font-size: 14px;
|
||
color: #333;
|
||
}
|
||
|
||
.mode-option-desc {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.mode-option input[type="radio"]:checked + .mode-option-text .mode-option-title {
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.upload-section {
|
||
margin-bottom: 32px;
|
||
}
|
||
|
||
.upload-btn {
|
||
display: inline-block;
|
||
padding: 12px 28px;
|
||
background: #333;
|
||
color: white;
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
transition: background 0.2s ease;
|
||
border: none;
|
||
}
|
||
|
||
.upload-btn:hover {
|
||
background: #555;
|
||
}
|
||
|
||
.upload-btn input[type="file"] {
|
||
display: none;
|
||
}
|
||
|
||
.upload-btn.disabled {
|
||
background: #ccc;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.songs-list {
|
||
list-style: none;
|
||
}
|
||
|
||
.song-item {
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid #eee;
|
||
font-size: 14px;
|
||
color: #333;
|
||
}
|
||
|
||
.song-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 40px 20px;
|
||
color: #999;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.loading {
|
||
text-align: center;
|
||
padding: 20px;
|
||
color: #666;
|
||
}
|
||
|
||
.progress-section {
|
||
margin-bottom: 24px;
|
||
display: none;
|
||
}
|
||
|
||
.progress-section.active {
|
||
display: block;
|
||
}
|
||
|
||
.progress-item {
|
||
margin-bottom: 12px;
|
||
padding: 12px;
|
||
background: #f9f9f9;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.progress-item .name {
|
||
font-size: 14px;
|
||
color: #333;
|
||
margin-bottom: 8px;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.progress-item .status {
|
||
font-size: 12px;
|
||
color: #666;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.progress-bar {
|
||
height: 6px;
|
||
background: #e0e0e0;
|
||
border-radius: 3px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.progress-bar .fill {
|
||
height: 100%;
|
||
background: #333;
|
||
border-radius: 3px;
|
||
transition: width 0.3s ease;
|
||
width: 0%;
|
||
}
|
||
|
||
.progress-item.success .fill {
|
||
background: #4caf50;
|
||
}
|
||
|
||
.progress-item.error .fill {
|
||
background: #f44336;
|
||
}
|
||
|
||
.progress-item.error .status {
|
||
color: #f44336;
|
||
}
|
||
|
||
.info-text {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.warning-text {
|
||
font-size: 12px;
|
||
color: #e65100;
|
||
margin-top: 8px;
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<div class="container">
|
||
<h1>云盘上传</h1>
|
||
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
|
||
|
||
<div class="mode-section">
|
||
<label>上传模式</label>
|
||
<div class="mode-options">
|
||
<label class="mode-option">
|
||
<input type="radio" name="uploadMode" value="direct" checked />
|
||
<span class="mode-option-text">
|
||
<span class="mode-option-title">客户端直传</span>
|
||
<span class="mode-option-desc">文件直接上传到云存储,支持大文件,适合 Vercel 等平台</span>
|
||
</span>
|
||
</label>
|
||
<label class="mode-option">
|
||
<input type="radio" name="uploadMode" value="proxy" />
|
||
<span class="mode-option-text">
|
||
<span class="mode-option-title">后端代理</span>
|
||
<span class="mode-option-desc">文件通过服务器转发,更简洁,需要服务器支持大文件</span>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="upload-section">
|
||
<label class="upload-btn" id="uploadBtn">
|
||
选择文件(支持多选)
|
||
<input id="file" type="file" multiple accept="audio/*" />
|
||
</label>
|
||
<p class="info-text" id="modeInfo">支持大文件上传,文件将直接传输到云存储服务器</p>
|
||
</div>
|
||
|
||
<div id="progressSection" class="progress-section"></div>
|
||
|
||
<div id="app">
|
||
<div v-if="loading" class="loading">加载中...</div>
|
||
<ul v-else-if="songs.length > 0" class="songs-list">
|
||
<li v-for="(item, index) in songs" :key="index" class="song-item">
|
||
{{ item.songName }}
|
||
</li>
|
||
</ul>
|
||
<div v-else class="empty-state">暂无云盘歌曲</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||
<script src="https://fastly.jsdelivr.net/npm/vue@3"></script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsmediatags/3.9.5/jsmediatags.min.js"></script>
|
||
<script>
|
||
const app = Vue.createApp({
|
||
data() {
|
||
return {
|
||
songs: [],
|
||
loading: false,
|
||
}
|
||
},
|
||
created() {
|
||
this.getData()
|
||
},
|
||
methods: {
|
||
getData() {
|
||
this.loading = true
|
||
axios({
|
||
url: `/user/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
|
||
})
|
||
.then((res) => {
|
||
this.songs = res.data.data || []
|
||
})
|
||
.catch((err) => {
|
||
console.error('获取云盘数据失败:', err)
|
||
})
|
||
.finally(() => {
|
||
this.loading = false
|
||
})
|
||
},
|
||
},
|
||
}).mount('#app')
|
||
|
||
let isUploading = false
|
||
let uploadMode = 'direct'
|
||
const progressSection = document.getElementById('progressSection')
|
||
const uploadBtn = document.getElementById('uploadBtn')
|
||
const fileInput = document.querySelector('input[type="file"]')
|
||
const modeInfo = document.getElementById('modeInfo')
|
||
|
||
document.querySelectorAll('input[name="uploadMode"]').forEach(radio => {
|
||
radio.addEventListener('change', function() {
|
||
uploadMode = this.value
|
||
if (uploadMode === 'direct') {
|
||
modeInfo.textContent = '支持大文件上传,文件将直接传输到云存储服务器'
|
||
modeInfo.className = 'info-text'
|
||
} else {
|
||
modeInfo.textContent = '文件将通过服务器转发,服务器需支持大文件上传(Vercel 限制 4.5MB)'
|
||
modeInfo.className = 'warning-text'
|
||
}
|
||
})
|
||
})
|
||
|
||
function main() {
|
||
fileInput.addEventListener('change', function (e) {
|
||
const files = this.files
|
||
if (files.length === 0) return
|
||
if (isUploading) return
|
||
|
||
uploadFilesSequentially(Array.from(files))
|
||
this.value = ''
|
||
})
|
||
}
|
||
main()
|
||
|
||
async function uploadFilesSequentially(files) {
|
||
isUploading = true
|
||
uploadBtn.classList.add('disabled')
|
||
progressSection.classList.add('active')
|
||
progressSection.innerHTML = ''
|
||
|
||
for (let i = 0; i < files.length; i++) {
|
||
if (uploadMode === 'direct') {
|
||
await uploadFileDirect(files[i], i + 1, files.length)
|
||
} else {
|
||
await uploadFileProxy(files[i], i + 1, files.length)
|
||
}
|
||
}
|
||
|
||
isUploading = false
|
||
uploadBtn.classList.remove('disabled')
|
||
app.getData()
|
||
}
|
||
|
||
function createProgressItem(file, index, total) {
|
||
const item = document.createElement('div')
|
||
item.className = 'progress-item'
|
||
item.id = `progress-${index}`
|
||
item.innerHTML = `
|
||
<div class="name">${file.name} (${formatSize(file.size)})</div>
|
||
<div class="status">准备中...</div>
|
||
<div class="progress-bar"><div class="fill"></div></div>
|
||
`
|
||
progressSection.appendChild(item)
|
||
return item
|
||
}
|
||
|
||
function updateProgress(index, status, percent, isError = false) {
|
||
const item = document.getElementById(`progress-${index}`)
|
||
if (!item) return
|
||
item.querySelector('.status').textContent = status
|
||
item.querySelector('.fill').style.width = `${percent}%`
|
||
if (isError) {
|
||
item.classList.add('error')
|
||
} else if (percent >= 100) {
|
||
item.classList.add('success')
|
||
}
|
||
}
|
||
|
||
function formatSize(bytes) {
|
||
if (bytes < 1024) return bytes + ' B'
|
||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
||
}
|
||
|
||
async function uploadFileProxy(file, index, total) {
|
||
createProgressItem(file, index, total)
|
||
|
||
try {
|
||
updateProgress(index, '上传中...', 10)
|
||
|
||
const formData = new FormData()
|
||
formData.append('songFile', file)
|
||
|
||
await axios({
|
||
method: 'post',
|
||
url: `/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
|
||
headers: {
|
||
'Content-Type': 'multipart/form-data',
|
||
},
|
||
data: formData,
|
||
onUploadProgress: (progressEvent) => {
|
||
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 90) + 10
|
||
updateProgress(index, `上传中... ${Math.round(progressEvent.loaded / progressEvent.total * 100)}%`, Math.min(percent, 100))
|
||
},
|
||
timeout: 600000,
|
||
})
|
||
|
||
updateProgress(index, '上传完成!', 100)
|
||
|
||
} catch (err) {
|
||
console.error(`${file.name} 上传失败:`, err)
|
||
const errorMsg = err.response?.data?.msg || err.message || '未知错误'
|
||
if (err.response?.status === 413 || errorMsg.includes('PAYLOAD_TOO_LARGE')) {
|
||
updateProgress(index, '文件过大,请切换到客户端直传模式', 0, true)
|
||
} else {
|
||
updateProgress(index, `上传失败: ${errorMsg}`, 0, true)
|
||
}
|
||
}
|
||
}
|
||
|
||
async function calculateMD5(file) {
|
||
return new Promise((resolve, reject) => {
|
||
const chunkSize = 2 * 1024 * 1024
|
||
const chunks = Math.ceil(file.size / chunkSize)
|
||
let currentChunk = 0
|
||
const spark = new SparkMD5.ArrayBuffer()
|
||
const reader = new FileReader()
|
||
|
||
reader.onload = (e) => {
|
||
spark.append(e.target.result)
|
||
currentChunk++
|
||
if (currentChunk < chunks) {
|
||
loadNext()
|
||
} else {
|
||
resolve(spark.end())
|
||
}
|
||
}
|
||
|
||
reader.onerror = () => reject(reader.error)
|
||
|
||
function loadNext() {
|
||
const start = currentChunk * chunkSize
|
||
const end = Math.min(start + chunkSize, file.size)
|
||
reader.readAsArrayBuffer(file.slice(start, end))
|
||
}
|
||
|
||
loadNext()
|
||
})
|
||
}
|
||
|
||
async function parseMediaTags(file) {
|
||
return new Promise((resolve) => {
|
||
jsmediatags.read(file, {
|
||
onSuccess: function(tag) {
|
||
resolve({
|
||
title: tag.tags.title || null,
|
||
artist: tag.tags.artist || null,
|
||
album: tag.tags.album || null,
|
||
})
|
||
},
|
||
onError: function() {
|
||
resolve({ title: null, artist: null, album: null })
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
async function uploadFileDirect(file, index, total) {
|
||
createProgressItem(file, index, total)
|
||
|
||
try {
|
||
updateProgress(index, '计算文件MD5...', 5)
|
||
|
||
const md5 = await calculateMD5(file)
|
||
const fileSize = file.size
|
||
const filename = file.name
|
||
|
||
updateProgress(index, '解析音频元数据...', 8)
|
||
|
||
const mediaTags = await parseMediaTags(file)
|
||
|
||
updateProgress(index, '获取上传凭证...', 10)
|
||
|
||
const tokenRes = await axios({
|
||
method: 'post',
|
||
url: `/cloud/upload/token?time=${Date.now()}`,
|
||
data: {
|
||
cookie: localStorage.getItem('cookie'),
|
||
md5: md5,
|
||
fileSize: fileSize,
|
||
filename: filename,
|
||
},
|
||
})
|
||
|
||
if (tokenRes.data.code !== 200) {
|
||
throw new Error(tokenRes.data.msg || '获取上传凭证失败')
|
||
}
|
||
|
||
const tokenData = tokenRes.data.data
|
||
|
||
if (!tokenData.needUpload) {
|
||
updateProgress(index, '文件已存在,直接导入云盘...', 80)
|
||
await completeUpload(tokenData, file, mediaTags)
|
||
updateProgress(index, '上传完成!', 100)
|
||
return
|
||
}
|
||
|
||
updateProgress(index, '开始上传到云存储...', 15)
|
||
|
||
await axios({
|
||
method: 'post',
|
||
url: tokenData.uploadUrl,
|
||
headers: {
|
||
'x-nos-token': tokenData.uploadToken,
|
||
'Content-MD5': md5,
|
||
'Content-Type': 'audio/mpeg',
|
||
'Content-Length': String(fileSize),
|
||
},
|
||
data: file,
|
||
onUploadProgress: (progressEvent) => {
|
||
const percent = Math.round((progressEvent.loaded / progressEvent.total) * 70) + 15
|
||
updateProgress(index, `上传中... ${Math.round(progressEvent.loaded / progressEvent.total * 100)}%`, Math.min(percent, 85))
|
||
},
|
||
maxContentLength: Infinity,
|
||
maxBodyLength: Infinity,
|
||
timeout: 600000,
|
||
})
|
||
|
||
updateProgress(index, '上传完成,正在导入云盘...', 90)
|
||
|
||
await completeUpload(tokenData, file, mediaTags)
|
||
|
||
updateProgress(index, '上传完成!', 100)
|
||
|
||
} catch (err) {
|
||
console.error(`${file.name} 上传失败:`, err)
|
||
const errorMsg = err.response?.data?.msg || err.message || '未知错误'
|
||
updateProgress(index, `上传失败: ${errorMsg}`, 0, true)
|
||
}
|
||
}
|
||
|
||
async function completeUpload(tokenData, file, mediaTags = {}) {
|
||
const songName = mediaTags.title || file.name.replace(/\.[^.]+$/, '')
|
||
const artist = mediaTags.artist || '未知艺术家'
|
||
const album = mediaTags.album || '未知专辑'
|
||
|
||
const completeRes = await axios({
|
||
method: 'post',
|
||
url: `/cloud/upload/complete?time=${Date.now()}`,
|
||
data: {
|
||
cookie: localStorage.getItem('cookie'),
|
||
songId: tokenData.songId,
|
||
resourceId: tokenData.resourceId,
|
||
md5: tokenData.md5,
|
||
filename: file.name,
|
||
song: songName,
|
||
artist: artist,
|
||
album: album,
|
||
},
|
||
})
|
||
|
||
if (completeRes.data.code !== 200) {
|
||
throw new Error(completeRes.data.msg || '导入云盘失败')
|
||
}
|
||
|
||
return completeRes.data
|
||
}
|
||
</script>
|
||
<script src="https://fastly.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js"></script>
|
||
</body>
|
||
</html>
|