Files
Netease_url/templates/index.html
2025-11-05 14:22:18 +08:00

429 lines
29 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>