cloudyy / index.html
xukunCai
Update index.html
2be311a verified
<!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 href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; background: #f8f9fa; color: #333; overflow-x: hidden; line-height: 1.6; }
.navbar { background: #ffffff; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); padding: 15px 0; position: sticky; top: 0; z-index: 100; }
.nav-container { max-width: 1400px; margin: 0 auto; padding: 0 30px; display: flex; align-items: center; justify-content: space-between; }
.logo { display: flex; align-items: center; gap: 12px; font-size: 24px; font-weight: bold; color: #333; }
.logo i { color: #4a90e2; font-size: 28px; }
.search-container { flex: 1; max-width: 600px; margin: 0 40px; position: relative; }
.search-wrapper { display: flex; background: #f5f7fa; border-radius: 25px; overflow: hidden; border: 1px solid #e1e5eb; transition: all 0.3s ease; }
.search-wrapper:focus-within { background: #ffffff; border-color: #4a90e2; box-shadow: 0 0 15px rgba(74, 144, 226, 0.2); }
.search-input { flex: 1; padding: 12px 20px; background: transparent; border: none; color: #333; font-size: 16px; outline: none; }
.search-input::placeholder { color: #adb5bd; }
.source-select { background: #f0f3f7; border: none; color: #495057; padding: 12px 15px; outline: none; cursor: pointer; border-left: 1px solid #e1e5eb; }
.source-select option { background: #ffffff; color: #333; padding: 8px; }
.search-btn { background: #4a90e2; border: none; color: #fff; padding: 12px 20px; cursor: pointer; transition: all 0.3s ease; }
.search-btn:hover { background: #3a7bc8; }
.main-container { max-width: 1200px; margin: 0 auto; padding: 20px; min-height: calc(100vh - 170px); }
.content-panel { background: #ffffff; border-radius: 16px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05); padding: 25px; margin-bottom: 20px; height: 100%; }
.panel-title { font-size: 20px; font-weight: 600; margin-bottom: 20px; color: #333; display: flex; align-items: center; gap: 10px; }
.panel-title i { color: #4a90e2; }
.search-results { max-height: 500px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #ced4da transparent; display: grid; grid-template-columns: repeat(auto-fill, minmax(100%, 1fr)); gap: 12px; padding-right: 10px; }
.search-results::-webkit-scrollbar { width: 6px; }
.search-results::-webkit-scrollbar-track { background: transparent; }
.search-results::-webkit-scrollbar-thumb { background: #ced4da; border-radius: 3px; }
.song-card { display: flex; align-items: flex-start; padding: 12px 15px; border-radius: 12px; cursor: pointer; transition: all 0.3s ease; background: #f8f9fa; border: 1px solid #e9ecef; }
.song-card:hover { background: #ffffff; transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); }
.song-card.active { background: linear-gradient(135deg, rgba(74, 144, 226, 0.1), rgba(74, 144, 226, 0.05)); border: 1px solid rgba(74, 144, 226, 0.3); }
.song-index { width: 32px; height: 32px; border-radius: 50%; background: #e9ecef; display: flex; align-items: center; justify-content: center; margin-right: 12px; font-size: 14px; font-weight: 600; color: #495057; flex-shrink: 0; }
.song-card.active .song-index { background: linear-gradient(135deg, #4a90e2, #3a7bc8); color: #fff; }
.song-info { flex: 1; min-width: 0; padding: 2px 0; }
.song-name { font-weight: 600; margin-bottom: 3px; font-size: 15px; color: #333; white-space: normal; line-height: 1.4; }
.song-artist { color: #6c757d; font-size: 13px; white-space: normal; line-height: 1.4; }
.song-duration { color: #adb5bd; font-size: 13px; margin-left: 10px; white-space: nowrap; align-self: center; }
.song-actions { display: flex; gap: 8px; margin-left: 10px; align-self: center; }
.action-btn { width: 32px; height: 32px; border-radius: 50%; background: #e9ecef; border: none; color: #6c757d; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; font-size: 13px; }
.action-btn:hover { background: #dee2e6; color: #4a90e2; transform: scale(1.1); }
.player-panel { text-align: center; padding: 30px 25px; }
.current-song { margin-bottom: 30px; }
.current-cover-container { position: relative; display: inline-block; margin-bottom: 25px; }
.current-cover { width: 220px; height: 220px; border-radius: 50%; object-fit: cover; box-shadow: 0 15px 35px rgba(74, 144, 226, 0.2); transition: all 0.3s ease; border: 6px solid #f8f9fa; }
.current-cover.playing { animation: rotate 20s linear infinite; }
@keyframes rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.current-info h3 { font-size: 22px; font-weight: 600; margin-bottom: 8px; color: #333; }
.current-info p { color: #6c757d; font-size: 16px; }
.player-controls { display: flex; justify-content: center; align-items: center; gap: 25px; margin-bottom: 30px; }
.control-btn { background: #f1f3f5; border: none; border-radius: 50%; color: #495057; cursor: pointer; transition: all 0.3s ease; display: flex; align-items: center; justify-content: center; }
.control-btn:hover { background: #e9ecef; transform: scale(1.1); }
.control-btn.small { width: 50px; height: 50px; font-size: 18px; }
.play-btn { width: 70px; height: 70px; font-size: 28px; background: linear-gradient(135deg, #4a90e2, #3a7bc8); color: white; box-shadow: 0 8px 25px rgba(74, 144, 226, 0.3); }
.play-btn:hover { background: linear-gradient(135deg, #3a7bc8, #2d6bc1); box-shadow: 0 12px 35px rgba(74, 144, 226, 0.4); }
.progress-container { margin-bottom: 30px; padding: 0 20px; }
.progress-bar { width: 100%; height: 6px; background: #e9ecef; border-radius: 3px; cursor: pointer; margin-bottom: 10px; position: relative; }
.progress-fill { height: 100%; background: linear-gradient(90deg, #4a90e2, #6aa8e6); border-radius: 3px; width: 0%; transition: width 0.1s ease; position: relative; }
.progress-fill::after { content: ''; position: absolute; right: -2px; top: 50%; transform: translateY(-50%); width: 14px; height: 14px; background: #4a90e2; border-radius: 50%; box-shadow: 0 2px 8px rgba(74, 144, 226, 0.5); }
.time-info { display: flex; justify-content: space-between; font-size: 14px; color: #6c757d; }
.volume-container { display: flex; align-items: center; gap: 12px; margin-bottom: 30px; justify-content: center; }
.volume-icon { color: #6c757d; font-size: 18px; }
.volume-slider { width: 150px; height: 4px; background: #e9ecef; border-radius: 2px; outline: none; cursor: pointer; -webkit-appearance: none; }
.volume-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; background: #4a90e2; border-radius: 50%; cursor: pointer; }
.quality-container { display: flex; align-items: center; justify-content: space-between; margin-bottom: 25px; padding: 15px 20px; background: #f8f9fa; border-radius: 12px; border: 1px solid #e9ecef; }
.quality-label { display: flex; align-items: center; gap: 8px; color: #495057; font-size: 14px; }
.quality-select { background: #ffffff; border: 1px solid #ced4da; border-radius: 8px; color: #333; padding: 8px 12px; outline: none; cursor: pointer; font-size: 14px; }
.quality-select option { background: #ffffff; color: #333; padding: 8px; }
.download-container { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 20px; }
.download-btn { display: flex; align-items: center; justify-content: center; gap: 8px; padding: 12px 15px; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 12px; color: #495057; cursor: pointer; transition: all 0.3s ease; font-size: 14px; }
.download-btn:hover:not(:disabled) { background: #eef2f7; border-color: #4a90e2; color: #4a90e2; }
.download-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.lyrics-container { max-height: 500px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #ced4da transparent; padding-right: 10px; position: relative; }
.lyrics-container::-webkit-scrollbar { width: 6px; }
.lyrics-container::-webkit-scrollbar-track { background: transparent; }
.lyrics-container::-webkit-scrollbar-thumb { background: #ced4da; border-radius: 3px; }
.lyric-line { padding: 8px 15px; transition: all 0.3s ease; cursor: pointer; border-radius: 8px; margin-bottom: 6px; color: #6c757d; line-height: 1.6; text-align: center; font-size: 15px; }
.lyric-line:hover { background: #f8f9fa; color: #4a90e2; }
.lyric-line.active { color: #4a90e2; font-weight: 600; background: rgba(74, 144, 226, 0.08); transform: scale(1.02); }
.loading, .error, .empty-state { text-align: center; padding: 60px 20px; color: #6c757d; }
.loading i, .error i, .empty-state i { font-size: 48px; margin-bottom: 15px; display: block; color: #adb5bd; }
.loading i { animation: spin 1s linear infinite; color: #4a90e2; }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.error i { color: #e74c3c; }
.mobile-nav { display: none; position: fixed; bottom: 0; left: 0; width: 100%; background: #ffffff; box-shadow: 0 -2px 15px rgba(0, 0, 0, 0.05); z-index: 99; }
.mobile-nav-items { display: flex; justify-content: space-around; }
.mobile-nav-item { flex: 1; text-align: center; padding: 15px 0; color: #6c757d; text-decoration: none; font-size: 12px; transition: all 0.3s ease; }
.mobile-nav-item.active { color: #4a90e2; }
.mobile-nav-item i { font-size: 20px; margin-bottom: 5px; display: block; }
.view-container { display: block; }
@media (min-width: 1200px) { .desktop-layout { display: grid; grid-template-columns: 350px 450px 350px; gap: 25px; } .mobile-nav { display: none !important; } }
@media (max-width: 1199px) { .mobile-nav { display: block; } .main-container { padding-bottom: 80px; } .desktop-layout { display: block; } .view-container { display: none; } .view-container.active { display: block; } .song-name { font-size: 14px; line-height: 1.4; } .song-artist { font-size: 13px; line-height: 1.4; } .song-card { padding: 10px 12px; gap: 10px; } .song-index { width: 28px; height: 28px; font-size: 13px; } .action-btn { width: 28px; height: 28px; font-size: 12px; } }
@media (max-width: 768px) { .nav-container { flex-direction: column; gap: 15px; padding: 0 15px; } .search-container { margin: 0; max-width: none; width: 100%; } .current-cover { width: 180px; height: 180px; } .player-controls { gap: 15px; } .content-panel { padding: 15px 12px; } .panel-title { font-size: 17px; margin-bottom: 15px; } .search-results { gap: 10px; } }
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #f8f9fa; border-radius: 4px; }
::-webkit-scrollbar-thumb { background: #ced4da; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #adb5bd; }
</style>
</head>
<body>
<nav class="navbar">
<div class="nav-container">
<div class="logo"><i class="fas fa-music"></i><span>云音乐</span></div>
<div class="search-container">
<div class="search-wrapper">
<input type="text" class="search-input" placeholder="搜索音乐、歌手、专辑..." id="searchInput">
<select class="source-select" id="sourceSelect">
<option value="netease">网易云音乐</option>
<option value="tencent">QQ音乐</option>
<option value="kuwo">酷我音乐</option>
<option value="joox">JOOX</option>
<option value="kugou">酷狗音乐</option>
<option value="migu">咪咕音乐</option>
<option value="deezer">Deezer</option>
<option value="spotify">Spotify</option>
<option value="apple">Apple Music</option>
<option value="ytmusic">YouTube Music</option>
<option value="tidal">TIDAL</option>
<option value="qobuz">Qobuz</option>
<option value="ximalaya">喜马拉雅</option>
</select>
<button class="search-btn" onclick="searchMusic()"><i class="fas fa-search"></i></button>
</div>
</div>
</div>
</nav>
<div class="main-container">
<div class="desktop-layout">
<div class="view-container" id="searchView">
<div class="content-panel">
<h2 class="panel-title"><i class="fas fa-search"></i> 搜索结果</h2>
<div class="search-results" id="searchResults">
<div class="empty-state"><i class="fas fa-music"></i><div>在上方搜索框输入关键词开始搜索音乐</div></div>
</div>
</div>
</div>
<div class="view-container" id="playerView">
<div class="content-panel player-panel">
<div class="current-song">
<div class="current-cover-container"><img class="current-cover" id="currentCover" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIwIiBoZWlnaHQ9IjIyMCIgdmlld0JveD0iMCAwIDIyMCAyMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMjAiIGhlaWdodD0iMjIwIiBmaWxsPSIjZmZmZmZmIi8+CjxjaXJjbGUgY3g9IjExMCIgY3k9IjExMCIgcj0iODAiIGZpbGw9IiNmOGY5ZmEiLz4KPHBhdGggZD0iTTEwNSAxNzVWODVIMTE1VjE3NVoiIGZpbGw9IiM0YTkwZTIiLz4KPC9zdmc+" alt="专辑封面"></div>
<div class="current-info"><h3 id="currentTitle">未选择歌曲</h3><p id="currentArtist">请搜索并选择要播放的歌曲</p></div>
</div>
<div class="player-controls">
<button class="control-btn small" onclick="previousSong()"><i class="fas fa-step-backward"></i></button>
<button class="control-btn play-btn" id="playBtn" onclick="togglePlay()"><i class="fas fa-play"></i></button>
<button class="control-btn small" onclick="nextSong()"><i class="fas fa-step-forward"></i></button>
</div>
<div class="progress-container">
<div class="progress-bar" onclick="seekTo(event)"><div class="progress-fill" id="progressFill"></div></div>
<div class="time-info"><span id="currentTime">0:00</span><span id="totalTime">0:00</span></div>
</div>
<div class="quality-container">
<div class="quality-label"><i class="fas fa-music"></i><span>音质</span></div>
<select class="quality-select" id="qualitySelect">
<option value="128">标准 128K</option>
<option value="192">较高 192K</option>
<option value="320" selected>高品质 320K</option>
<option value="740">无损 FLAC</option>
<option value="999">Hi-Res</option>
</select>
</div>
<div class="volume-container">
<i class="fas fa-volume-up volume-icon"></i>
<input type="range" class="volume-slider" id="volumeSlider" min="0" max="100" value="80" onchange="setVolume(this.value)">
</div>
<div class="download-container">
<button class="download-btn" onclick="downloadCurrentSong()" id="downloadSongBtn" disabled><i class="fas fa-download"></i><span>下载音乐</span></button>
<button class="download-btn" onclick="downloadCurrentLyric()" id="downloadLyricBtn" disabled><i class="fas fa-file-text"></i><span>下载歌词</span></button>
</div>
<audio id="audioPlayer" preload="metadata"></audio>
</div>
</div>
<div class="view-container" id="lyricsView">
<div class="content-panel">
<h2 class="panel-title"><i class="fas fa-align-left"></i> 歌词</h2>
<div class="lyrics-container" id="lyricsContainer"><div class="lyric-line">暂无歌词</div></div>
</div>
</div>
</div>
</div>
<div class="mobile-nav">
<div class="mobile-nav-items">
<a href="#searchView" class="mobile-nav-item active" onclick="switchView('searchView', this)"><i class="fas fa-search"></i><span>搜索</span></a>
<a href="#playerView" class="mobile-nav-item" onclick="switchView('playerView', this)"><i class="fas fa-play-circle"></i><span>播放</span></a>
<a href="#lyricsView" class="mobile-nav-item" onclick="switchView('lyricsView', this)"><i class="fas fa-align-left"></i><span>歌词</span></a>
</div>
</div>
<script>
// Dr.Kun: 常量和全局变量
const API_BASE = 'https://music-api.gdstudio.xyz/api.php';
const PLAYER_STATE_KEY = 'cloudMusicPlayerState_v3';
let currentPlaylist = [];
let currentIndex = -1;
let isPlaying = false;
let timeToRestore = 0;
// Dr.Kun: DOM元素缓存
const audioPlayer = document.getElementById('audioPlayer');
const playBtn = document.getElementById('playBtn');
const progressFill = document.getElementById('progressFill');
const currentTimeSpan = document.getElementById('currentTime');
const totalTimeSpan = document.getElementById('totalTime');
const lyricsContainer = document.getElementById('lyricsContainer');
const currentCover = document.getElementById('currentCover');
const qualitySelect = document.getElementById('qualitySelect');
const sourceSelect = document.getElementById('sourceSelect');
const volumeSlider = document.getElementById('volumeSlider');
const searchInput = document.getElementById('searchInput');
function savePlayerState() {
if (currentIndex === -1) return; // 如果没有歌曲,则不保存
const state = {
playlist: currentPlaylist,
index: currentIndex,
volume: audioPlayer.volume,
quality: qualitySelect.value,
source: sourceSelect.value,
currentTime: audioPlayer.currentTime > 1 ? audioPlayer.currentTime : 0
};
try {
localStorage.setItem(PLAYER_STATE_KEY, JSON.stringify(state));
} catch (e) { console.error("保存状态失败:", e); }
}
async function loadPlayerState() {
try {
const savedState = localStorage.getItem(PLAYER_STATE_KEY);
if (!savedState) return;
const state = JSON.parse(savedState);
currentPlaylist = state.playlist || [];
currentIndex = state.index !== undefined ? state.index : -1;
timeToRestore = state.currentTime || 0;
qualitySelect.value = state.quality || '320';
sourceSelect.value = state.source || 'netease';
const volumeValue = (state.volume !== undefined ? state.volume : 0.8) * 100;
volumeSlider.value = volumeValue;
setVolume(volumeValue, false);
if (currentPlaylist.length > 0) {
displaySearchResults(currentPlaylist);
}
if (currentIndex > -1 && currentPlaylist[currentIndex]) {
const song = currentPlaylist[currentIndex];
await updateCurrentSongInfo(song);
updateActiveItem();
lyricsContainer.innerHTML = `<div class="loading"><i class="fas fa-spinner"></i><div>正在加载歌词...</div></div>`;
await loadLyrics(song);
const quality = qualitySelect.value;
const urlResponse = await fetch(`${API_BASE}?types=url&source=${song.source}&id=${song.id}&br=${quality}`);
const urlData = await urlResponse.json();
if(urlData && urlData.url) {
audioPlayer.src = urlData.url;
}
document.getElementById('downloadSongBtn').disabled = false;
document.getElementById('downloadLyricBtn').disabled = false;
}
} catch (e) {
console.error("加载状态失败, 数据已损坏:", e);
localStorage.removeItem(PLAYER_STATE_KEY);
}
}
async function initializeApp() {
if (window.innerWidth >= 1200) {
document.getElementById('searchView').classList.add('active');
document.getElementById('playerView').classList.add('active');
document.getElementById('lyricsView').classList.add('active');
} else {
switchView('searchView', document.querySelectorAll('.mobile-nav-item')[0]);
}
await loadPlayerState();
qualitySelect.onchange = savePlayerState;
sourceSelect.onchange = savePlayerState;
window.addEventListener('beforeunload', savePlayerState);
setInterval(() => {
if (isPlaying) savePlayerState();
}, 5000);
}
function setVolume(value, shouldSave = true) {
audioPlayer.volume = value / 100;
const volumeIcon = document.querySelector('.volume-icon');
if (value == 0) volumeIcon.className = 'fas fa-volume-mute volume-icon';
else if (value < 50) volumeIcon.className = 'fas fa-volume-down volume-icon';
else volumeIcon.className = 'fas fa-volume-up volume-icon';
if (shouldSave) {
savePlayerState();
}
}
async function playSong(index) {
if (index < 0 || index >= currentPlaylist.length) return;
currentIndex = index;
timeToRestore = 0;
const song = currentPlaylist[index];
await updateCurrentSongInfo(song);
updateActiveItem();
try {
showNotification('正在加载音乐...', 'info');
lyricsContainer.innerHTML = `<div class="loading"><i class="fas fa-spinner"></i><div>正在加载歌词...</div></div>`;
await loadLyrics(song);
const quality = qualitySelect.value;
const urlResponse = await fetch(`${API_BASE}?types=url&source=${song.source}&id=${song.id}&br=${quality}`);
const urlData = await urlResponse.json();
if (urlData && urlData.url) {
audioPlayer.src = urlData.url;
audioPlayer.load();
document.getElementById('downloadSongBtn').disabled = false;
document.getElementById('downloadLyricBtn').disabled = false;
const playPromise = audioPlayer.play();
if (playPromise !== undefined) {
playPromise.catch(error => {
showNotification('自动播放失败,请手动点击播放', 'warning');
});
}
} else {
showNotification('无法获取音乐链接', 'error');
}
} catch (error) {
console.error('播放失败:', error);
showNotification('播放失败,请检查网络连接', 'error');
}
}
audioPlayer.addEventListener('loadedmetadata', () => {
totalTimeSpan.textContent = formatTime(audioPlayer.duration);
if (timeToRestore > 0 && timeToRestore < audioPlayer.duration) {
audioPlayer.currentTime = timeToRestore;
timeToRestore = 0;
updateProgress();
showNotification('已恢复上次播放进度', 'info');
}
});
audioPlayer.addEventListener('play', () => { isPlaying = true; updatePlayButton(); currentCover.classList.add('playing'); });
audioPlayer.addEventListener('pause', () => { isPlaying = false; updatePlayButton(); currentCover.classList.remove('playing'); savePlayerState(); });
audioPlayer.addEventListener('ended', nextSong);
setInterval(updateProgress, 500);
searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') searchMusic(); });
document.addEventListener('keydown', (e) => { if (['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName)) return; const keyMap = { ' ': togglePlay, 'ArrowRight': () => { if(audioPlayer.duration) audioPlayer.currentTime = Math.min(audioPlayer.currentTime + 5, audioPlayer.duration); }, 'ArrowLeft': () => { if(audioPlayer.duration) audioPlayer.currentTime = Math.max(audioPlayer.currentTime - 5, 0); }, 'ArrowUp': () => { const v = Math.min(parseInt(volumeSlider.value) + 5, 100); volumeSlider.value = v; setVolume(v); }, 'ArrowDown': () => { const v = Math.max(parseInt(volumeSlider.value) - 5, 0); volumeSlider.value = v; setVolume(v); }, 'n': nextSong, 'N': nextSong, 'p': previousSong, 'P': previousSong }; if (keyMap[e.key]) { e.preventDefault(); keyMap[e.key](); } });
function switchView(viewId, navItem) {
document.querySelectorAll('.view-container').forEach(view => view.classList.remove('active'));
document.getElementById(viewId).classList.add('active');
if (navItem) {
document.querySelectorAll('.mobile-nav-item').forEach(item => item.classList.remove('active'));
navItem.classList.add('active');
}
}
async function searchMusic() {
const keyword = searchInput.value.trim();
const source = sourceSelect.value;
if (!keyword) { showNotification('请输入搜索关键词', 'warning'); return; }
const resultsContainer = document.getElementById('searchResults');
resultsContainer.innerHTML = `<div class="loading"><i class="fas fa-spinner"></i><div>正在搜索音乐...</div></div>`;
try {
const response = await fetch(`${API_BASE}?types=search&source=${source}&name=${encodeURIComponent(keyword)}&count=30`);
const data = await response.json();
if (data && data.length > 0) {
currentPlaylist = data;
currentIndex = -1;
displaySearchResults(data);
savePlayerState();
} else {
resultsContainer.innerHTML = `<div class="error"><i class="fas fa-exclamation-triangle"></i><div>未找到相关歌曲</div></div>`;
}
} catch (error) {
console.error('搜索失败:', error);
resultsContainer.innerHTML = `<div class="error"><i class="fas fa-wifi"></i><div>网络连接失败</div></div>`;
}
}
async function getAlbumCoverUrl(song, size = 300) {
if (!song || !song.pic_id) { return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIwIiBoZWlnaHQ9IjIyMCIgdmlld0JveD0iMCAwIDIyMCAyMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMjAiIGhlaWdodD0iMjIwIiBmaWxsPSIjZmZmZmZmIi8+CjxjaXJjbGUgY3g9IjExMCIgY3k9IjExMCIgcj0iODAiIGZpbGw9IiNmOGY5ZmEiLz4KPHBhdGggZD0iTTEwNSAxNzVWODVIMTE1VjE3NVoiIGZpbGw9IiM0YTkwZTIiLz4KPC9zdmc+'; }
try {
const response = await fetch(`${API_BASE}?types=pic&source=${song.source}&id=${song.pic_id}&size=${size}`);
const data = await response.json();
return (data && data.url) ? data.url : 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIwIiBoZWlnaHQ9IjIyMCIgdmlld0JveD0iMCAwIDIyMCAyMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMjAiIGhlaWdodD0iMjIwIiBmaWxsPSIjZmZmZmZmIi8+CjxjaXJjbGUgY3g9IjExMCIgY3k9IjExMCIgcj0iODAiIGZpbGw9IiNmOGY5ZmEiLz4KPHBhdGggZD0iTTEwNSAxNzVWODVIMTE1VjE3NVoiIGZpbGw9IiM0YTkwZTIiLz4KPC9zdmc+';
} catch (error) {
console.error('获取专辑图失败:', error);
return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIwIiBoZWlnaHQ9IjIyMCIgdmlld0JveD0iMCAwIDIyMCAyMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMjAiIGhlaWdodD0iMjIwIiBmaWxsPSIjZmZmZmZmIi8+CjxjaXJjbGUgY3g9IjExMCIgY3k9IjExMCIgcj0iODAiIGZpbGw9IiNmOGY5ZmEiLz4KPHBhdGggZD0iTTEwNSAxNzVWODVIMTE1VjE3NVoiIGZpbGw9IiM0YTkwZTIiLz4KPC9zdmc+';
}
}
function displaySearchResults(songs) {
const resultsContainer = document.getElementById('searchResults');
resultsContainer.innerHTML = '';
songs.forEach((song, index) => {
const songCard = document.createElement('div');
songCard.className = 'song-card';
songCard.onclick = () => playSong(index);
const artistText = Array.isArray(song.artist) ? song.artist.join(' / ') : song.artist;
songCard.innerHTML = ` <div class="song-index">${(index + 1).toString().padStart(2, '0')}</div> <div class="song-info"> <div class="song-name">${song.name}</div> <div class="song-artist">${artistText} ${song.album ? '· ' + song.album : ''}</div> </div> <div class="song-actions"> <button class="action-btn" onclick="event.stopPropagation(); downloadSong(${index})" title="下载音乐"><i class="fas fa-download"></i></button> <button class="action-btn" onclick="event.stopPropagation(); downloadLyric(${index})" title="下载歌词"><i class="fas fa-file-text"></i></button> </div>`;
resultsContainer.appendChild(songCard);
});
}
async function downloadSong(index) { const song = currentPlaylist[index]; const quality = qualitySelect.value; try { showNotification('正在获取下载链接...', 'info'); const response = await fetch(`${API_BASE}?types=url&source=${song.source}&id=${song.id}&br=${quality}`); const data = await response.json(); if (data && data.url) { const link = document.createElement('a'); link.href = data.url; link.download = `${song.name} - ${Array.isArray(song.artist) ? song.artist.join(', ') : song.artist}.mp3`; link.target = '_blank'; document.body.appendChild(link); link.click(); document.body.removeChild(link); showNotification('开始下载音乐文件', 'success'); } else { showNotification('无法获取下载链接', 'error'); } } catch (error) { console.error('下载失败:', error); showNotification('下载失败,请稍后重试', 'error'); } }
async function downloadLyric(index) { const song = currentPlaylist[index]; try { showNotification('正在获取歌词...', 'info'); const response = await fetch(`${API_BASE}?types=lyric&source=${song.source}&id=${song.lyric_id || song.id}`); const data = await response.json(); if (data && data.lyric) { let lyricContent = `歌曲:${song.name}\r\n歌手:${Array.isArray(song.artist) ? song.artist.join(', ') : song.artist}\r\n专辑:${song.album}\r\n\r\n${data.lyric}`; if (data.tlyric) lyricContent += `\r\n\r\n=== 翻译 ===\r\n${data.tlyric}`; const blob = new Blob([lyricContent], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${song.name} - ${Array.isArray(song.artist) ? song.artist.join(', ') : song.artist}.lrc`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); showNotification('歌词下载完成', 'success'); } else { showNotification('该歌曲暂无歌词', 'warning'); } } catch (error) { console.error('下载歌词失败:', error); showNotification('下载歌词失败,请稍后重试', 'error'); } }
async function downloadCurrentSong() { if (currentIndex === -1) { showNotification('请先选择歌曲', 'warning'); return; } await downloadSong(currentIndex); }
async function downloadCurrentLyric() { if (currentIndex === -1) { showNotification('请先选择歌曲', 'warning'); return; } await downloadLyric(currentIndex); }
async function updateCurrentSongInfo(song) { document.getElementById('currentTitle').textContent = song.name; const artistText = Array.isArray(song.artist) ? song.artist.join(' / ') : song.artist; document.getElementById('currentArtist').textContent = `${artistText}${song.album ? ' · ' + song.album : ''}`; currentCover.src = await getAlbumCoverUrl(song, 500); }
function updateActiveItem() { document.querySelectorAll('.song-card').forEach((item, index) => { item.classList.toggle('active', index === currentIndex); }); }
function updatePlayButton() { playBtn.querySelector('i').className = isPlaying ? 'fas fa-pause' : 'fas fa-play'; }
function togglePlay() { if (!audioPlayer.src) { showNotification('请先选择一首歌曲', 'warning'); return; } if (isPlaying) { audioPlayer.pause(); } else { audioPlayer.play().catch(e => console.error("播放命令被拒绝", e)); } }
function seekTo(event) { if(!audioPlayer.duration) return; const progressBar = event.currentTarget; const rect = progressBar.getBoundingClientRect(); audioPlayer.currentTime = ((event.clientX - rect.left) / rect.width) * audioPlayer.duration; }
async function loadLyrics(song) {
try {
const response = await fetch(`${API_BASE}?types=lyric&source=${song.source}&id=${song.lyric_id || song.id}`);
const data = await response.json();
let lyrics = [];
if (data && data.lyric) {
lyrics = parseLyrics(data.lyric);
if (data.tlyric) {
const tLyrics = parseLyrics(data.tlyric);
mergeLyrics(lyrics, tLyrics);
}
}
displayLyrics(lyrics);
} catch (error) {
console.error('加载歌词失败:', error);
lyricsContainer.innerHTML = '<div class="lyric-line">加载歌词失败</div>';
}
}
function parseLyrics(lyricText) {
const lines = lyricText.split('\n');
const lyrics = [];
const timeRegex = /\[(\d{2}):(\d{2})[.:](\d{2,3})\]/g;
for (const line of lines) {
if (!line.trim()) continue;
let text = line.replace(timeRegex, '').trim();
if (text) {
let match;
timeRegex.lastIndex = 0;
while ((match = timeRegex.exec(line)) !== null) {
const timeInSeconds = parseInt(match[1]) * 60 + parseInt(match[2]) + parseInt(match[3]) / (match[3].length === 3 ? 1000 : 100);
lyrics.push({ time: timeInSeconds, text: text });
}
}
}
return lyrics.sort((a, b) => a.time - b.time);
}
function mergeLyrics(originalLyrics, translatedLyrics) {
const translatedMap = new Map(translatedLyrics.map(l => [Math.round(l.time * 10), l.text]));
originalLyrics.forEach(l => {
const key = Math.round(l.time * 10);
const translatedText = translatedMap.get(key);
if (translatedText) {
l.text += `<br><span style="color: #adb5bd; font-size: 0.9em;">${translatedText}</span>`;
}
});
}
function displayLyrics(lyrics) {
lyricsContainer.innerHTML = lyrics.length === 0 ? '<div class="lyric-line">暂无歌词</div>' : '';
lyrics.forEach(lyric => {
const line = document.createElement('div');
line.className = 'lyric-line';
line.innerHTML = lyric.text;
line.setAttribute('data-time', lyric.time);
line.onclick = () => { if(audioPlayer.duration) audioPlayer.currentTime = lyric.time; };
lyricsContainer.appendChild(line);
});
}
function updateProgress() {
if (audioPlayer.duration) {
progressFill.style.width = `${(audioPlayer.currentTime / audioPlayer.duration) * 100}%`;
currentTimeSpan.textContent = formatTime(audioPlayer.currentTime);
highlightCurrentLyric();
}
}
function formatTime(seconds) {
const min = Math.floor(seconds / 60);
const sec = Math.floor(seconds % 60);
return `${min}:${sec.toString().padStart(2, '0')}`;
}
function highlightCurrentLyric() {
const currentTime = audioPlayer.currentTime;
const lyricLines = document.querySelectorAll('.lyric-line');
if (lyricLines.length === 0 || lyricsContainer.offsetParent === null) return;
let activeIndex = -1;
for (let i = lyricLines.length - 1; i >= 0; i--) {
if (parseFloat(lyricLines[i].getAttribute('data-time')) <= currentTime + 0.2) {
activeIndex = i;
break;
}
}
lyricLines.forEach((line, index) => {
if (line.classList.contains('active') !== (index === activeIndex)) {
line.classList.toggle('active', index === activeIndex);
}
});
if (activeIndex > -1) {
const activeLine = lyricLines[activeIndex];
const scrollPosition = activeLine.offsetTop - lyricsContainer.clientHeight / 2 + activeLine.clientHeight / 2;
lyricsContainer.scrollTo({ top: scrollPosition, behavior: 'smooth' });
}
}
function showNotification(message, type = 'info') {
const existing = document.querySelector('.custom-notification');
if (existing) existing.remove();
const notification = document.createElement('div');
notification.className = `custom-notification notification-${type}`;
const icons = { info: 'info-circle', success: 'check-circle', error: 'exclamation-circle', warning: 'exclamation-triangle' };
notification.innerHTML = `<i class="fas fa-${icons[type]}"></i><span>${message}</span>`;
document.body.appendChild(notification);
Object.assign(notification.style, { position: 'fixed', bottom: '20px', right: '20px', padding: '12px 20px', borderRadius: '8px', boxShadow: '0 4px 12px rgba(0,0,0,0.15)', display: 'flex', alignItems: 'center', gap: '10px', fontSize: '14px', zIndex: '9999', transition: 'all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)', transform: 'translateY(100px)', opacity: '0' });
const colors = { success: { bg: 'rgba(40, 167, 69, 0.9)', color: '#fff' }, error: { bg: 'rgba(220, 53, 69, 0.9)', color: '#fff' }, warning: { bg: 'rgba(255, 193, 7, 0.9)', color: '#212529' }, info: { bg: 'rgba(74, 144, 226, 0.9)', color: '#fff' } };
notification.style.backgroundColor = colors[type].bg;
notification.style.color = colors[type].color;
setTimeout(() => {
notification.style.transform = 'translateY(0)';
notification.style.opacity = '1';
}, 50);
setTimeout(() => {
notification.style.transform = 'translateY(100px)';
notification.style.opacity = '0';
setTimeout(() => notification.remove(), 400);
}, 3000);
}
function previousSong() {
if (currentPlaylist.length === 0) return;
currentIndex = (currentIndex - 1 + currentPlaylist.length) % currentPlaylist.length;
playSong(currentIndex);
}
function nextSong() {
if (currentPlaylist.length === 0) return;
currentIndex = (currentIndex + 1) % currentPlaylist.length;
playSong(currentIndex);
}
initializeApp();
</script>
</body>
</html>