(() => { let songs, audio, currentIndex = 0; let floatingButton, progressRing, playerPanel, coverImg, popupBgImg, animationFrameId; let lyricLines = []; let isPlayerClosed = false; const playlistApiUrl = "https://api.i-meto.com/meting/api?server=netease&type=playlist&id=8034031191"; // ========================= // 工具 // ========================= const getRandomSongIndex = () => { if (!songs || !songs.length) return 0; return Math.floor(Math.random() * songs.length); }; const getCoverSrc = () => { if (!songs || !songs.length) return "/logo.webp"; const pic = songs[currentIndex % songs.length]?.pic; return pic?.trim() ? pic.trim() : "/logo.webp"; }; const setPopupBackground = (src) => { if (!popupBgImg) return; const fallback = "/logo.webp"; const nextSrc = src?.trim() ? src.trim() : fallback; popupBgImg.src = nextSrc; }; // ========================= // 封面 // ========================= const setCover = () => { if (!coverImg) return; const cover = getCoverSrc(); coverImg.src = cover; setPopupBackground(cover); }; // ========================= // audio // ========================= const initAudio = () => { if (!audio) { audio = new Audio(); audio.volume = 0.5; audio.onplay = () => { syncUI(); setRotate(); }; audio.onpause = () => { syncUI(); setRotate(); }; audio.onended = () => next(); } }; // ========================= // LRC解析 // ========================= const parseLrc = (text) => { const lines = text.split("\n"); const res = []; const reg = /\[(\d{2}):(\d{2})\.(\d{2,3})\]/; for (let line of lines) { const timeMatch = line.match(reg); if (!timeMatch) continue; const time = (+timeMatch[1]) * 60 + (+timeMatch[2]) + (+timeMatch[3]) / (timeMatch[3].length === 3 ? 1000 : 100); const txt = line.replace(reg, "").trim(); if (txt) res.push({ time, text: txt }); } return res.sort((left, right) => left.time - right.time); }; // ========================= // 歌词渲染 // ========================= const renderLrc = (top, mid, bottom) => { const box = playerPanel?.querySelector(".lrc"); if (!box) return; box.innerHTML = `
${top}
${mid}
${bottom}
`; }; const renderLoadingLrc = () => { renderLrc("加", "载", "中"); }; // ========================= // loading歌词 // ========================= const loadLyric = (song) => { lyricLines = []; renderLoadingLrc(); if (!song?.lrc) { renderLrc("-", "暂无歌词", "-"); return; } fetch(song.lrc) .then(response => response.text()) .then(text => { lyricLines = parseLrc(text); }) .catch(() => { lyricLines = []; renderLrc("-", "加载失败", "-"); }); }; // ========================= // load song // ========================= const loadSong = () => { if (!songs || !songs.length) return; const currentSong = songs[currentIndex % songs.length]; initAudio(); audio.src = currentSong.url; loadLyric(currentSong); }; // ========================= // UI(标题) // ========================= const syncUI = () => { if (!playerPanel) return; const titleBox = playerPanel.querySelector(".t"); if (!titleBox) return; const currentSong = songs?.length ? songs[currentIndex % songs.length] : null; titleBox.innerHTML = `
${currentSong?.title || "加载中"}
${currentSong?.author || "请稍候"}
`; const isPlaying = audio && !audio.paused; playerPanel.querySelector(".pp").textContent = isPlaying ? "暂停" : "播放"; // 关闭(彻底关闭) titleBox.querySelector(".closePlayerBtn").onclick = (event) => { event.stopPropagation(); if (audio) { audio.pause(); audio.src = ""; } isPlayerClosed = true; floatingButton.style.display = "none"; playerPanel.style.display = "none"; }; // 最小化(只收起面板) titleBox.querySelector(".minPlayerBtn").onclick = (event) => { event.stopPropagation(); if (playerPanel) playerPanel.style.display = "none"; }; }; // ========================= // rotate // ========================= const setRotate = () => { if (!coverImg) return; coverImg.style.animationPlayState = (audio && !audio.paused) ? "running" : "paused"; }; // ========================= // 歌词更新 // ========================= const updateLrc = () => { if (!lyricLines.length || !audio) return; const currentTime = audio.currentTime; let lyricIndex = 0; for (let lyricLineIndex = 0; lyricLineIndex < lyricLines.length; lyricLineIndex++) { if (currentTime >= lyricLines[lyricLineIndex].time) lyricIndex = lyricLineIndex; else break; } renderLrc( lyricLines[lyricIndex - 1]?.text || "-", lyricLines[lyricIndex]?.text || "-", lyricLines[lyricIndex + 1]?.text || "-" ); }; // ========================= // progress // ========================= const progress = () => { if (!audio || !audio.duration) return; const progressPercent = audio.currentTime / audio.duration; progressRing.style.background = `conic-gradient(#fff ${progressPercent * 360}deg,rgba(255,255,255,.15) 0)`; const seek = playerPanel?.querySelector(".seek"); if (seek && !seek.matches(":active")) { seek.value = Math.floor(progressPercent * 1000); } updateLrc(); }; // ========================= // play / pause / next // ========================= const play = async () => { initAudio(); if (!audio.src) loadSong(); try { await audio.play(); } catch { } syncUI(); setRotate(); cancelAnimationFrame(animationFrameId); loop(); }; const pause = () => { if (audio) audio.pause(); syncUI(); setRotate(); }; const next = () => { if (!songs || !songs.length) return; let newIndex = getRandomSongIndex(); if (songs.length > 1) { while (newIndex === currentIndex) newIndex = getRandomSongIndex(); } currentIndex = newIndex; const currentSong = songs[currentIndex % songs.length]; loadSong(); syncUI(); setRotate(); setCover(); loadLyric(currentSong); audio.play().catch(() => { }); }; // ========================= // volume // ========================= const volUp = () => { if (!audio) return; audio.volume = Math.min(1, audio.volume + 0.1); }; const volDown = () => { if (!audio) return; audio.volume = Math.max(0, audio.volume - 0.1); }; // ========================= // loop // ========================= const loop = () => { progress(); animationFrameId = requestAnimationFrame(loop); }; // ========================= // UI init // ========================= const initUI = () => { floatingButton = document.createElement("div"); floatingButton.style.cssText = "position:fixed;right:18px;bottom:18px;width:58px;height:58px;border-radius:50%;background:#111;z-index:99999;display:flex;align-items:center;justify-content:center;cursor:pointer;overflow:hidden"; progressRing = document.createElement("div"); progressRing.style.cssText = "position:absolute;inset:0;border-radius:50%;background:conic-gradient(rgba(255,255,255,.15) 0deg,transparent 0);z-index:0"; coverImg = document.createElement("img"); coverImg.src = "/logo.webp"; coverImg.style.cssText = "width:46px;height:46px;border-radius:50%;object-fit:cover;z-index:2;position:relative;animation:spin 6s linear infinite;animation-play-state:paused"; floatingButton.appendChild(progressRing); floatingButton.appendChild(coverImg); playerPanel = document.createElement("div"); playerPanel.style.cssText = "position:fixed;right:18px;bottom:90px;width:240px;background:#000;color:#fff;border-radius:12px;display:none;z-index:99999;overflow:hidden"; playerPanel.innerHTML = `
`; const popupBg = document.createElement("img"); popupBg.className = "popupBg"; popupBg.alt = ""; const popupShade = document.createElement("div"); popupShade.className = "popupShade"; const popupContent = document.createElement("div"); popupContent.className = "popupContent"; while (playerPanel.firstChild) popupContent.appendChild(playerPanel.firstChild); playerPanel.appendChild(popupBg); playerPanel.appendChild(popupShade); playerPanel.appendChild(popupContent); popupBgImg = popupBg; setPopupBackground("/logo.webp"); document.body.appendChild(floatingButton); document.body.appendChild(playerPanel); const style = document.createElement("style"); style.textContent = ` @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } button { transition: transform .05s ease, background .05s ease; } button:active { transform: scale(0.92); } .pp, .cc, .vu, .vd { background:transparent; color:#fff; border:none; font-size:16px; } .popupBg { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; z-index:0; } .popupShade { position:absolute; inset:0; background:rgba(0,0,0,.58); z-index:1; } .popupContent { position:relative; z-index:2; padding:10px; } `; document.head.appendChild(style); floatingButton.onclick = () => { if (isPlayerClosed) return; playerPanel.style.display = playerPanel.style.display === "none" ? "block" : "none"; }; playerPanel.querySelector(".pp").onclick = (event) => { event.stopPropagation(); if (!audio || audio.paused) play(); else pause(); }; playerPanel.querySelector(".cc").onclick = () => next(); playerPanel.querySelector(".vu").onclick = () => volUp(); playerPanel.querySelector(".vd").onclick = () => volDown(); const seek = playerPanel.querySelector(".seek"); seek.addEventListener("input", () => { if (!audio || !audio.duration) return; audio.currentTime = (seek.value / 1000) * audio.duration; }); setInterval(progress, 100); }; // ========================= // init // ========================= initUI(); renderLoadingLrc(); syncUI(); fetch(playlistApiUrl) .then(response => response.json()) .then(responseSongs => { songs = responseSongs; if (songs && songs.length) { currentIndex = getRandomSongIndex(); setCover(); syncUI(); loadSong(); } else { syncUI(); } }); })();