429 lines
29 KiB
HTML
429 lines
29 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>网易云音乐工具箱</title>
|
||
|
||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||
|
||
<script>
|
||
console.log(
|
||
'%c \u26A0 \u505C\u6B62! \u26A0', // ⚠️ 停止! ⚠️
|
||
'color: red; font-size: 32px; font-weight: bold; -webkit-text-stroke: 1px black;'
|
||
);
|
||
console.log(
|
||
'%c\u8FD9\u662F\u6D4F\u89C8\u5666\u7684\u5F00\u53D1\u8005\u529F\u80FD\uFF0C\u4E13\u4F9B\u5F00\u53D1\u4EBA\u5458\u4F7F\u7528\u3002', // "的"
|
||
'font-size: 16px;'
|
||
);
|
||
console.log(
|
||
'%c\u5982\u679C\u6709\u4EBA\u8BA9\u4F60\u5728\u8FD9\u91CC\u590D\u5236\u7C98\u8D34\u4EE3\u7801\uFF0C\u8FD9\u662F\u4E00\u4E2A\u9A97\u5C40\u3002',
|
||
'font-size: 16px; color: #dc3545;'
|
||
);
|
||
console.log(
|
||
'%c\u5728\u6B64\u5904\u6267\u884C\u4EFB\u610F\u4EE3\u7801\u90FD\u53EF\u80FD\u5BFC\u81B4\u4F60\u7684 Cookie \u88AB\u76D7\u3002', // "的"
|
||
'font-size: 16px; color: #dc3545; font-weight: bold;'
|
||
);
|
||
</script>
|
||
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.css">
|
||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.2.1/css/all.min.css">
|
||
|
||
<style>
|
||
:root {
|
||
--primary-color: #ec4141; --primary-hover: #d73535; --bg-color: #f5f5f7;
|
||
--card-bg: #ffffff; --text-color: #343a40; --text-muted-color: #6c757d;
|
||
--border-color: #e9ecef; --shadow-light: rgba(0, 0, 0, 0.04); --shadow-medium: rgba(0, 0, 0, 0.06);
|
||
}
|
||
@keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
|
||
body { background-color: var(--bg-color); color: var(--text-color); font-family: 'PingFang SC', 'Helvetica Neue', 'Microsoft YaHei', sans-serif; -webkit-font-smoothing: antialiased; padding-top: 90px; padding-bottom: 120px; }
|
||
.navbar { background-color: rgba(255,255,255,0.85); backdrop-filter: saturate(180%) blur(10px); padding: 1rem 0; }
|
||
.main-container { max-width: 900px; }
|
||
#login-status-container { display: flex; align-items: center; }
|
||
.login-button { cursor: pointer; font-weight: 500; color: var(--primary-color); transition: all 0.2s ease; }
|
||
.login-button:hover { color: var(--primary-hover); }
|
||
.logged-in-text { color: var(--text-muted-color); margin-right: 1rem; }
|
||
#global-player-container { position: fixed; bottom: 0; left: 0; width: 100%; z-index: 1040; background: var(--card-bg); box-shadow: 0 -4px 20px var(--shadow-medium); }
|
||
#qr-modal-body { text-align: center; min-height: 300px; display: flex; flex-direction: column; align-items: center; justify-content: center; }
|
||
#qr-image { width: 250px; height: 250px; border-radius: 8px; background-color: #f5f5f5; }
|
||
#qr-status-text { font-weight: 500; margin-top: 1rem; min-height: 1.5rem; }
|
||
.hero-section { font-weight: 700; margin-top: 2.5rem; }
|
||
.hero-section h1 { font-weight: 700; }
|
||
.hero-section .lead { color: var(--text-muted-color); max-width: 600px; margin: 1rem auto; }
|
||
.step-title { font-weight: 600; color: var(--text-muted-color); }
|
||
.function-card { background: var(--card-bg); border: 2px solid transparent; border-radius: 12px; padding: 1.5rem; text-align: center; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 4px 12px var(--shadow-light); height: 100%; }
|
||
.function-card:hover { transform: translateY(-5px); box-shadow: 0 8px 25px var(--shadow-medium); }
|
||
.function-card.active { transform: translateY(-5px); box-shadow: 0 8px 25px var(--shadow-medium); border-color: var(--primary-color); }
|
||
#input-container { display: none; animation: fadeIn 0.5s; }
|
||
.btn-primary { background-color: var(--primary-color); border-color: var(--primary-color); border-radius: 8px; font-weight: 500; transition: all 0.2s ease; padding: 0.75rem 1.5rem; }
|
||
#results-area { animation: fadeIn 0.6s ease-out; }
|
||
.accordion-button:not(.collapsed) { color: var(--primary-color); background-color: #fdf5f5; box-shadow: none; }
|
||
#alert-container { position: fixed; top: 90px; right: 20px; z-index: 1055; min-width: 320px; }
|
||
#format-guide { font-size: 0.8rem; color: var(--text-muted-color); display: none; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<nav class="navbar navbar-light fixed-top shadow-sm">
|
||
<div class="container main-container">
|
||
<a class="navbar-brand fw-bold fs-4" href="#">🎵 网易云音乐工具箱</a>
|
||
<div id="login-status-container">
|
||
</div>
|
||
</div>
|
||
</nav>
|
||
|
||
<div class="container main-container">
|
||
<div id="alert-container"></div>
|
||
<div class="hero-section text-center mb-5">
|
||
<h1>一站式音乐解析与下载</h1>
|
||
<p class="lead">无论是单曲、歌单还是专辑,轻松获取高品质音乐资源。</p>
|
||
</div>
|
||
|
||
<h4 class="text-center mb-3 step-title">① 选择音质与设置</h4>
|
||
<div class="card card-body border-0 shadow-sm mb-5">
|
||
<select id="quality-select" class="form-select form-select-lg">
|
||
<option value="lossless" selected>无损 FLAC (推荐)</option> <option value="hires">Hi-Res</option>
|
||
<option value="jymaster">超清母带</option> <option value="exhigh">极高 320k MP3</option>
|
||
<option value="standard">标准 128k MP3</option> <option value="sky">沉浸环绕声</option>
|
||
<option value="jyeffect">高清环绕声</option>
|
||
</select>
|
||
|
||
<div class="mt-3 d-flex justify-content-center">
|
||
<div class="form-check form-switch">
|
||
<input class="form-check-input" type="checkbox" role="switch" id="autoplay-switch" checked>
|
||
<label class="form-check-label" for="autoplay-switch">自动播放解析的单曲</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<h4 class="text-center mb-4 step-title">② 选择功能</h4>
|
||
<div class="row g-4 mb-4">
|
||
<div class="col-md-6 col-lg-3"><div class="function-card" data-action="search" data-placeholder="输入歌曲名、歌手或专辑..." data-button-text="立即搜索"><div class="icon"><i class="fas fa-search"></i></div><h5>歌曲搜索</h5><p>通过关键词发现音乐</p></div></div>
|
||
<div class="col-md-6 col-lg-3"><div class="function-card" data-action="song" data-placeholder="输入歌曲链接或ID..." data-button-text="开始解析"><div class="icon"><i class="fas fa-music"></i></div><h5>单曲解析</h5><p>获取歌曲详细信息</p></div></div>
|
||
<div class="col-md-6 col-lg-3"><div class="function-card" data-action="playlist" data-placeholder="输入歌单链接或ID..." data-button-text="解析歌单"><div class="icon"><i class="fas fa-list-ol"></i></div><h5>歌单解析</h5><p>批量打包下载歌单</p></div></div>
|
||
<div class="col-md-6 col-lg-3"><div class="function-card" data-action="album" data-placeholder="输入专辑链接或ID..." data-button-text="解析专辑"><div class="icon"><i class="fas fa-compact-disc"></i></div><h5>专辑解析</h5><p>解析并打包完整专辑</p></div></div>
|
||
</div>
|
||
|
||
<div id="input-container" class="mb-5" style="display: none;">
|
||
<div class="card card-body border-0 shadow-sm">
|
||
<div class="input-group input-group-lg">
|
||
<input type="text" id="main-input" class="form-control" placeholder="">
|
||
<button class="btn btn-primary" id="main-button" data-action=""></button>
|
||
</div>
|
||
<div id="format-guide" class="mt-2 ps-2">
|
||
<p class="mb-0">支持格式: `music.163.com/song?id=xxx`, `.../playlist?id=xxx` 等各种PC/移动端链接或纯ID。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="results-area" class="mb-5"></div>
|
||
|
||
<div id="quality-guide" class="mb-5">
|
||
<h4 class="text-center mb-4 step-title">音质选择指南</h4>
|
||
<div class="accordion" id="qualityAccordion">
|
||
<div class="accordion-item rounded-3"><h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne"><strong><i class="fas fa-headphones-alt me-3"></i>标准 & 极高音质</strong></button></h2><div id="collapseOne" class="accordion-collapse collapse"><div class="accordion-body"><strong>特点:</strong>有损压缩(MP3),文件体积最小,兼容所有设备。<br><strong>建议:</strong>适合节省流量或存储空间,在嘈杂环境或普通设备上听感差异不大。</div></div></div>
|
||
<div class="accordion-item rounded-3 mt-2"><h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo"><strong><i class="fas fa-star me-3"></i>无损音质 (FLAC)</strong></button></h2><div id="collapseTwo" class="accordion-collapse collapse"><div class="accordion-body"><strong>特点:</strong>音质与CD完全相同,保留了全部声音细节。<br><strong><span class="text-danger fw-bold">推荐:</span></strong>绝大多数场景下的最佳选择,完美平衡了音质与文件大小。</div></div></div>
|
||
<div class="accordion-item rounded-3 mt-2"><h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree"><strong><i class="fas fa-gem me-3"></i>Hi-Res & 超清母带</strong></button></h2><div id="collapseThree" class="accordion-collapse collapse"><div class="accordion-body"><strong>特点:</strong>超越CD音质,达到录音室母带级别。<br><strong>建议:</strong>为音乐发烧友准备。需配合专业播放器(DAC)和高端耳机才能完全发挥其潜力。</div></div></div>
|
||
<div class="accordion-item rounded-3 mt-2"><h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFour"><strong><i class="fas fa-volume-up me-3"></i>环绕声 (空间音频)</strong></button></h2><div id="collapseFour" class="accordion-collapse collapse"><div class="accordion-body"><strong>特点:</strong>通过算法模拟多声道音效,营造出空间感和包围感。<br><strong>建议:</strong>适合搭配耳机使用,尤其在特定曲目或希望获得新奇听感时尝试。</div></div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<footer class="text-center text-muted small mt-5 py-4">
|
||
<p class="mb-1">网易云音乐工具箱 - 仅供学习交流</p>
|
||
<p class="mb-0">All music resources copyright belongs to NetEase, Inc.</p>
|
||
</footer>
|
||
|
||
</div>
|
||
|
||
<div id="global-player-container">
|
||
</div>
|
||
|
||
<div class="modal fade" id="qrLoginModal" tabindex="-1" aria-labelledby="qrLoginModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-dialog-centered">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="qrLoginModalLabel">扫码登录</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
<div class="modal-body" id="qr-modal-body">
|
||
<div class="spinner-border text-danger" role="status" style="width: 3rem; height: 3rem;">
|
||
<span class="visually-hidden">Loading...</span>
|
||
</div>
|
||
<img id="qr-image" src="" alt="QR Code" class="img-fluid rounded mt-3" style="display: none;">
|
||
<p id="qr-status-text" class="text-muted">正在生成二维码...</p>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.0/dist/jquery.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/aplayer@1.10.1/dist/APlayer.min.js"></script>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
|
||
const qualitySelect = document.getElementById('quality-select');
|
||
const inputContainer = document.getElementById('input-container');
|
||
const mainInput = document.getElementById('main-input');
|
||
const mainButton = document.getElementById('main-button');
|
||
const formatGuide = document.getElementById('format-guide');
|
||
const resultsArea = document.getElementById('results-area');
|
||
const alertContainer = document.getElementById('alert-container');
|
||
const loginStatusContainer = document.getElementById('login-status-container');
|
||
const qrLoginModalEl = document.getElementById('qrLoginModal');
|
||
const qrLoginModal = new bootstrap.Modal(qrLoginModalEl);
|
||
const qrImage = document.getElementById('qr-image');
|
||
const qrStatusText = document.getElementById('qr-status-text');
|
||
let globalAPlayer;
|
||
let userCookie = null;
|
||
let progressInterval;
|
||
let qrCheckInterval;
|
||
let autoplayEnabled = true;
|
||
const autoplaySwitch = document.getElementById('autoplay-switch');
|
||
|
||
const getQuality = () => qualitySelect.value;
|
||
const showAlert = (message, type = 'danger') => {
|
||
const alertId = `alert-${Date.now()}`;
|
||
const alertHtml = `<div id="${alertId}" class="alert alert-${type} alert-dismissible fade show" role="alert">${message}<button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>`;
|
||
alertContainer.insertAdjacentHTML('beforeend', alertHtml);
|
||
setTimeout(() => { const el = document.getElementById(alertId); if(el) new bootstrap.Alert(el).close(); }, 5000);
|
||
};
|
||
const showLoading = (button) => { button.dataset.originalHtml = button.innerHTML; button.disabled = true; button.innerHTML = '<span class="spinner-border spinner-border-sm"></span>'; };
|
||
const hideLoading = (button) => { if (button.dataset.originalHtml) { button.innerHTML = button.dataset.originalHtml; button.disabled = false; } };
|
||
function getApiPayload(action, value = null) {
|
||
const payload = { cookie: userCookie || null };
|
||
const quality = getQuality();
|
||
switch (action) {
|
||
case 'search': payload.keyword = value; break;
|
||
case 'song': payload.url = value; payload.level = quality; break;
|
||
case 'playlist': case 'album': payload.id = value; break;
|
||
case 'download_song_zip': case 'download_playlist_zip': case 'download_album_zip': payload.id = value; payload.quality = quality; break;
|
||
}
|
||
return payload;
|
||
}
|
||
|
||
function updateLoginUI() {
|
||
if (userCookie) {
|
||
loginStatusContainer.innerHTML = `<span class="logged-in-text d-none d-md-block">欢迎您, 黑胶会员</span><a id="logout-button" class="login-button text-muted small">退出登录</a>`;
|
||
document.getElementById('logout-button').addEventListener('click', handleLogout);
|
||
} else {
|
||
loginStatusContainer.innerHTML = `<a id="login-button" class="login-button"><i class="fas fa-user-circle me-1"></i><span>登录以获取高音质</span></a>`;
|
||
document.getElementById('login-button').addEventListener('click', handleLoginClick);
|
||
}
|
||
}
|
||
function handleLoginClick() {
|
||
qrLoginModal.show();
|
||
qrImage.style.display = 'none';
|
||
qrStatusText.textContent = '正在生成二维码...';
|
||
qrStatusText.classList.remove('text-danger', 'text-success');
|
||
if (qrCheckInterval) clearInterval(qrCheckInterval);
|
||
fetch('/login/qr/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, })
|
||
.then(res => res.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
qrImage.src = data.data.qr_img_b64;
|
||
qrImage.style.display = 'block';
|
||
qrStatusText.textContent = '请使用网易云音乐App扫码';
|
||
startQRCheckPolling(data.data.qr_key);
|
||
} else {
|
||
qrStatusText.textContent = `生成失败: ${data.message}`;
|
||
qrStatusText.classList.add('text-danger');
|
||
}
|
||
})
|
||
.catch(err => {
|
||
qrStatusText.textContent = `请求错误: ${err.message}`;
|
||
qrStatusText.classList.add('text-danger');
|
||
});
|
||
}
|
||
function startQRCheckPolling(qrKey) {
|
||
qrCheckInterval = setInterval(() => {
|
||
const payload = { qr_key: qrKey };
|
||
fetch('/login/qr/check', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) })
|
||
.then(res => res.json())
|
||
.then(data => {
|
||
if (!data.success) {
|
||
qrStatusText.textContent = `检查失败: ${data.message}`;
|
||
qrStatusText.classList.add('text-danger');
|
||
clearInterval(qrCheckInterval);
|
||
return;
|
||
}
|
||
const status = data.data;
|
||
switch (status.code) {
|
||
case 800: qrStatusText.textContent = '二维码已过期,请关闭重试'; qrStatusText.classList.add('text-danger'); clearInterval(qrCheckInterval); break;
|
||
case 801: break;
|
||
case 802: qrStatusText.textContent = '扫码成功!请在手机上确认登录'; qrStatusText.classList.remove('text-danger'); break;
|
||
case 803:
|
||
qrStatusText.textContent = '登录成功!'; qrStatusText.classList.add('text-success');
|
||
clearInterval(qrCheckInterval);
|
||
userCookie = status.cookie;
|
||
localStorage.setItem('user_cookie', userCookie);
|
||
updateLoginUI();
|
||
showAlert('登录成功!', 'success');
|
||
// [!! 关键修复 !!] 删除了上一版错误的 'Done;'
|
||
setTimeout(() => qrLoginModal.hide(), 1000);
|
||
break;
|
||
default: qrStatusText.textContent = `未知状态: ${status.message}`; qrStatusText.classList.add('text-danger'); clearInterval(qrCheckInterval); break;
|
||
}
|
||
});
|
||
}, 3000);
|
||
qrLoginModalEl.addEventListener('hidden.bs.modal', () => {
|
||
clearInterval(qrCheckInterval);
|
||
}, { once: true });
|
||
}
|
||
function handleLogout() {
|
||
userCookie = null;
|
||
localStorage.removeItem('user_cookie');
|
||
updateLoginUI();
|
||
showAlert('已退出登录', 'info');
|
||
}
|
||
|
||
function handleCardClick(card) {
|
||
if (card.classList.contains('active')) return;
|
||
document.querySelectorAll('.function-card').forEach(c => c.classList.remove('active'));
|
||
card.classList.add('active');
|
||
mainInput.placeholder = card.dataset.placeholder;
|
||
mainButton.innerHTML = card.dataset.buttonText;
|
||
mainButton.dataset.action = card.dataset.action;
|
||
formatGuide.style.display = (card.dataset.action === 'search') ? 'none' : 'block';
|
||
if (inputContainer.style.display === 'none') $(inputContainer).slideDown();
|
||
mainInput.focus();
|
||
}
|
||
function handleMainButtonClick() {
|
||
const action = mainButton.dataset.action;
|
||
const value = mainInput.value.trim();
|
||
if (!value) { showAlert('请输入内容!', 'warning'); return; }
|
||
const endpoints = { search: '/search', song: '/song', playlist: '/playlist', album: '/album' };
|
||
const payload = getApiPayload(action, value);
|
||
showLoading(mainButton);
|
||
fetch(endpoints[action], { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) })
|
||
.then(res => res.json()).then(data => {
|
||
if (data.success) {
|
||
const renderers = { search: renderSearchResults, song: renderSongDetails, playlist: d => renderCollection(d, 'playlist'), album: d => renderCollection(d, 'album') };
|
||
renderers[action](data.data);
|
||
} else { showAlert(data.message || '操作失败'); }
|
||
}).catch(err => showAlert(`请求错误: ${err.message}`)).finally(() => hideLoading(mainButton));
|
||
}
|
||
function handleDownloadClick(button) {
|
||
const isSingle = button.classList.contains('download-song-zip');
|
||
const type = button.dataset.type;
|
||
const endpointMap = { single: '/download_song_zip', playlist: '/download_playlist_zip', album: '/download_album_zip' };
|
||
const payloadActionMap = { single: 'download_song_zip', playlist: 'download_playlist_zip', album: 'download_album_zip' };
|
||
const endpointKey = isSingle ? 'single' : type;
|
||
const endpoint = endpointMap[endpointKey];
|
||
const payloadAction = payloadActionMap[endpointKey];
|
||
const payload = getApiPayload(payloadAction, button.dataset.id);
|
||
showLoading(button);
|
||
const statusSpan = button.nextElementSibling.matches('.zip-status') ? button.nextElementSibling : null;
|
||
if(statusSpan) {
|
||
const total = button.dataset.total || 1;
|
||
let current = 0; const messages = ["获取信息...", "下载文件...", "嵌入封面...", "即将完成..."]; let msgIndex = 0;
|
||
statusSpan.style.display = 'inline';
|
||
progressInterval = setInterval(() => {
|
||
current++; if (current > total) current = total;
|
||
if (total > 1 && current > 0 && current % Math.ceil(total / (messages.length-1)) === 0) msgIndex = Math.min(msgIndex + 1, messages.length - 1);
|
||
statusSpan.textContent = `${messages[msgIndex]} (${current}/${total})`;
|
||
if(current >= total) clearInterval(progressInterval);
|
||
}, 1800);
|
||
}
|
||
fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) })
|
||
.then(res => {
|
||
clearInterval(progressInterval);
|
||
if (!res.ok) return res.json().then(err => { throw new Error(err.message) });
|
||
const disposition = res.headers.get('content-disposition');
|
||
let filename = "download.zip";
|
||
if (disposition) {
|
||
const utf8Match = disposition.match(/filename\*=UTF-8''([\w%\-\.\s]+)/i);
|
||
filename = utf8Match ? decodeURIComponent(utf8Match[1]) : 'download.zip';
|
||
}
|
||
return res.blob().then(blob => ({ blob, filename }));
|
||
}).then(({ blob, filename }) => {
|
||
const link = document.createElement('a');
|
||
link.href = URL.createObjectURL(blob); link.download = filename;
|
||
document.body.appendChild(link); link.click(); document.body.removeChild(link);
|
||
showAlert('ZIP文件已开始下载!', 'success');
|
||
}).catch(err => showAlert(`下载失败: ${err.message}`)).finally(() => {
|
||
clearInterval(progressInterval);
|
||
if(statusSpan) statusSpan.style.display = 'none';
|
||
hideLoading(button);
|
||
});
|
||
}
|
||
|
||
const renderSearchResults = data => {
|
||
let itemsHtml = data.map(song => `<li class="list-group-item d-flex justify-content-between align-items-center"><div><img src="${song.picUrl}?param=40y40" class="rounded me-3"><strong>${song.name}</strong><small class="text-muted ms-2">${song.artists}</small></div><div><button class="btn btn-sm btn-outline-primary js-parse-from-list" data-id="${song.id}"><i class="fa-solid fa-magnifying-glass-chart me-1"></i>解析</button></div></li>`).join('');
|
||
resultsArea.innerHTML = `<div class="card mt-4"><div class="card-header fw-bold">搜索结果</div><ul class="list-group list-group-flush">${itemsHtml}</ul></div>`;
|
||
};
|
||
const renderSongDetails = data => {
|
||
const qualityText = data.level || '文件';
|
||
resultsArea.innerHTML = `<div class="card mt-4"><div class="card-body"><div class="row"><div class="col-md-3 text-center"><img src="${data.pic}?param=200y200" class="img-fluid rounded shadow-sm"></div><div class="col-md-9"><h3>${data.name}</h3><p class="text-muted mb-2"><strong>歌手:</strong> ${data.ar_name}</p><p><span class="badge bg-success">${data.level}</span> <span class="badge bg-info text-dark">${data.size}</span></p><div class="lyric-box mb-3" style="max-height: 150px; overflow-y: auto;">${data.lyric ? data.lyric.replace(/\n/g, '<br>') : '暂无歌词'}</div><div><button class="btn btn-primary download-song-zip" data-id="${data.id}"><i class="fa-solid fa-file-zipper me-2"></i>打包下载 (${qualityText})</button><span class="zip-status ms-2 text-muted" style="display:none;"></span></div></div></div></div></div>`;
|
||
if (data.url && globalAPlayer) {
|
||
globalAPlayer.list.clear();
|
||
globalAPlayer.list.add({
|
||
name: data.name,
|
||
artist: data.ar_name,
|
||
url: data.url,
|
||
cover: data.pic,
|
||
lrc: data.lyric || '[00:00.00] 暂无歌词'
|
||
});
|
||
if (autoplayEnabled) {
|
||
globalAPlayer.play();
|
||
}
|
||
$('html, body').animate({ scrollTop: $(document).height() }, 500);
|
||
}
|
||
};
|
||
const renderCollection = (data, type) => {
|
||
const collection = data.playlist || data.album;
|
||
const tracks = collection.tracks || collection.songs;
|
||
const creator = data.playlist ? `<p class="card-text"><small class="text-muted">创建者: ${collection.creator}</small></p>` : `<p class="card-text"><small class="text-muted">艺术家: ${collection.artist}</small></p>`;
|
||
let itemsHtml = tracks.map((song, i) => `<li class="list-group-item d-flex justify-content-between align-items-center"><div><span class="text-muted me-3" style="width:25px;display:inline-block;">${i+1}</span><strong>${song.name}</strong><small class="text-muted ms-2">${song.artists}</small></div><button class="btn btn-sm btn-outline-primary js-parse-from-list" data-id="${song.id}"><i class="fa-solid fa-magnifying-glass-chart"></i></button></li>`).join('');
|
||
resultsArea.innerHTML = `<div class="card mt-4"><div class="card-body"><div class="row align-items-center"><div class="col-md-2 text-center"><img src="${collection.coverImgUrl}?param=150y150" class="img-fluid rounded"></div><div class="col-md-10"><h4 class="card-title">${collection.name}</h4>${creator}<div><button class="btn btn-primary download-collection-zip mt-2" data-id="${collection.id}" data-type="${type}" data-total="${tracks.length}"><i class="fa-solid fa-file-zipper me-2"></i>打包下载 (${tracks.length}首)</button><span class="zip-status ms-2 text-muted" style="display:none;"></span></div></div></div></div><ul class="list-group list-group-flush">${itemsHtml}</ul></div>`;
|
||
};
|
||
|
||
|
||
globalAPlayer = new APlayer({
|
||
container: document.getElementById('global-player-container'),
|
||
fixed: false,
|
||
lrcType: 2,
|
||
audio: []
|
||
});
|
||
|
||
const savedAutoplay = localStorage.getItem('autoplay');
|
||
if (savedAutoplay !== null) {
|
||
autoplayEnabled = (savedAutoplay === 'true');
|
||
}
|
||
autoplaySwitch.checked = autoplayEnabled;
|
||
|
||
autoplaySwitch.addEventListener('change', (e) => {
|
||
autoplayEnabled = e.target.checked;
|
||
localStorage.setItem('autoplay', autoplayEnabled);
|
||
showAlert(`自动播放已 ${autoplayEnabled ? '开启' : '关闭'}`, 'info');
|
||
});
|
||
|
||
userCookie = localStorage.getItem('user_cookie');
|
||
updateLoginUI();
|
||
|
||
document.body.addEventListener('click', function (e) {
|
||
const card = e.target.closest('.function-card');
|
||
if (card) { handleCardClick(card); return; }
|
||
if (e.target.id === 'main-button') { handleMainButtonClick(); return; }
|
||
const parseBtn = e.target.closest('.js-parse-from-list');
|
||
if (parseBtn) {
|
||
const songId = parseBtn.dataset.id;
|
||
const songCard = document.querySelector('.function-card[data-action="song"]');
|
||
if (!songCard.classList.contains('active')) songCard.click();
|
||
mainInput.value = songId;
|
||
mainButton.click();
|
||
$('html, body').animate({ scrollTop: $(resultsArea).offset().top - 120 }, 500);
|
||
return;
|
||
}
|
||
const downloadBtn = e.target.closest('.download-song-zip, .download-collection-zip');
|
||
if (downloadBtn) { handleDownloadClick(downloadBtn); return; }
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html> |