Files
api-python/data/pan/anian-music.js
T
2026-07-02 15:59:08 +08:00

461 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
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 = `
<div style="opacity:.4;font-size:12px;line-height:1.2;min-height:16px">${top}</div>
<div style="font-size:14px;font-weight:600;margin:2px 0;line-height:1.2;min-height:18px">${mid}</div>
<div style="opacity:.4;font-size:12px;line-height:1.2;min-height:16px">${bottom}</div>
`;
};
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 = `
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:6px">
<div style="flex:1;line-height:1.2">
<div style="font-size:13px">${currentSong?.title || "加载中"}</div>
<div style="font-size:11px;opacity:.7;margin-top:2px">${currentSong?.author || "请稍候"}</div>
</div>
<div style="display:flex;gap:6px;align-items:center;flex-shrink:0;">
<button class="minPlayerBtn" style="
width:24px;height:24px;
border:none;
border-radius:6px;
background:rgba(255,255,255,.08);
color:#fff;
font-size:14px;
cursor:pointer;
"></button>
<button class="closePlayerBtn" style="
width:24px;height:24px;
border:none;
border-radius:6px;
background:rgba(255,255,255,.08);
color:#fff;
font-size:14px;
cursor:pointer;
">×</button>
</div>
</div>
`;
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 = `
<div class="t"></div>
<div class="lrc" style="margin:10px 0 8px 0;text-align:center"></div>
<input class="seek" type="range" min="0" max="1000" value="0"
style="width:100%;margin:0 0 8px 0;accent-color:#fff">
<div style="display:flex;justify-content:space-evenly;align-items:center">
<button class="pp">播放</button>
<button class="cc">切歌</button>
<button class="vu">音量+</button>
<button class="vd">音量-</button>
</div>`;
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);
}
.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();
}
});
})();