xukunCai commited on
Commit
2be311a
·
verified ·
1 Parent(s): 14ddbbc

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +216 -50
index.html CHANGED
@@ -6,7 +6,6 @@
6
  <title>云音乐 - 在线音乐播放器</title>
7
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
8
  <style>
9
- /* CSS样式部分保持不变,此处省略以保持简洁 */
10
  * { margin: 0; padding: 0; box-sizing: border-box; }
11
  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; }
12
  .navbar { background: #ffffff; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); padding: 15px 0; position: sticky; top: 0; z-index: 100; }
@@ -101,7 +100,6 @@
101
  </style>
102
  </head>
103
  <body>
104
- <!-- HTML结构部分保持不变 -->
105
  <nav class="navbar">
106
  <div class="nav-container">
107
  <div class="logo"><i class="fas fa-music"></i><span>云音乐</span></div>
@@ -193,11 +191,11 @@
193
  <script>
194
  // Dr.Kun: 常量和全局变量
195
  const API_BASE = 'https://music-api.gdstudio.xyz/api.php';
196
- const PLAYER_STATE_KEY = 'cloudMusicPlayerState_v2'; // Dr.Kun: 升级版本号以避免旧数据冲突
197
  let currentPlaylist = [];
198
  let currentIndex = -1;
199
  let isPlaying = false;
200
- let timeToRestore = 0; // Dr.Kun: 核心升级 - 用于暂存需要恢复的播放时间
201
 
202
  // Dr.Kun: DOM元素缓存
203
  const audioPlayer = document.getElementById('audioPlayer');
@@ -212,24 +210,21 @@
212
  const volumeSlider = document.getElementById('volumeSlider');
213
  const searchInput = document.getElementById('searchInput');
214
 
215
- // Dr.Kun: 核心升级 - 保存播放器状态(现在包含播放进度)
216
  function savePlayerState() {
 
217
  const state = {
218
  playlist: currentPlaylist,
219
  index: currentIndex,
220
  volume: audioPlayer.volume,
221
  quality: qualitySelect.value,
222
  source: sourceSelect.value,
223
- currentTime: audioPlayer.currentTime > 1 ? audioPlayer.currentTime : 0 // Dr.Kun: 只在播放超过1秒时保存进度,避免存入无效的0
224
  };
225
  try {
226
  localStorage.setItem(PLAYER_STATE_KEY, JSON.stringify(state));
227
- } catch (e) {
228
- console.error("保存状态失败:", e);
229
- }
230
  }
231
 
232
- // Dr.Kun: 核心升级 - 从localStorage加载播放器状态
233
  async function loadPlayerState() {
234
  try {
235
  const savedState = localStorage.getItem(PLAYER_STATE_KEY);
@@ -238,15 +233,15 @@
238
  const state = JSON.parse(savedState);
239
 
240
  currentPlaylist = state.playlist || [];
241
- currentIndex = state.index || -1;
242
- timeToRestore = state.currentTime || 0; // Dr.Kun: 读取要恢复的时间
243
 
244
  qualitySelect.value = state.quality || '320';
245
  sourceSelect.value = state.source || 'netease';
246
 
247
  const volumeValue = (state.volume !== undefined ? state.volume : 0.8) * 100;
248
  volumeSlider.value = volumeValue;
249
- setVolume(volumeValue, false); // Dr.Kun: 初始化时不保存状态,避免循环
250
 
251
  if (currentPlaylist.length > 0) {
252
  displaySearchResults(currentPlaylist);
@@ -254,10 +249,12 @@
254
 
255
  if (currentIndex > -1 && currentPlaylist[currentIndex]) {
256
  const song = currentPlaylist[currentIndex];
257
- // Dr.Kun: 只更新UI,不获取URL,等待用户操作或后续逻辑
258
  await updateCurrentSongInfo(song);
259
  updateActiveItem();
260
- // Dr.Kun: 关键一步 - 设置音频源,以便后续恢复进度
 
 
 
261
  const quality = qualitySelect.value;
262
  const urlResponse = await fetch(`${API_BASE}?types=url&source=${song.source}&id=${song.id}&br=${quality}`);
263
  const urlData = await urlResponse.json();
@@ -274,7 +271,6 @@
274
  }
275
  }
276
 
277
- // Dr.Kun: 页面加载时执行的初始化函数
278
  async function initializeApp() {
279
  if (window.innerWidth >= 1200) {
280
  document.getElementById('searchView').classList.add('active');
@@ -286,19 +282,15 @@
286
 
287
  await loadPlayerState();
288
 
289
- // Dr.Kun: 监听器
290
  qualitySelect.onchange = savePlayerState;
291
  sourceSelect.onchange = savePlayerState;
292
-
293
- // Dr.Kun: 核心升级 - 在页面卸载前做最后一次保存
294
  window.addEventListener('beforeunload', savePlayerState);
295
- // Dr.Kun: 核心升级 - 定期保存进度
296
- setInterval(savePlayerState, 5000);
 
297
  }
298
 
299
- // ... (其他函数基本不变,除了setVolume增加一个参数) ...
300
-
301
- function setVolume(value, shouldSave = true) { // Dr.Kun: 增加一个参数判断是否需要保存
302
  audioPlayer.volume = value / 100;
303
  const volumeIcon = document.querySelector('.volume-icon');
304
  if (value == 0) volumeIcon.className = 'fas fa-volume-mute volume-icon';
@@ -312,13 +304,16 @@
312
  async function playSong(index) {
313
  if (index < 0 || index >= currentPlaylist.length) return;
314
  currentIndex = index;
315
- timeToRestore = 0; // Dr.Kun: 用户主动切歌,不再需要恢复旧进度
316
  const song = currentPlaylist[index];
317
  await updateCurrentSongInfo(song);
318
  updateActiveItem();
319
- // ... (playSong的后续逻辑不变)
320
  try {
321
  showNotification('正在加载音乐...', 'info');
 
 
 
322
  const quality = qualitySelect.value;
323
  const urlResponse = await fetch(`${API_BASE}?types=url&source=${song.source}&id=${song.id}&br=${quality}`);
324
  const urlData = await urlResponse.json();
@@ -326,15 +321,12 @@
326
  if (urlData && urlData.url) {
327
  audioPlayer.src = urlData.url;
328
  audioPlayer.load();
329
- loadLyrics(song);
330
  document.getElementById('downloadSongBtn').disabled = false;
331
  document.getElementById('downloadLyricBtn').disabled = false;
332
 
333
  const playPromise = audioPlayer.play();
334
  if (playPromise !== undefined) {
335
- playPromise.then(() => {
336
- showNotification(`开始播放: ${song.name}`, 'success');
337
- }).catch(error => {
338
  showNotification('自动播放失败,请手动点击播放', 'warning');
339
  });
340
  }
@@ -347,28 +339,80 @@
347
  }
348
  }
349
 
350
- // Dr.Kun: 核心升级 - 在元数据加载后恢复播放进度
351
  audioPlayer.addEventListener('loadedmetadata', () => {
352
  totalTimeSpan.textContent = formatTime(audioPlayer.duration);
353
  if (timeToRestore > 0 && timeToRestore < audioPlayer.duration) {
354
  audioPlayer.currentTime = timeToRestore;
355
- timeToRestore = 0; // Dr.Kun: 恢复后立即清零,避免影响下次播放
356
- updateProgress(); // 立即更新一次进度条显示
357
  showNotification('已恢复上次播放进度', 'info');
358
  }
359
  });
360
 
361
- // Dr.Kun: 保持其他事件监听和函数不变
362
  audioPlayer.addEventListener('play', () => { isPlaying = true; updatePlayButton(); currentCover.classList.add('playing'); });
363
- audioPlayer.addEventListener('pause', () => { isPlaying = false; updatePlayButton(); currentCover.classList.remove('playing'); savePlayerState(); }); // Dr.Kun: 暂停时也保存一下
364
  audioPlayer.addEventListener('ended', nextSong);
365
  setInterval(updateProgress, 500);
366
  searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') searchMusic(); });
367
  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](); } });
368
- 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'); } }
369
- 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>`; } }
370
- 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+'; } }
371
- 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); }); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  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'); } }
373
  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'); } }
374
  async function downloadCurrentSong() { if (currentIndex === -1) { showNotification('请先选择歌曲', 'warning'); return; } await downloadSong(currentIndex); }
@@ -378,18 +422,140 @@
378
  function updatePlayButton() { playBtn.querySelector('i').className = isPlaying ? 'fas fa-pause' : 'fas fa-play'; }
379
  function togglePlay() { if (!audioPlayer.src) { showNotification('请先选择一首歌曲', 'warning'); return; } if (isPlaying) { audioPlayer.pause(); } else { audioPlayer.play().catch(e => console.error("播放命令被拒绝", e)); } }
380
  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; }
381
- 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>'; } }
382
- 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); }
383
- 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>`; } }); }
384
- 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); }); }
385
- function updateProgress() { if (audioPlayer.duration) { progressFill.style.width = `${(audioPlayer.currentTime / audioPlayer.duration) * 100}%`; currentTimeSpan.textContent = formatTime(audioPlayer.currentTime); highlightCurrentLyric(); } }
386
- function formatTime(seconds) { const min = Math.floor(seconds / 60); const sec = Math.floor(seconds % 60); return `${min}:${sec.toString().padStart(2, '0')}`; }
387
- 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' }); } }
388
- 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); }
389
- function previousSong() { if (currentPlaylist.length === 0) return; currentIndex = (currentIndex - 1 + currentPlaylist.length) % currentPlaylist.length; playSong(currentIndex); }
390
- function nextSong() { if (currentPlaylist.length === 0) return; currentIndex = (currentIndex + 1) % currentPlaylist.length; playSong(currentIndex); }
391
 
