- app.py: FastAPI 后端,POST /convert 接收文件上传并调用 vipsong.convert_file 转码 GET /download/<filename> 提供下载,GET /health 健康检查 文件名安全过滤保留扩展名,支持多格式输出(mp3/flac/wav/ogg/m4a) - templates/index.html: 纯HTML/CSS/JS单页应用,拖拽上传、格式选择、状态显示、下载链接 - .gitignore: 添加 static/uploads/、static/outputs/、音频输出文件过滤 - requirements.txt: 添加 fastapi、uvicorn、python-multipart 验证 MCP 可用性: - Baota MCP SystemInfo/SiteList/ServiceStatus 全部正常返回 - nginx+mysql+php-fpm-74/82/85 运行中,5个网站正常 - Web API 端对端测试: NCM→MP3/FLAC/OGG 均输出有效音频,错误处理返回 HTTP 400
216 lines
7.5 KiB
HTML
216 lines
7.5 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; }
|
|
|
|
@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 = [];
|
|
|
|
// 点击上传区域选择文件
|
|
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) {
|
|
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>
|