|
|
<!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> |
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
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> |