262 lines
9.4 KiB
HTML
262 lines
9.4 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<title>vipsong Web — 音乐加密格式转码</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: #f5f6fa; color: #2d3436; min-height: 100vh; padding: 2rem;
|
|
}
|
|
.container { max-width: 720px; margin: 0 auto; }
|
|
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
|
|
.subtitle { color: #636e72; font-size: 0.9rem; margin-bottom: 1.5rem; }
|
|
|
|
.upload-area {
|
|
border: 2px dashed #dfe6e9; border-radius: 12px; padding: 2.5rem 1.5rem;
|
|
text-align: center; cursor: pointer; transition: all .2s; background: #fff;
|
|
}
|
|
.upload-area:hover { border-color: #0984e3; background: #f8f9fa; }
|
|
.upload-area.dragover { border-color: #0984e3; background: #e3f2fd; }
|
|
.upload-area i { font-size: 2rem; color: #b2bec3; margin-bottom: 0.5rem; display: block; }
|
|
.upload-area p { color: #636e72; font-size: 0.9rem; }
|
|
|
|
.format-selector {
|
|
margin: 1.5rem 0; display: flex; align-items: center; gap: 1rem;
|
|
}
|
|
.format-selector label { font-weight: 600; color: #2d3436; }
|
|
select {
|
|
padding: 0.5rem 1rem; border: 1px solid #dfe6e9; border-radius: 8px;
|
|
font-size: 0.9rem; background: #fff; cursor: pointer;
|
|
}
|
|
|
|
.btn-convert {
|
|
display: block; width: 100%; padding: 0.8rem; margin-top: 1rem;
|
|
background: #0984e3; color: #fff; border: none; border-radius: 8px;
|
|
font-size: 1rem; font-weight: 600; cursor: pointer; transition: .2s;
|
|
}
|
|
.btn-convert:hover:not(:disabled) { background: #074c8a; }
|
|
.btn-convert:disabled { background: #b2bec3; cursor: not-allowed; }
|
|
|
|
.file-list { margin-top: 1.5rem; }
|
|
.file-item {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 0.8rem 1rem; border-radius: 8px; margin-bottom: 0.5rem;
|
|
background: #fff; border: 1px solid #dfe6e9;
|
|
}
|
|
.file-info { display: flex; align-items: center; gap: 0.8rem; }
|
|
.file-name { font-size: 0.9rem; max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
.file-status { font-size: 0.8rem; padding: 0.2rem 0.6rem; border-radius: 12px; }
|
|
.status-pending { background: #f1f2f6; color: #636e72; }
|
|
.status-upload { background: #fff3cd; color: #856404; }
|
|
.status-convert { background: #fff3cd; color: #856404; }
|
|
.status-done { background: #d4edda; color: #155724; }
|
|
.status-error { background: #f8d7da; color: #721c24; }
|
|
.btn-download { padding: 0.3rem 0.8rem; background: #00b894; color: #fff; border: none; border-radius: 6px; font-size: 0.8rem; cursor: pointer; }
|
|
.btn-download:hover { background: #0984e3; }
|
|
|
|
.supported { margin-top: 1rem; font-size: 0.8rem; color: #636e72; }
|
|
.supported span { color: #0984e3; font-weight: 600; }
|
|
|
|
/* 弹窗模态框 - 毛玻璃居中效果 */
|
|
.modal-overlay {
|
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
background: rgba(0, 0, 0, 0.45);
|
|
display: flex; align-items: center; justify-content: center;
|
|
z-index: 10000;
|
|
}
|
|
.modal-content {
|
|
background: rgba(255, 255, 255, 0.25);
|
|
border-radius: 16px; padding: 1.8rem 1.5rem;
|
|
text-align: center; width: 340px; max-width: 90%;
|
|
backdrop-filter: blur(12px);
|
|
-webkit-backdrop-filter: blur(12px);
|
|
border: 1px solid rgba(255, 255, 255, 0.35);
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
|
|
}
|
|
.modal-content h3 { color: #2d3436; font-size: 1.1rem; margin-bottom: 0.5rem; }
|
|
.modal-content p { color: #636e72; font-size: 0.85rem; margin-bottom: 1.2rem; line-height: 1.4; }
|
|
.modal-btn {
|
|
padding: 0.5rem 1.5rem; background: #0984e3; color: #fff;
|
|
border: none; border-radius: 8px; font-size: 0.9rem; cursor: pointer;
|
|
transition: background .2s;
|
|
}
|
|
.modal-btn:hover { background: #074c8a; }
|
|
|
|
@media (max-width: 480px) {
|
|
body { padding: 1rem; }
|
|
h1 { font-size: 1.2rem; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>vipsong — 音乐加密格式转码</h1>
|
|
<p class="subtitle">支持 .ncm (网易云) / .kgm .vpr (酷狗) → mp3 / flac / wav / ogg / m4a</p>
|
|
|
|
<form id="uploadForm">
|
|
<div class="upload-area" id="dropZone">
|
|
<i>📁</i>
|
|
<p>点击选择文件,或拖拽到这里</p>
|
|
<input type="file" id="fileInput" multiple accept=".ncm,.kgm,.kgma,.vpr" style="display:none">
|
|
</div>
|
|
|
|
<div class="format-selector">
|
|
<label>输出格式:</label>
|
|
<select id="formatSelect">
|
|
<option value="mp3">MP3 (默认)</option>
|
|
<option value="flac">FLAC (无损)</option>
|
|
<option value="wav">WAV (无损)</option>
|
|
<option value="ogg">OGG</option>
|
|
<option value="m4a">M4A</option>
|
|
</select>
|
|
</div>
|
|
|
|
<button type="button" id="btnConvert" class="btn-convert" disabled>开始转码</button>
|
|
</form>
|
|
|
|
<div class="file-list" id="fileList"></div>
|
|
|
|
<p class="supported">
|
|
<span>支持格式:</span> .ncm / .kgm / .kgma / .vpr
|
|
| <span>输出格式:</span> mp3 / flac / wav / ogg / m4a
|
|
</p>
|
|
</div>
|
|
|
|
<script>
|
|
const dropZone = document.getElementById('dropZone');
|
|
const fileInput = document.getElementById('fileInput');
|
|
const formatSelect = document.getElementById('formatSelect');
|
|
const btnConvert = document.getElementById('btnConvert');
|
|
const fileList = document.getElementById('fileList');
|
|
|
|
let files = [];
|
|
|
|
const SUPPORTED_EXTENSIONS = ['.ncm', '.kgm', '.kgma', '.vpr'];
|
|
|
|
function showModal(message) {
|
|
const existing = document.querySelector('.modal-overlay');
|
|
if (existing) existing.remove();
|
|
const overlay = document.createElement('div');
|
|
overlay.className = 'modal-overlay';
|
|
overlay.innerHTML =
|
|
'<div class="modal-content">' +
|
|
'<h3>⚠️ 文件格式不支持</h3>' +
|
|
'<p>' + message + '</p>' +
|
|
'<button class="modal-btn" onclick="this.closest(\'.modal-overlay\').remove()">确定</button>' +
|
|
'</div>';
|
|
document.body.appendChild(overlay);
|
|
}
|
|
|
|
// 点击上传区域选择文件
|
|
dropZone.addEventListener('click', () => fileInput.click());
|
|
|
|
// 拖拽
|
|
['dragenter', 'dragover'].forEach(evt => {
|
|
dropZone.addEventListener(evt, e => {
|
|
e.preventDefault(); e.stopPropagation();
|
|
dropZone.classList.add('dragover');
|
|
});
|
|
});
|
|
['dragleave', 'drop'].forEach(evt => {
|
|
dropZone.addEventListener(evt, e => {
|
|
e.preventDefault(); e.stopPropagation();
|
|
dropZone.classList.remove('dragover');
|
|
});
|
|
});
|
|
|
|
// 拖拽放下
|
|
dropZone.addEventListener('drop', e => {
|
|
const dt = e.dataTransfer;
|
|
handleFiles(dt.files);
|
|
});
|
|
|
|
// 文件选择
|
|
fileInput.addEventListener('change', e => {
|
|
handleFiles(e.target.files);
|
|
});
|
|
|
|
function handleFiles(fileListObj) {
|
|
for (const f of fileListObj) {
|
|
const ext = f.name.slice(f.name.lastIndexOf('.')).toLowerCase();
|
|
if (!SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
showModal('文件 "' + f.name + '" 格式不受支持<br>仅支持: .ncm / .kgm / .kgma / .vpr 格式的音乐文件');
|
|
continue;
|
|
}
|
|
if (files.some(existing => existing.name === f.name)) continue;
|
|
files.push(f);
|
|
}
|
|
renderFileList();
|
|
}
|
|
|
|
function renderFileList() {
|
|
fileList.innerHTML = '';
|
|
if (files.length === 0) {
|
|
btnConvert.disabled = true;
|
|
return;
|
|
}
|
|
btnConvert.disabled = false;
|
|
|
|
files.forEach((f, idx) => {
|
|
const item = document.createElement('div');
|
|
item.className = 'file-item';
|
|
item.innerHTML = `
|
|
<div class="file-info">
|
|
<span class="file-name">${f.name}</span>
|
|
<span class="file-status status-pending" id="status-${idx}">等待</span>
|
|
</div>
|
|
<div id="actions-${idx}"></div>
|
|
`;
|
|
fileList.appendChild(item);
|
|
});
|
|
}
|
|
|
|
function setStatus(idx, text, cls) {
|
|
const el = document.getElementById(`status-${idx}`);
|
|
if (el) { el.textContent = text; el.className = 'file-status status-' + cls; }
|
|
}
|
|
|
|
async function convertFile(file, idx) {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('format', formatSelect.value);
|
|
|
|
setStatus(idx, '上传中', 'upload');
|
|
|
|
try {
|
|
const resp = await fetch('/convert', { method: 'POST', body: formData });
|
|
const data = await resp.json();
|
|
|
|
if (!resp.ok) throw new Error(data.detail || '转换失败');
|
|
|
|
setStatus(idx, '完成', 'done');
|
|
const actions = document.getElementById(`actions-${idx}`);
|
|
const a = document.createElement('a');
|
|
a.href = data.download_url;
|
|
a.textContent = '下载';
|
|
a.className = 'btn-download';
|
|
a.download = data.output_name;
|
|
actions.appendChild(a);
|
|
} catch (error) {
|
|
setStatus(idx, '失败: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
// 开始转码
|
|
btnConvert.addEventListener('click', async () => {
|
|
btnConvert.disabled = true;
|
|
btnConvert.textContent = '转码中...';
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
await convertFile(files[i], i);
|
|
}
|
|
|
|
btnConvert.disabled = false;
|
|
btnConvert.textContent = '开始转码';
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|