Download index.html from Kicool/cloudyy: direct link, hf CLI and curl.
- Browser
- Download file 41.6 kB
-
https://huggingface.co/spaces/Kicool/cloudyy/resolve/main/index.html
- Command line
-
hf download hf://spaces/Kicool/cloudyy/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Kicool/cloudyy/resolve/main/index.html
41.6 kB
| <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 ; } } | |
| @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> |