392
- // Dr.Kun: 启动应用
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  initializeApp();
394
  </script>
395
  </body>
 
6
  <title>云音乐 - 在线音乐播放器</title>
7
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
8
  <style>
 
9
  * { margin: 0; padding: 0; box-sizing: border-box; }
10
  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; }
11
  .navbar { background: #ffffff; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); padding: 15px 0; position: sticky; top: 0; z-index: 100; }
 
100
  </style>
101
  </head>
102
  <body>
 
103
  <nav class="navbar">
104
  <div class="nav-container">
105
  <div class="logo"><i class="fas fa-music"></i><span>云音乐</span></div>
 
191
  <script>
192
  // Dr.Kun: 常量和全局变量
193
  const API_BASE = 'https://music-api.gdstudio.xyz/api.php';
194
+ const PLAYER_STATE_KEY = 'cloudMusicPlayerState_v3';
195
  let currentPlaylist = [];
196
  let currentIndex = -1;
197
  let isPlaying = false;
198
+ let timeToRestore = 0;
199
 
200
  // Dr.Kun: DOM元素缓存
201
  const audioPlayer = document.getElementById('audioPlayer');
 
210
  const volumeSlider = document.getElementById('volumeSlider');
211
  const searchInput = document.getElementById('searchInput');
212
 
 
213
  function savePlayerState() {
214
+ if (currentIndex === -1) return; // 如果没有歌曲,则不保存
215
  const state = {
216
  playlist: currentPlaylist,
217
  index: currentIndex,
218
  volume: audioPlayer.volume,
219
  quality: qualitySelect.value,
220
  source: sourceSelect.value,
221
+ currentTime: audioPlayer.currentTime > 1 ? audioPlayer.currentTime : 0
222
  };
223
  try {
224
  localStorage.setItem(PLAYER_STATE_KEY, JSON.stringify(state));
225
+ } catch (e) { console.error("保存状态失败:", e); }
 
 
226
  }
227
 
 
228
  async function loadPlayerState() {
229
  try {
230
  const savedState = localStorage.getItem(PLAYER_STATE_KEY);
 
233
  const state = JSON.parse(savedState);
234
 
235
  currentPlaylist = state.playlist || [];
236
+ currentIndex = state.index !== undefined ? state.index : -1;
237
+ timeToRestore = state.currentTime || 0;
238
 
239
  qualitySelect.value = state.quality || '320';
240
  sourceSelect.value = state.source || 'netease';
241
 
242
  const volumeValue = (state.volume !== undefined ? state.volume : 0.8) * 100;
243
  volumeSlider.value = volumeValue;
244
+ setVolume(volumeValue, false);
245
 
246
  if (currentPlaylist.length > 0) {
247
  displaySearchResults(currentPlaylist);
 
249
 
250
  if (currentIndex > -1 && currentPlaylist[currentIndex]) {
251
  const song = currentPlaylist[currentIndex];
 
252
  await updateCurrentSongInfo(song);
253
  updateActiveItem();
254
+
255
+ lyricsContainer.innerHTML = `<div class="loading"><i class="fas fa-spinner"></i><div>正在加载歌词...</div></div>`;
256
+ await loadLyrics(song);
257
+
258
  const quality = qualitySelect.value;
259
  const urlResponse = await fetch(`${API_BASE}?types=url&source=${song.source}&id=${song.id}&br=${quality}`);
260
  const urlData = await urlResponse.json();
 
271
  }
272
  }
273
 
 
274
  async function initializeApp() {
275
  if (window.innerWidth >= 1200) {
276
  document.getElementById('searchView').classList.add('active');
 
282
 
283
  await loadPlayerState();
284
 
 
285
  qualitySelect.onchange = savePlayerState;
286
  sourceSelect.onchange = savePlayerState;
 
 
287
  window.addEventListener('beforeunload', savePlayerState);
288
+ setInterval(() => {
289
+ if (isPlaying) savePlayerState();
290
+ }, 5000);
291
  }
292
 
293
+ function setVolume(value, shouldSave = true) {
 
 
294
  audioPlayer.volume = value / 100;
295
  const volumeIcon = document.querySelector('.volume-icon');
296
  if (value == 0) volumeIcon.className = 'fas fa-volume-mute volume-icon';
 
304
  async function playSong(index) {
305
  if (index < 0 || index >= currentPlaylist.length) return;
306
  currentIndex = index;
307
+ timeToRestore = 0;
308
  const song = currentPlaylist[index];
309
  await updateCurrentSongInfo(song);
310
  updateActiveItem();
311
+
312
  try {
313
  showNotification('正在加载音乐...', 'info');
314
+ lyricsContainer.innerHTML = `<div class="loading"><i class="fas fa-spinner"></i><div>正在加载歌词...</div></div>`;
315
+ await loadLyrics(song);
316
+
317
  const quality = qualitySelect.value;
318
  const urlResponse = await fetch(`${API_BASE}?types=url&source=${song.source}&id=${song.id}&br=${quality}`);
319
  const urlData = await urlResponse.json();
 
321
  if (urlData && urlData.url) {
322
  audioPlayer.src = urlData.url;
323
  audioPlayer.load();
 
324
  document.getElementById('downloadSongBtn').disabled = false;
325
  document.getElementById('downloadLyricBtn').disabled = false;
326
 
327
  const playPromise = audioPlayer.play();
328
  if (playPromise !== undefined) {
329
+ playPromise.catch(error => {
 
 
330
  showNotification('自动播放失败,请手动点击播放', 'warning');
331
  });
332
  }
 
339
  }
340
  }
341
 
 
342
  audioPlayer.addEventListener('loadedmetadata', () => {
343
  totalTimeSpan.textContent = formatTime(audioPlayer.duration);
344
  if (timeToRestore > 0 && timeToRestore < audioPlayer.duration) {
345
  audioPlayer.currentTime = timeToRestore;
346
+ timeToRestore = 0;
347
+ updateProgress();
348
  showNotification('已恢复上次播放进度', 'info');
349
  }
350
  });
351
 
 
352
  audioPlayer.addEventListener('play', () => { isPlaying = true; updatePlayButton(); currentCover.classList.add('playing'); });
353
+ audioPlayer.addEventListener('pause', () => { isPlaying = false; updatePlayButton(); currentCover.classList.remove('playing'); savePlayerState(); });
354
  audioPlayer.addEventListener('ended', nextSong);
355
  setInterval(updateProgress, 500);
356
  searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') searchMusic(); });
357
  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](); } });
358
+
359
+ function switchView(viewId, navItem) {
360
+ document.querySelectorAll('.view-container').forEach(view => view.classList.remove('active'));
361
+ document.getElementById(viewId).classList.add('active');
362
+ if (navItem) {
363
+ document.querySelectorAll('.mobile-nav-item').forEach(item => item.classList.remove('active'));
364
+ navItem.classList.add('active');
365
+ }
366
+ }
367
+
368
+ async function searchMusic() {
369
+ const keyword = searchInput.value.trim();
370
+ const source = sourceSelect.value;
371
+ if (!keyword) { showNotification('请输入搜索关键词', 'warning'); return; }
372
+ const resultsContainer = document.getElementById('searchResults');
373
+ resultsContainer.innerHTML = `<div class="loading"><i class="fas fa-spinner"></i><div>正在搜索音乐...</div></div>`;
374
+ try {
375
+ const response = await fetch(`${API_BASE}?types=search&source=${source}&name=${encodeURIComponent(keyword)}&count=30`);
376
+ const data = await response.json();
377
+ if (data && data.length > 0) {
378
+ currentPlaylist = data;
379
+ currentIndex = -1;
380
+ displaySearchResults(data);
381
+ savePlayerState();
382
+ } else {
383
+ resultsContainer.innerHTML = `<div class="error"><i class="fas fa-exclamation-triangle"></i><div>未找到相关歌曲</div></div>`;
384
+ }
385
+ } catch (error) {
386
+ console.error('搜索失败:', error);
387
+ resultsContainer.innerHTML = `<div class="error"><i class="fas fa-wifi"></i><div>网络连接失败</div></div>`;
388
+ }
389
+ }
390
+
391
+ async function getAlbumCoverUrl(song, size = 300) {
392
+ if (!song || !song.pic_id) { return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIwIiBoZWlnaHQ9IjIyMCIgdmlld0JveD0iMCAwIDIyMCAyMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMjAiIGhlaWdodD0iMjIwIiBmaWxsPSIjZmZmZmZmIi8+CjxjaXJjbGUgY3g9IjExMCIgY3k9IjExMCIgcj0iODAiIGZpbGw9IiNmOGY5ZmEiLz4KPHBhdGggZD0iTTEwNSAxNzVWODVIMTE1VjE3NVoiIGZpbGw9IiM0YTkwZTIiLz4KPC9zdmc+'; }
393
+ try {
394
+ const response = await fetch(`${API_BASE}?types=pic&source=${song.source}&id=${song.pic_id}&size=${size}`);
395
+ const data = await response.json();
396
+ return (data && data.url) ? data.url : 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIwIiBoZWlnaHQ9IjIyMCIgdmlld0JveD0iMCAwIDIyMCAyMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMjAiIGhlaWdodD0iMjIwIiBmaWxsPSIjZmZmZmZmIi8+CjxjaXJjbGUgY3g9IjExMCIgY3k9IjExMCIgcj0iODAiIGZpbGw9IiNmOGY5ZmEiLz4KPHBhdGggZD0iTTEwNSAxNzVWODVIMTE1VjE3NVoiIGZpbGw9IiM0YTkwZTIiLz4KPC9zdmc+';
397
+ } catch (error) {
398
+ console.error('获取专辑图失败:', error);
399
+ return 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjIwIiBoZWlnaHQ9IjIyMCIgdmlld0JveD0iMCAwIDIyMCAyMjAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIyMjAiIGhlaWdodD0iMjIwIiBmaWxsPSIjZmZmZmZmIi8+CjxjaXJjbGUgY3g9IjExMCIgY3k9IjExMCIgcj0iODAiIGZpbGw9IiNmOGY5ZmEiLz4KPHBhdGggZD0iTTEwNSAxNzVWODVIMTE1VjE3NVoiIGZpbGw9IiM0YTkwZTIiLz4KPC9zdmc+';
400
+ }
401
+ }
402
+
403
+ function displaySearchResults(songs) {
404
+ const resultsContainer = document.getElementById('searchResults');
405
+ resultsContainer.innerHTML = '';
406
+ songs.forEach((song, index) => {
407
+ const songCard = document.createElement('div');
408
+ songCard.className = 'song-card';
409
+ songCard.onclick = () => playSong(index);
410
+ const artistText = Array.isArray(song.artist) ? song.artist.join(' / ') : song.artist;
411
+ 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>`;
412
+ resultsContainer.appendChild(songCard);
413
+ });
414
+ }
415
+
416
  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'); } }
417
  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'); } }
418
  async function downloadCurrentSong() { if (currentIndex === -1) { showNotification('请先选择歌曲', 'warning'); return; } await downloadSong(currentIndex); }
 
422
  function updatePlayButton() { playBtn.querySelector('i').className = isPlaying ? 'fas fa-pause' : 'fas fa-play'; }
423
  function togglePlay() { if (!audioPlayer.src) { showNotification('请先选择一首歌曲', 'warning'); return; } if (isPlaying) { audioPlayer.pause(); } else { audioPlayer.play().catch(e => console.error("播放命令被拒绝", e)); } }
424
  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; }
 
 
 
 
 
 
 
 
 
 
425
 
426
+ async function loadLyrics(song) {
427
+ try {
428
+ const response = await fetch(`${API_BASE}?types=lyric&source=${song.source}&id=${song.lyric_id || song.id}`);
429
+ const data = await response.json();
430
+ let lyrics = [];
431
+ if (data && data.lyric) {
432
+ lyrics = parseLyrics(data.lyric);
433
+ if (data.tlyric) {
434
+ const tLyrics = parseLyrics(data.tlyric);
435
+ mergeLyrics(lyrics, tLyrics);
436
+ }
437
+ }
438
+ displayLyrics(lyrics);
439
+ } catch (error) {
440
+ console.error('加载歌词失败:', error);
441
+ lyricsContainer.innerHTML = '<div class="lyric-line">加载歌词失败</div>';
442
+ }
443
+ }
444
+
445
+ function parseLyrics(lyricText) {
446
+ const lines = lyricText.split('\n');
447
+ const lyrics = [];
448
+ const timeRegex = /\[(\d{2}):(\d{2})[.:](\d{2,3})\]/g;
449
+ for (const line of lines) {
450
+ if (!line.trim()) continue;
451
+ let text = line.replace(timeRegex, '').trim();
452
+ if (text) {
453
+ let match;
454
+ timeRegex.lastIndex = 0;
455
+ while ((match = timeRegex.exec(line)) !== null) {
456
+ const timeInSeconds = parseInt(match[1]) * 60 + parseInt(match[2]) + parseInt(match[3]) / (match[3].length === 3 ? 1000 : 100);
457
+ lyrics.push({ time: timeInSeconds, text: text });
458
+ }
459
+ }
460
+ }
461
+ return lyrics.sort((a, b) => a.time - b.time);
462
+ }
463
+
464
+ function mergeLyrics(originalLyrics, translatedLyrics) {
465
+ const translatedMap = new Map(translatedLyrics.map(l => [Math.round(l.time * 10), l.text]));
466
+ originalLyrics.forEach(l => {
467
+ const key = Math.round(l.time * 10);
468
+ const translatedText = translatedMap.get(key);
469
+ if (translatedText) {
470
+ l.text += `<br><span style="color: #adb5bd; font-size: 0.9em;">${translatedText}</span>`;
471
+ }
472
+ });
473
+ }
474
+
475
+ function displayLyrics(lyrics) {
476
+ lyricsContainer.innerHTML = lyrics.length === 0 ? '<div class="lyric-line">暂无歌词</div>' : '';
477
+ lyrics.forEach(lyric => {
478
+ const line = document.createElement('div');
479
+ line.className = 'lyric-line';
480
+ line.innerHTML = lyric.text;
481
+ line.setAttribute('data-time', lyric.time);
482
+ line.onclick = () => { if(audioPlayer.duration) audioPlayer.currentTime = lyric.time; };
483
+ lyricsContainer.appendChild(line);
484
+ });
485
+ }
486
+
487
+ function updateProgress() {
488
+ if (audioPlayer.duration) {
489
+ progressFill.style.width = `${(audioPlayer.currentTime / audioPlayer.duration) * 100}%`;
490
+ currentTimeSpan.textContent = formatTime(audioPlayer.currentTime);
491
+ highlightCurrentLyric();
492
+ }
493
+ }
494
+
495
+ function formatTime(seconds) {
496
+ const min = Math.floor(seconds / 60);
497
+ const sec = Math.floor(seconds % 60);
498
+ return `${min}:${sec.toString().padStart(2, '0')}`;
499
+ }
500
+
501
+ function highlightCurrentLyric() {
502
+ const currentTime = audioPlayer.currentTime;
503
+ const lyricLines = document.querySelectorAll('.lyric-line');
504
+ if (lyricLines.length === 0 || lyricsContainer.offsetParent === null) return;
505
+ let activeIndex = -1;
506
+ for (let i = lyricLines.length - 1; i >= 0; i--) {
507
+ if (parseFloat(lyricLines[i].getAttribute('data-time')) <= currentTime + 0.2) {
508
+ activeIndex = i;
509
+ break;
510
+ }
511
+ }
512
+ lyricLines.forEach((line, index) => {
513
+ if (line.classList.contains('active') !== (index === activeIndex)) {
514
+ line.classList.toggle('active', index === activeIndex);
515
+ }
516
+ });
517
+ if (activeIndex > -1) {
518
+ const activeLine = lyricLines[activeIndex];
519
+ const scrollPosition = activeLine.offsetTop - lyricsContainer.clientHeight / 2 + activeLine.clientHeight / 2;
520
+ lyricsContainer.scrollTo({ top: scrollPosition, behavior: 'smooth' });
521
+ }
522
+ }
523
+
524
+ function showNotification(message, type = 'info') {
525
+ const existing = document.querySelector('.custom-notification');
526
+ if (existing) existing.remove();
527
+ const notification = document.createElement('div');
528
+ notification.className = `custom-notification notification-${type}`;
529
+ const icons = { info: 'info-circle', success: 'check-circle', error: 'exclamation-circle', warning: 'exclamation-triangle' };
530
+ notification.innerHTML = `<i class="fas fa-${icons[type]}"></i><span>${message}</span>`;
531
+ document.body.appendChild(notification);
532
+ 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' });
533
+ 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' } };
534
+ notification.style.backgroundColor = colors[type].bg;
535
+ notification.style.color = colors[type].color;
536
+ setTimeout(() => {
537
+ notification.style.transform = 'translateY(0)';
538
+ notification.style.opacity = '1';
539
+ }, 50);
540
+ setTimeout(() => {
541
+ notification.style.transform = 'translateY(100px)';
542
+ notification.style.opacity = '0';
543
+ setTimeout(() => notification.remove(), 400);
544
+ }, 3000);
545
+ }
546
+
547
+ function previousSong() {
548
+ if (currentPlaylist.length === 0) return;
549
+ currentIndex = (currentIndex - 1 + currentPlaylist.length) % currentPlaylist.length;
550
+ playSong(currentIndex);
551
+ }
552
+
553
+ function nextSong() {
554
+ if (currentPlaylist.length === 0) return;
555
+ currentIndex = (currentIndex + 1) % currentPlaylist.length;
556
+ playSong(currentIndex);
557
+ }
558
+
559
  initializeApp();
560
  </script>
561
  </body>