/**
* [NMPv2] NeteaseMiniPlayer v2 JavaScript
* Lightweight Player Component Based on NetEase Cloud Music API
*
* Copyright 2025 BHCN STUDIO & 北海的佰川(ImBHCN[numakkiyu])
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
(() => {
try {
const s = document.currentScript;
if (s && s.src) {
fetch(s.src, { mode: "cors", credentials: "omit" }).catch(() => {});
}
} catch (e) {}
})();
const GlobalAudioManager = {
currentPlayer: null,
setCurrent(player) {
if (this.currentPlayer && this.currentPlayer !== player) {
this.currentPlayer.pause();
}
this.currentPlayer = player;
},
};
const ICONS = {
prev: ``,
next: ``,
play: ``,
pause: ``,
volume: ``,
lyrics: ``,
list: ``,
minimize: ``,
maximize: ``,
loopList: ``,
loopSingle: ``,
shuffle: ``,
};
class NeteaseMiniPlayer {
constructor(element) {
this.element = element;
this.element.neteasePlayer = this;
this.config = this.parseConfig();
this.currentSong = null;
this.playlist = [];
this.currentIndex = 0;
this.audio = new Audio();
this.wasPlayingBeforeHidden = false;
this.isPlaying = false;
this.currentTime = 0;
this.duration = 0;
this.volume = 0.7;
this.lyrics = [];
this.currentLyricIndex = -1;
this.showLyrics = this.config.lyric;
this.cache = new Map();
this.init();
this.playMode = "list";
this.shuffleHistory = [];
this.idleTimeout = null;
this.idleDelay = 5000;
this.isIdle = false;
}
parseConfig() {
const element = this.element;
const position = element.dataset.position || "static";
const validPositions = ["static", "top-left", "top-right", "bottom-left", "bottom-right"];
const finalPosition = validPositions.includes(position) ? position : "static";
const defaultMinimized = element.dataset.defaultMinimized === "true";
const embedValue = element.getAttribute("data-embed") || element.dataset.embed;
const isEmbed = embedValue === "true" || embedValue === true;
const autoPauseAttr = element.getAttribute("data-auto-pause") ?? element.dataset.autoPause;
const autoPauseDisabled = autoPauseAttr === "true" || autoPauseAttr === true;
const apiUrls = JSON.parse(element.dataset.apiUrls) || apiUrls === [];
// 读取 autoPause 配置
let autoPause = true; // 默认启用自动暂停
if (window.__NETEASE_MUSIC_CONFIG__?.autoPause !== undefined) {
autoPause = window.__NETEASE_MUSIC_CONFIG__.autoPause;
} else if (element.dataset.autoPause !== undefined) {
autoPause = element.dataset.autoPause !== "false";
}
return {
embed: isEmbed,
autoplay: element.dataset.autoplay === "true",
playlistId: element.dataset.playlistId,
songId: element.dataset.songId,
position: finalPosition,
lyric: element.dataset.lyric !== "false",
theme: element.dataset.theme || "auto",
size: element.dataset.size || "compact",
defaultMinimized: defaultMinimized,
autoPauseDisabled: autoPauseDisabled,
autoPause: autoPause,
apiUrls: apiUrls,
};
}
async init() {
if (this.config.embed) {
this.element.setAttribute("data-embed", "true");
}
this.element.setAttribute("data-position", this.config.position);
if (this.config.embed) {
this.element.classList.add("netease-mini-player-embed");
}
this.initTheme();
this.createPlayerHTML();
this.applyResponsiveControls?.();
this.setupEnvListeners?.();
this.bindEvents();
this.setupAudioEvents();
try {
if (this.config.embed) {
if (this.config.songId) {
await this.loadSingleSong(this.config.songId);
} else if (this.config.playlistId) {
await this.loadPlaylist(this.config.playlistId);
this.playlist = [this.playlist[0]];
}
} else {
if (this.config.playlistId) {
await this.loadPlaylist(this.config.playlistId);
} else if (this.config.songId) {
await this.loadSingleSong(this.config.songId);
}
}
if (this.playlist.length > 0) {
await this.loadCurrentSong();
if (this.config.autoplay && !this.config.embed) {
this.play();
}
}
if (this.config.defaultMinimized && !this.config.embed && this.config.position !== "static") {
this.toggleMinimize();
}
} catch (error) {
console.error("播放器初始化失败:", error);
this.showError("加载失败,请稍后重试");
}
}
createPlayerHTML() {
this.element.innerHTML = `
${
!this.config.embed
? ``
: ""
}
${
!this.config.embed
? ``
: ""
}
${ICONS.lyrics}
${
!this.config.embed
? `
${ICONS.loopList}`
: ""
}
${
!this.config.embed
? `
${ICONS.list}`
: ""
}
${
!this.config.embed
? `
${ICONS.minimize}`
: ""
}
`;
this.elements = {
albumCover: this.element.querySelector(".album-cover"),
albumCoverContainer: this.element.querySelector(".album-cover-container"),
songTitle: this.element.querySelector(".song-title"),
songArtist: this.element.querySelector(".song-artist"),
lyricsContainer: this.element.querySelector(".lyrics-container"),
lyricLine: this.element.querySelector(".lyric-line.original"),
lyricTranslation: this.element.querySelector(".lyric-line.translation"),
playBtn: this.element.querySelector(".play-btn"),
playIcon: this.element.querySelector(".play-icon"),
pauseIcon: this.element.querySelector(".pause-icon"),
prevBtn: this.element.querySelector(".prev-btn"),
nextBtn: this.element.querySelector(".next-btn"),
progressContainer: this.element.querySelector(".progress-bar-container"),
progressBar: this.element.querySelector(".progress-bar"),
currentTime: this.element.querySelector(".current-time"),
totalTime: this.element.querySelector(".total-time"),
volumeContainer: this.element.querySelector(".volume-container"),
volumeSlider: this.element.querySelector(".volume-slider"),
volumeBar: this.element.querySelector(".volume-bar"),
volumeIcon: this.element.querySelector(".volume-icon"),
lyricsBtn: this.element.querySelector(".lyrics-btn"),
listBtn: this.element.querySelector(".list-btn"),
minimizeBtn: this.element.querySelector(".minimize-btn"),
playlistContainer: this.element.querySelector(".playlist-container"),
playlistContent: this.element.querySelector(".playlist-content"),
};
this.isMinimized = false;
this.elements.loopModeBtn = this.element.querySelector(".loop-mode-btn");
}
bindEvents() {
this.elements.playBtn.addEventListener("click", () => this.togglePlay());
if (this.elements.prevBtn) {
this.elements.prevBtn.addEventListener("click", () => this.previousSong());
}
if (this.elements.nextBtn) {
this.elements.nextBtn.addEventListener("click", () => this.nextSong());
}
if (this.elements.loopModeBtn) {
this.elements.loopModeBtn.addEventListener("click", () => this.togglePlayMode());
}
this.elements.albumCoverContainer.addEventListener("click", () => {
if (this.element.classList.contains("minimized")) {
this.elements.albumCoverContainer.classList.toggle("expanded");
return;
}
if (this.currentSong && this.currentSong.id) {
const songUrl = `https://music.163.com/song?id=${this.currentSong.id}`;
window.open(songUrl, "_blank", "noopener,noreferrer");
}
});
let isDragging = false;
this.elements.progressContainer.addEventListener("mousedown", (e) => {
isDragging = true;
this.seekTo(e);
});
document.addEventListener("mousemove", (e) => {
if (isDragging) {
this.seekTo(e);
}
});
document.addEventListener("mouseup", () => {
isDragging = false;
});
this.elements.progressContainer.addEventListener("click", (e) => this.seekTo(e));
let isVolumesDragging = false;
this.elements.volumeSlider.addEventListener("mousedown", (e) => {
isVolumesDragging = true;
this.setVolume(e);
});
document.addEventListener("mousemove", (e) => {
if (isVolumesDragging) {
this.setVolume(e);
}
});
document.addEventListener("mouseup", () => {
isVolumesDragging = false;
});
this.elements.volumeSlider.addEventListener("click", (e) => this.setVolume(e));
this.elements.lyricsBtn.addEventListener("click", () => this.toggleLyrics());
if (this.elements.listBtn) {
this.elements.listBtn.addEventListener("click", () => this.togglePlaylist());
}
if (this.elements.minimizeBtn) {
this.elements.minimizeBtn.addEventListener("click", () => this.toggleMinimize());
}
document.addEventListener("click", (e) => {
if (this.elements.playlistContainer && this.elements.playlistContainer.classList.contains("show")) {
if (!this.element.contains(e.target)) {
this.togglePlaylist(false);
}
}
});
if (this.config.position !== "static" && !this.config.embed) {
this.setupDragAndDrop();
}
// 标签页非激活时自动暂停的处理
if (typeof document.hidden !== "undefined" && this.config.autoPause) {
document.addEventListener("visibilitychange", () => {
if (document.hidden && this.isPlaying) {
this.wasPlayingBeforeHidden = true;
this.pause();
} else if (!document.hidden && this.wasPlayingBeforeHidden) {
this.play();
this.wasPlayingBeforeHidden = false;
}
});
}
this.element.addEventListener("mouseenter", () => {
this.restoreOpacity();
});
this.element.addEventListener("mouseleave", () => {
this.startIdleTimer();
});
this.applyIdlePolicyOnInit();
}
startIdleTimer() {
this.clearIdleTimer();
if (!this.shouldEnableIdleOpacity()) return;
this.idleTimeout = setTimeout(() => {
this.triggerFadeOut();
}, this.idleDelay);
}
clearIdleTimer() {
if (this.idleTimeout) {
clearTimeout(this.idleTimeout);
this.idleTimeout = null;
}
}
triggerFadeOut() {
if (!this.shouldEnableIdleOpacity()) return;
if (this.isIdle) return;
this.isIdle = true;
this.element.classList.remove("fading-in");
const side = this.getDockSide();
if (side) {
this.element.classList.add(`docked-${side}`);
}
this.element.classList.add("fading-out");
const onEnd = (e) => {
if (e.animationName !== "player-fade-out") return;
this.element.classList.remove("fading-out");
this.element.classList.add("idle");
this.element.removeEventListener("animationend", onEnd);
};
this.element.addEventListener("animationend", onEnd);
}
restoreOpacity() {
this.clearIdleTimer();
const side = this.getDockSide();
const hasDock = side ? this.element.classList.contains(`docked-${side}`) : false;
if (hasDock) {
const popAnim = side === "right" ? "player-popout-right" : "player-popout-left";
this.element.classList.add(`popping-${side}`);
const onPopEnd = (e) => {
if (e.animationName !== popAnim) return;
this.element.removeEventListener("animationend", onPopEnd);
this.element.classList.remove(`popping-${side}`);
this.element.classList.remove(`docked-${side}`);
if (this.isIdle) {
this.isIdle = false;
}
this.element.classList.remove("idle", "fading-out");
this.element.classList.add("fading-in");
const onEndIn = (ev) => {
if (ev.animationName !== "player-fade-in") return;
this.element.classList.remove("fading-in");
this.element.removeEventListener("animationend", onEndIn);
};
this.element.addEventListener("animationend", onEndIn);
};
this.element.addEventListener("animationend", onPopEnd);
return;
}
if (!this.isIdle) return;
this.isIdle = false;
this.element.classList.remove("idle", "fading-out");
this.element.classList.add("fading-in");
const onEndIn = (ev) => {
if (ev.animationName !== "player-fade-in") return;
this.element.classList.remove("fading-in");
this.element.removeEventListener("animationend", onEndIn);
};
this.element.addEventListener("animationend", onEndIn);
}
shouldEnableIdleOpacity() {
return this.isMinimized === true;
}
applyIdlePolicyOnInit() {
if (!this.shouldEnableIdleOpacity()) {
this.clearIdleTimer();
this.isIdle = false;
this.element.classList.remove(
"idle",
"fading-in",
"fading-out",
"docked-left",
"docked-right",
"popping-left",
"popping-right"
);
}
}
getDockSide() {
const pos = this.config.position;
if (pos === "top-left" || pos === "bottom-left") return "left";
if (pos === "top-right" || pos === "bottom-right") return "right";
return "right";
}
static getUAInfo() {
if (NeteaseMiniPlayer._uaCache) return NeteaseMiniPlayer._uaCache;
const nav = typeof navigator !== "undefined" ? navigator : {};
const uaRaw = nav.userAgent || "";
const ua = uaRaw.toLowerCase();
const platform = (nav.platform || "").toLowerCase();
const maxTP = nav.maxTouchPoints || 0;
const isWeChat = /micromessenger/.test(ua);
const isQQ = /(mqqbrowser| qq)/.test(ua);
const isInAppWebView = /\bwv\b|; wv/.test(ua) || /version\/\d+.*chrome/.test(ua);
const isiPhone = /iphone/.test(ua);
const isiPadUA = /ipad/.test(ua);
const isIOSLikePad = !isiPadUA && platform.includes("mac") && maxTP > 1;
const isiOS = isiPhone || isiPadUA || isIOSLikePad;
const isAndroid = /android/.test(ua);
const isHarmonyOS = /harmonyos/.test(uaRaw) || /huawei|honor/.test(ua);
const isMobileToken = /mobile/.test(ua) || /sm-|mi |redmi|huawei|honor|oppo|vivo|oneplus/.test(ua);
const isHarmonyDesktop = isHarmonyOS && !isMobileToken && !isAndroid && !isiOS;
const isPWA =
(typeof window !== "undefined" &&
((window.matchMedia && window.matchMedia("(display-mode: standalone)").matches) ||
nav.standalone === true)) ||
false;
const isMobile = isiOS || isAndroid || (isHarmonyOS && !isHarmonyDesktop) || isMobileToken || isInAppWebView;
const info = {
isMobile,
isiOS,
isAndroid,
isHarmonyOS,
isHarmonyDesktop,
isWeChat,
isQQ,
isInAppWebView,
isPWA,
isiPad: isiPadUA || isIOSLikePad,
};
NeteaseMiniPlayer._uaCache = info;
return info;
}
applyResponsiveControls() {
const env = NeteaseMiniPlayer.getUAInfo();
const shouldHideVolume = !!env.isMobile;
this.element.classList.toggle("mobile-env", shouldHideVolume);
if (this.elements && this.elements.volumeContainer == null) {
this.elements.volumeContainer = this.element.querySelector(".volume-container");
}
if (this.elements.volumeContainer) {
if (shouldHideVolume) {
this.elements.volumeContainer.classList.add("sr-visually-hidden");
this.elements.volumeContainer.setAttribute("aria-hidden", "false");
this.elements.volumeSlider?.setAttribute("aria-label", "音量控制(移动端隐藏,仅无障碍可见)");
} else {
this.elements.volumeContainer.classList.remove("sr-visually-hidden");
this.elements.volumeContainer.removeAttribute("aria-hidden");
this.elements.volumeSlider?.removeAttribute("aria-label");
}
}
}
setupEnvListeners() {
const reapply = () => this.applyResponsiveControls();
if (window.matchMedia) {
try {
const mq1 = window.matchMedia("(orientation: portrait)");
const mq2 = window.matchMedia("(orientation: landscape)");
mq1.addEventListener?.("change", reapply);
mq2.addEventListener?.("change", reapply);
} catch (e) {
mq1.onchange = reapply;
mq2.onchange = reapply;
}
} else {
window.addEventListener("orientationchange", reapply);
}
window.addEventListener("resize", reapply);
}
setupAudioEvents() {
this.audio.addEventListener("loadedmetadata", () => {
this.duration = this.audio.duration;
this.updateTimeDisplay();
});
this.audio.addEventListener("timeupdate", () => {
this.currentTime = this.audio.currentTime;
this.updateProgress();
this.updateLyrics();
this.updateTimeDisplay();
});
this.audio.addEventListener("ended", async () => {
await this.nextSong();
});
this.audio.addEventListener("error", async (e) => {
console.error("音频播放错误:", e);
console.error("错误详情:", {
code: e.target.error?.code,
message: e.target.error?.message,
src: e.target.src,
});
this.showError("播放失败,尝试下一首");
setTimeout(async () => {
await this.nextSong();
}, 1000);
});
this.audio.addEventListener("abort", () => {
console.warn("音频加载被中断");
});
this.audio.addEventListener("stalled", () => {
console.warn("音频加载停滞");
});
this.audio.addEventListener("canplay", () => {
if (this.isPlaying && this.audio.paused) {
this.audio.play().catch((e) => console.error("自动播放失败:", e));
}
});
this.audio.volume = this.volume;
this.updateVolumeDisplay();
}
async apiRequest(endpoint, params = {}) {
const apiUrls = this.config.apiUrls;
for (const baseUrl of apiUrls) {
try {
const queryParams = {
server: "netease",
type: "playlist",
id: params.id,
...params,
};
const queryString = new URLSearchParams(queryParams).toString();
const url = `${baseUrl}?${queryString}`;
const response = await fetch(url, { mode: "cors", timeout: 5000 });
const data = await response.json();
if (!data) {
continue;
}
return {
code: 200,
songs: data || [],
};
} catch (error) {
console.warn(`API ${baseUrl} 请求失败:`, error);
continue;
}
}
throw new Error("所有 API 都请求失败");
}
getCacheKey(type, id) {
return `${type}_${id}`;
}
setCache(key, data, expiry = 5 * 60 * 1000) {
this.cache.set(key, {
data,
expiry: Date.now() + expiry,
});
}
getCache(key) {
const cached = this.cache.get(key);
if (cached && cached.expiry > Date.now()) {
return cached.data;
}
this.cache.delete(key);
return null;
}
async loadPlaylist(playlistId) {
const cacheKey = this.getCacheKey("playlist_all", playlistId);
let tracks = this.getCache(cacheKey);
if (!tracks) {
const response = await this.apiRequest("", {
id: playlistId,
});
tracks = response.songs || [];
this.setCache(cacheKey, tracks);
}
if (!tracks || tracks.length === 0) {
console.warn("歌单为空或无法加载,使用默认歌曲");
// 提供一个默认歌曲,避免播放器崩溃
// 使用特殊 ID "_empty" 表示这是一个占位符歌曲
this.playlist = [
{
id: "_empty",
name: "网络加载失败",
artists: "Cloud Home",
album: "Demo",
picUrl: "",
duration: 0,
},
];
return;
}
this.playlist = tracks.map((song) => {
// Meting API 返回格式可能变化,需要从多个地方获取 ID
let songId = song.id || song.mid;
// 如果没有直接的 ID,尝试从 URL 中提取
if (!songId && song.url) {
const urlMatch = song.url.match(/id=(\d+)/);
songId = urlMatch ? urlMatch[1] : null;
}
// 或从 lrc URL 中提取
if (!songId && song.lrc) {
const lrcMatch = song.lrc.match(/id=(\d+)/);
songId = lrcMatch ? lrcMatch[1] : null;
}
if (!songId) {
console.warn("歌曲缺少ID,无法播放:", song.title || song.name);
}
return {
id: songId || "",
name: song.name || song.title || "Unknown",
artists: song.artist || song.author || "Unknown Artist",
album: song.album || "Unknown Album",
picUrl: song.pic || song.cover || "",
duration: song.duration
? typeof song.duration === "string"
? parseInt(song.duration) * 1000
: song.duration * 1000
: 0,
// 保存原始 API 返回的 URL 供后续使用
rawUrl: song.url || null,
rawLyricUrl: song.lrc || null,
};
});
this.setCache(cacheKey, tracks);
this.updatePlaylistDisplay();
}
async loadSingleSong(songId) {
const cacheKey = this.getCacheKey("song", songId);
let songData = this.getCache(cacheKey);
if (!songData) {
const apiUrls = [
`https://www.bilibili.uno/api?server=netease&type=song&id=${songId}`,
`https://meting-api.wangcy.site/api?server=netease&type=song&id=${songId}`,
];
for (const url of apiUrls) {
try {
const response = await fetch(url);
const songs = await response.json();
if (songs && songs.length > 0) {
const song = songs[0];
songData = {
id: song.id || song.mid || songId,
name: song.name || song.title || "Unknown",
artists: song.artist || song.author || "Unknown Artist",
album: song.album || "Unknown Album",
picUrl: song.pic || song.cover || "",
duration: song.duration
? typeof song.duration === "string"
? parseInt(song.duration) * 1000
: song.duration * 1000
: 0,
};
this.setCache(cacheKey, songData);
break;
}
} catch (error) {
console.warn("从此API获取歌曲失败:", error);
continue;
}
}
if (!songData) {
throw new Error("歌曲信息获取失败");
}
}
this.playlist = [songData];
}
async loadCurrentSong() {
if (this.playlist.length === 0) return;
if (this.showLyrics) {
this.elements.lyricLine.textContent = "♪ 加载歌词中... ♪";
this.elements.lyricTranslation.style.display = "none";
this.elements.lyricLine.classList.remove("current", "scrolling");
this.elements.lyricTranslation.classList.remove("current", "scrolling");
this.lyrics = [];
this.currentLyricIndex = -1;
}
const song = this.playlist[this.currentIndex];
this.currentSong = song;
this.updateSongInfo(song);
if (song.picUrl) {
this.elements.albumCover.src = song.picUrl;
}
await this.loadSongUrl(song);
if (this.showLyrics) {
await this.loadLyrics(song);
}
}
updateSongInfo(song) {
if (!song) return;
this.elements.songTitle.textContent = song.name || "未知歌曲";
if (song.artists) {
const truncatedArtist = this.truncateArtistName(song.artists);
this.elements.songArtist.textContent = truncatedArtist;
if (truncatedArtist !== song.artists) {
this.elements.songArtist.setAttribute("title", song.artists);
} else {
this.elements.songArtist.removeAttribute("title");
}
}
}
truncateArtistName(artistText) {
if (!artistText) return "";
const tempElement = document.createElement("span");
tempElement.style.visibility = "hidden";
tempElement.style.position = "absolute";
tempElement.style.fontSize = "12px";
tempElement.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
tempElement.textContent = artistText;
document.body.appendChild(tempElement);
const fullWidth = tempElement.offsetWidth;
const availableWidth = 200;
if (fullWidth <= availableWidth) {
document.body.removeChild(tempElement);
return artistText;
}
const artists = artistText.split(" / ");
let result = "";
let currentWidth = 0;
for (let i = 0; i < artists.length; i++) {
const testText = result ? `${result} / ${artists[i]}` : artists[i];
tempElement.textContent = testText + "...";
const testWidth = tempElement.offsetWidth;
if (testWidth > availableWidth) {
if (result) {
break;
} else {
const artist = artists[i];
for (let j = 1; j < artist.length; j++) {
const partialArtist = artist.substring(0, j);
tempElement.textContent = partialArtist + "...";
if (tempElement.offsetWidth > availableWidth) {
result = artist.substring(0, Math.max(1, j - 1));
break;
}
result = partialArtist;
}
break;
}
}
result = testText;
}
document.body.removeChild(tempElement);
return result + (result !== artistText ? "..." : "");
}
async loadSongUrl(song) {
if (!song || !song.id || song.id === "_empty") {
console.warn("歌曲对象无效,跳过加载音频URL");
return;
}
const songId = String(song.id); // 确保转换为字符串
const cacheKey = this.getCacheKey("song_url", songId);
let urlData = this.getCache(cacheKey);
if (!urlData) {
// 优先尝试使用 playlist 中已有的 URL
if (song.rawUrl) {
try {
const response = await fetch(song.rawUrl, { method: "HEAD" });
if (response.ok) {
urlData = { url: song.rawUrl };
this.setCache(cacheKey, urlData, 30 * 60 * 1000);
}
} catch (error) {
console.warn("验证歌单URL失败:", error);
}
}
// 如果没有原始 URL,尝试从 API 获取
if (!urlData) {
const baseUrls = this.config.apiUrls;
const apiUrls = baseUrls.map((baseUrl) => `${baseUrl}?server=netease&type=song&id=${songId}`);
for (const url of apiUrls) {
try {
const response = await fetch(url, { mode: "cors" });
const data = await response.json();
if (data && data.length > 0) {
urlData = {
url: data[0].url || data[0],
};
this.setCache(cacheKey, urlData, 30 * 60 * 1000);
break;
}
} catch (error) {
console.warn("从此API获取失败:", error);
continue;
}
}
}
}
if (urlData && urlData.url) {
const httpsUrl = this.ensureHttps(urlData.url);
this.audio.src = httpsUrl;
} else {
console.warn("无法获取音频URL");
}
}
ensureHttps(url) {
if (!url) return url;
if (url.includes("music.126.net")) {
return url.replace(/^http:\/\//, "https://");
}
if (url.startsWith("http://")) {
return url.replace("http://", "https://");
}
return url;
}
async loadLyrics(song) {
if (!song || !song.id || song.id === "_empty") {
console.warn("歌曲对象无效,跳过加载歌词");
return;
}
const songId = String(song.id); // 确保转换为字符串
const cacheKey = this.getCacheKey("lyric", songId);
let lyricData = this.getCache(cacheKey);
if (!lyricData) {
const baseUrls = this.config.apiUrls;
const apiUrls = baseUrls.map((baseUrl) => `${baseUrl}?server=netease&type=lrc&id=${songId}`);
for (const url of apiUrls) {
try {
const response = await fetch(url);
const contentType = response.headers.get("content-type") || "";
if (contentType.includes("application/json")) {
lyricData = await response.json();
} else {
// API 直接返回 lrc 文件内容(纯文本)
const lrcText = await response.text();
lyricData = { lrc: { lyric: lrcText } };
}
if (lyricData) {
this.setCache(cacheKey, lyricData, 60 * 60 * 1000);
break;
}
} catch (error) {
console.warn("从此API获取歌词失败:", error);
continue;
}
}
if (!lyricData) {
console.warn("无法获取歌词");
this.lyrics = [];
return;
}
}
this.parseLyrics(lyricData);
}
parseLyrics(lyricData) {
this.lyrics = [];
this.currentLyricIndex = -1;
if (!lyricData || (!lyricData.lrc?.lyric && !lyricData.tlyric?.lyric)) {
this.elements.lyricLine.textContent = "暂无歌词";
this.elements.lyricTranslation.style.display = "none";
this.elements.lyricLine.classList.remove("current", "scrolling");
this.elements.lyricTranslation.classList.remove("current", "scrolling");
return;
}
// 处理 lrc 数据可能是字符串或对象的情况
const lrcContent = typeof lyricData.lrc === "string" ? lyricData.lrc : lyricData.lrc?.lyric || "";
const tlyricContent = typeof lyricData.tlyric === "string" ? lyricData.tlyric : lyricData.tlyric?.lyric || "";
const lrcLines = lrcContent.split("\n");
const tlyricLines = tlyricContent ? tlyricContent.split("\n") : [];
const lrcMap = new Map();
lrcLines.forEach((line) => {
const match = line.match(/\[(\d{2}):(\d{2})\.(\d{2,3})\](.*)/);
if (match) {
const minutes = parseInt(match[1]);
const seconds = parseInt(match[2]);
const milliseconds = parseInt(match[3].padEnd(3, "0"));
const time = minutes * 60 + seconds + milliseconds / 1000;
const text = match[4].trim();
if (text) {
lrcMap.set(time, text);
}
}
});
const tlyricMap = new Map();
tlyricLines.forEach((line) => {
const match = line.match(/\[(\d{2}):(\d{2})\.(\d{2,3})\](.*)/);
if (match) {
const minutes = parseInt(match[1]);
const seconds = parseInt(match[2]);
const milliseconds = parseInt(match[3].padEnd(3, "0"));
const time = minutes * 60 + seconds + milliseconds / 1000;
const text = match[4].trim();
if (text) {
tlyricMap.set(time, text);
}
}
});
const allTimes = Array.from(new Set([...lrcMap.keys(), ...tlyricMap.keys()])).sort((a, b) => a - b);
this.lyrics = allTimes.map((time) => ({
time,
text: lrcMap.get(time) || "",
translation: tlyricMap.get(time) || "",
}));
this.currentLyricIndex = -1;
this.updateLyrics();
}
async togglePlay() {
if (this.isPlaying) {
this.pause();
} else {
await this.play();
}
}
async play() {
GlobalAudioManager.setCurrent(this);
try {
await this.audio.play();
this.isPlaying = true;
this.elements.playIcon.style.display = "none";
this.elements.pauseIcon.style.display = "inline";
this.elements.albumCover.classList.add("playing");
this.element.classList.add("player-playing");
} catch (error) {
console.error("播放失败:", error);
this.showError("播放失败");
}
}
pause() {
this.audio.pause();
this.isPlaying = false;
this.elements.playIcon.style.display = "inline";
this.elements.pauseIcon.style.display = "none";
this.elements.albumCover.classList.remove("playing");
this.element.classList.remove("player-playing");
}
async previousSong() {
if (this.playlist.length <= 1) return;
this.currentIndex = this.currentIndex > 0 ? this.currentIndex - 1 : this.playlist.length - 1;
await this.loadCurrentSong();
if (this.isPlaying) {
await this.play();
}
}
async nextSong() {
const wasPlaying = this.isPlaying;
if (this.playlist.length <= 1) {
if (this.playMode === "single") {
this.audio.currentTime = 0;
if (wasPlaying) await this.play();
return;
}
this.audio.currentTime = 0;
if (wasPlaying) await this.play();
return;
}
let newIndex;
if (this.playMode === "shuffle") {
const availableIndices = this.playlist.map((_, i) => i).filter((i) => i !== this.currentIndex);
if (availableIndices.length === 0) {
newIndex = this.currentIndex;
} else {
newIndex = availableIndices[Math.floor(Math.random() * availableIndices.length)];
}
this.shuffleHistory.push(this.currentIndex);
if (this.shuffleHistory.length > 2) {
this.shuffleHistory.shift();
}
} else if (this.playMode === "single") {
newIndex = this.currentIndex;
} else {
newIndex = (this.currentIndex + 1) % this.playlist.length;
}
this.currentIndex = newIndex;
await this.loadCurrentSong();
this.updatePlaylistDisplay();
if (wasPlaying) {
setTimeout(async () => {
try {
await this.play();
} catch (error) {
console.error("自动播放下一首失败:", error);
}
}, 100);
}
}
updateProgress() {
if (this.duration > 0) {
const progress = (this.currentTime / this.duration) * 100;
this.elements.progressBar.style.width = `${progress}%`;
}
}
updateTimeDisplay() {
const formatTime = (time) => {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
};
this.elements.currentTime.textContent = formatTime(this.currentTime);
this.elements.totalTime.textContent = formatTime(this.duration);
}
updateVolumeDisplay() {
this.elements.volumeBar.style.width = `${this.volume * 100}%`;
}
updateLyrics() {
if (this.lyrics.length === 0) return;
let newIndex = -1;
for (let i = 0; i < this.lyrics.length; i++) {
if (this.currentTime >= this.lyrics[i].time) {
newIndex = i;
} else {
break;
}
}
if (newIndex !== this.currentLyricIndex) {
this.currentLyricIndex = newIndex;
if (newIndex >= 0 && newIndex < this.lyrics.length) {
const lyric = this.lyrics[newIndex];
const lyricText = lyric.text || "♪";
this.elements.lyricLine.classList.remove("current");
requestAnimationFrame(() => {
this.elements.lyricLine.textContent = lyricText;
this.checkLyricScrolling(this.elements.lyricLine, lyricText);
this.elements.lyricLine.classList.add("current");
if (lyric.translation) {
this.elements.lyricTranslation.textContent = lyric.translation;
this.elements.lyricTranslation.style.display = "block";
this.elements.lyricTranslation.classList.remove("current");
requestAnimationFrame(() => {
this.elements.lyricTranslation.classList.add("current");
});
} else {
this.elements.lyricTranslation.style.display = "none";
this.elements.lyricTranslation.classList.remove("current", "scrolling");
}
});
this.elements.lyricsContainer.classList.add("switching");
setTimeout(() => {
this.elements.lyricsContainer.classList.remove("switching");
}, 500);
if (lyric.translation) {
this.elements.lyricTranslation.textContent = lyric.translation;
this.elements.lyricTranslation.classList.add("current");
this.elements.lyricTranslation.style.display = "block";
this.checkLyricScrolling(this.elements.lyricTranslation, lyric.translation);
} else {
this.elements.lyricTranslation.style.display = "none";
this.elements.lyricTranslation.classList.remove("current", "scrolling");
}
} else {
this.elements.lyricLine.textContent = "♪ 纯音乐,请欣赏 ♪";
this.elements.lyricLine.classList.remove("current", "scrolling");
this.elements.lyricTranslation.style.display = "none";
this.elements.lyricTranslation.classList.remove("current", "scrolling");
}
}
}
checkLyricScrolling(element, text) {
if (!element || !text) return;
const tempElement = document.createElement("span");
tempElement.style.visibility = "hidden";
tempElement.style.position = "absolute";
tempElement.style.fontSize = window.getComputedStyle(element).fontSize;
tempElement.style.fontFamily = window.getComputedStyle(element).fontFamily;
tempElement.style.fontWeight = window.getComputedStyle(element).fontWeight;
tempElement.textContent = text;
document.body.appendChild(tempElement);
const textWidth = tempElement.offsetWidth;
document.body.removeChild(tempElement);
const containerWidth = element.parentElement.offsetWidth - 16;
if (textWidth > containerWidth) {
element.classList.add("scrolling");
} else {
element.classList.remove("scrolling");
}
}
updatePlaylistDisplay() {
if (!this.elements.playlistContent || !this.playlist || this.playlist.length === 0) return;
const html = this.playlist
.map(
(song, index) => `
${(index + 1).toString().padStart(2, "0")}
${song.name}
${song.artists}
`
)
.join("");
this.elements.playlistContent.innerHTML = html;
this.elements.playlistContent.querySelectorAll(".playlist-item").forEach((item) => {
item.addEventListener("click", async () => {
const index = parseInt(item.dataset.index);
if (index !== this.currentIndex) {
this.currentIndex = index;
await this.loadCurrentSong();
if (this.isPlaying) {
await this.play();
}
this.updatePlaylistDisplay();
this.togglePlaylist();
}
});
});
const activeItem = this.elements.playlistContent.querySelector(".playlist-item.active");
if (activeItem) {
activeItem.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
}
seekTo(e) {
if (!this.elements.progressContainer || !this.audio) return;
const rect = this.elements.progressContainer.getBoundingClientRect();
const percent = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
const newTime = percent * this.duration;
if (isFinite(newTime) && newTime >= 0) {
this.audio.currentTime = newTime;
}
}
setVolume(e) {
if (!this.elements.volumeSlider) return;
const rect = this.elements.volumeSlider.getBoundingClientRect();
const percent = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
this.volume = percent;
this.audio.volume = this.volume;
this.updateVolumeDisplay();
}
toggleLyrics() {
this.showLyrics = !this.showLyrics;
this.elements.lyricsContainer.classList.toggle("hidden", !this.showLyrics);
this.elements.lyricsBtn.classList.toggle("active", this.showLyrics);
}
togglePlaylist(show = null) {
if (!this.elements.playlistContainer) return;
const isShowing = this.elements.playlistContainer.classList.contains("show");
const shouldShow = show !== null ? show : !isShowing;
if (shouldShow) {
this.determinePlaylistDirection();
this.updatePlaylistDisplay();
this.elements.playlistContainer.classList.add("show");
if (this.elements.listBtn) {
this.elements.listBtn.classList.add("active");
}
} else {
this.elements.playlistContainer.classList.remove("show", "show-above", "show-below");
if (this.elements.listBtn) {
this.elements.listBtn.classList.remove("active");
}
}
}
togglePlayMode() {
const modes = ["list", "single", "shuffle"];
const currentIndex = modes.indexOf(this.playMode);
this.playMode = modes[(currentIndex + 1) % 3];
const iconSvgs = { list: ICONS.loopList, single: ICONS.loopSingle, shuffle: ICONS.shuffle };
const titles = { list: "列表循环", single: "单曲循环", shuffle: "随机播放" };
if (this.elements.loopModeBtn) {
this.elements.loopModeBtn.innerHTML = iconSvgs[this.playMode];
this.elements.loopModeBtn.title = titles[this.playMode];
}
}
toggleMinimize() {
const isCurrentlyMinimized = this.element.classList.contains("minimized");
this.isMinimized = isCurrentlyMinimized;
if (!isCurrentlyMinimized) {
this.element.classList.add("minimized");
this.isMinimized = true;
if (this.elements.minimizeBtn) {
this.elements.minimizeBtn.classList.add("active");
this.elements.minimizeBtn.title = "展开";
this.elements.minimizeBtn.innerHTML = ICONS.maximize;
}
this.clearIdleTimer();
this.isIdle = false;
this.element.classList.remove(
"idle",
"fading-in",
"fading-out",
"docked-left",
"docked-right",
"popping-left",
"popping-right"
);
this.startIdleTimer();
} else {
this.element.classList.remove("minimized");
this.isMinimized = false;
if (this.elements.minimizeBtn) {
this.elements.minimizeBtn.classList.remove("active");
this.elements.minimizeBtn.title = "缩小";
this.elements.minimizeBtn.innerHTML = ICONS.minimize;
}
this.clearIdleTimer();
if (this.isIdle) {
this.restoreOpacity();
} else {
this.element.classList.remove(
"idle",
"fading-in",
"fading-out",
"docked-left",
"docked-right",
"popping-left",
"popping-right"
);
}
this.isIdle = false;
}
}
determinePlaylistDirection() {
const playerRect = this.element.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const spaceBelow = viewportHeight - playerRect.bottom;
const spaceAbove = playerRect.top;
const playlistHeight = 220;
this.elements.playlistContainer.classList.remove("expand-up");
if (spaceBelow >= playlistHeight || spaceBelow >= spaceAbove) {
} else {
this.elements.playlistContainer.classList.add("expand-up");
}
}
setupDragAndDrop() {
return;
}
showError(message) {
this.elements.songTitle.textContent = message;
this.elements.songArtist.textContent = "";
this.elements.lyricLine.textContent = "";
}
initTheme() {
this.setTheme(this.config.theme);
if (this.config.theme === "auto") {
this.setupThemeListener();
}
}
setTheme(theme) {
if (theme === "auto") {
const detectedTheme = this.detectTheme();
this.element.setAttribute("data-theme", "auto");
if (detectedTheme === "dark") {
this.element.classList.add("theme-dark-detected");
} else {
this.element.classList.remove("theme-dark-detected");
}
} else {
this.element.setAttribute("data-theme", theme);
this.element.classList.remove("theme-dark-detected");
}
}
detectTheme() {
const hostTheme = this.detectHostTheme();
if (hostTheme) {
return hostTheme;
}
const cssTheme = this.detectCSSTheme();
if (cssTheme) {
return cssTheme;
}
return this.detectSystemTheme();
}
detectHostTheme() {
const html = document.documentElement;
const body = document.body;
const darkClasses = ["dark", "theme-dark", "dark-theme", "dark-mode"];
const lightClasses = ["light", "theme-light", "light-theme", "light-mode"];
for (const className of darkClasses) {
if (html.classList.contains(className)) return "dark";
}
for (const className of lightClasses) {
if (html.classList.contains(className)) return "light";
}
if (body) {
for (const className of darkClasses) {
if (body.classList.contains(className)) return "dark";
}
for (const className of lightClasses) {
if (body.classList.contains(className)) return "light";
}
}
const htmlTheme = html.getAttribute("data-theme");
if (htmlTheme === "dark" || htmlTheme === "light") {
return htmlTheme;
}
const bodyTheme = body?.getAttribute("data-theme");
if (bodyTheme === "dark" || bodyTheme === "light") {
return bodyTheme;
}
return null;
}
detectCSSTheme() {
try {
const rootStyles = getComputedStyle(document.documentElement);
const bgColor =
rootStyles.getPropertyValue("--bg-color") ||
rootStyles.getPropertyValue("--background-color") ||
rootStyles.getPropertyValue("--color-bg");
const textColor =
rootStyles.getPropertyValue("--text-color") ||
rootStyles.getPropertyValue("--color-text") ||
rootStyles.getPropertyValue("--text-primary");
if (bgColor || textColor) {
const isDarkBg = this.isColorDark(bgColor);
const isLightText = this.isColorLight(textColor);
if (isDarkBg || isLightText) {
return "dark";
}
if (!isDarkBg || !isLightText) {
return "light";
}
}
} catch (error) {
console.warn("CSS主题检测失败:", error);
}
return null;
}
detectSystemTheme() {
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark";
}
return "light";
}
isColorDark(color) {
if (!color) return false;
color = color.replace(/\s/g, "").toLowerCase();
if (color.includes("dark") || color.includes("black") || color === "transparent") {
return true;
}
const rgb = color.match(/rgb\((\d+),(\d+),(\d+)\)/);
if (rgb) {
const [, r, g, b] = rgb.map(Number);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
return brightness < 128;
}
const hex = color.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/);
if (hex) {
const hexValue = hex[1];
const r = parseInt(hexValue.length === 3 ? hexValue[0] + hexValue[0] : hexValue.substr(0, 2), 16);
const g = parseInt(hexValue.length === 3 ? hexValue[1] + hexValue[1] : hexValue.substr(2, 2), 16);
const b = parseInt(hexValue.length === 3 ? hexValue[2] + hexValue[2] : hexValue.substr(4, 2), 16);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
return brightness < 128;
}
return false;
}
isColorLight(color) {
return !this.isColorDark(color);
}
setupThemeListener() {
if (window.matchMedia) {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleThemeChange = () => {
if (this.config.theme === "auto") {
this.setTheme("auto");
}
};
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener("change", handleThemeChange);
} else {
mediaQuery.addListener(handleThemeChange);
}
}
if (window.MutationObserver) {
const observer = new MutationObserver((mutations) => {
if (this.config.theme === "auto") {
let shouldUpdate = false;
mutations.forEach((mutation) => {
if (
mutation.type === "attributes" &&
(mutation.attributeName === "class" || mutation.attributeName === "data-theme")
) {
shouldUpdate = true;
}
});
if (shouldUpdate) {
this.setTheme("auto");
}
}
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class", "data-theme"],
});
if (document.body) {
observer.observe(document.body, {
attributes: true,
attributeFilter: ["class", "data-theme"],
});
}
}
}
static init() {
document.querySelectorAll(".netease-mini-player").forEach((element) => {
if (!element._neteasePlayer) {
element._neteasePlayer = new NeteaseMiniPlayer(element);
}
});
}
static initPlayer(element) {
if (!element._neteasePlayer) {
element._neteasePlayer = new NeteaseMiniPlayer(element);
}
return element._neteasePlayer;
}
}
if (typeof window !== "undefined") {
window.NeteaseMiniPlayer = NeteaseMiniPlayer;
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", NeteaseMiniPlayer.init);
} else {
NeteaseMiniPlayer.init();
}
}
class NMPv2ShortcodeParser {
constructor() {
this.paramMappings = {
position: "data-position",
theme: "data-theme",
lyric: "data-lyric",
embed: "data-embed",
minimized: "data-default-minimized",
autoplay: "data-autoplay",
"idle-opacity": "data-idle-opacity",
"auto-pause": "data-auto-pause",
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => this.init());
} else {
this.init();
}
}
init() {
this.processContainer(document.body);
}
/**
* 处理容器内的所有短语法
*/
processContainer(container) {
this.processTextNodes(container);
this.processExistingElements(container);
this.initializePlayers(container);
}
/**
* 处理文本节点中的短语法
*/
processTextNodes(container) {
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null, false);
const textNodes = [];
let node;
while ((node = walker.nextNode())) {
if (node.textContent.includes("{nmpv2:")) {
textNodes.push(node);
}
}
textNodes.forEach((node) => {
const content = node.textContent;
const shortcodes = this.extractShortcodes(content);
if (shortcodes.length > 0) {
const fragment = document.createDocumentFragment();
let lastIndex = 0;
shortcodes.forEach((shortcode) => {
if (shortcode.startIndex > lastIndex) {
fragment.appendChild(
document.createTextNode(content.substring(lastIndex, shortcode.startIndex))
);
}
const playerElement = this.createPlayerElement(shortcode);
fragment.appendChild(playerElement);
lastIndex = shortcode.endIndex;
});
if (lastIndex < content.length) {
fragment.appendChild(document.createTextNode(content.substring(lastIndex)));
}
node.parentNode.replaceChild(fragment, node);
}
});
}
processExistingElements(container) {
container.querySelectorAll(".netease-mini-player:not([data-shortcode-processed])").forEach((element) => {
element.setAttribute("data-shortcode-processed", "true");
});
}
initializePlayers(container) {
container.querySelectorAll(".netease-mini-player:not([data-initialized])").forEach((element) => {
element.setAttribute("data-initialized", "true");
NeteaseMiniPlayer.initPlayer(element);
});
}
extractShortcodes(text) {
const regex = /\{nmpv2:([^}]*)\}/g;
let match;
const results = [];
let lastIndex = 0;
while ((match = regex.exec(text)) !== null) {
const content = match[1].trim();
const startIndex = match.index;
const endIndex = match.index + match[0].length;
let shortcode = {
type: "song",
id: null,
params: {},
startIndex,
endIndex,
};
this.parseShortcodeContent(content, shortcode);
results.push(shortcode);
}
return results;
}
parseShortcodeContent(content, shortcode) {
if (content.startsWith("playlist=")) {
shortcode.type = "playlist";
const parts = content.split(/,\s*/);
const firstPart = parts.shift();
shortcode.id = firstPart.replace("playlist=", "").trim();
parts.forEach((part) => this.parseParam(part, shortcode.params));
} else if (content.includes("=")) {
const parts = content.split(/,\s*/);
const firstPart = parts.shift();
if (firstPart.includes("=")) {
this.parseParam(firstPart, shortcode.params);
parts.forEach((part) => this.parseParam(part, shortcode.params));
} else {
shortcode.id = firstPart.trim();
parts.forEach((part) => this.parseParam(part, shortcode.params));
}
} else {
shortcode.id = content.trim();
}
if (shortcode.params.position === undefined || shortcode.params.position === "static") {
shortcode.params.embed = shortcode.params.embed ?? "true";
} else if (shortcode.params.embed === undefined) {
shortcode.params.embed = "false";
}
}
parseParam(paramStr, params) {
const [key, value] = paramStr.split("=");
if (!key || !value) return;
const cleanKey = key.trim().toLowerCase();
const cleanValue = value.trim().toLowerCase();
if (cleanKey === "song-id") {
params.songId = cleanValue;
} else if (cleanKey === "playlist-id") {
params.playlistId = cleanValue;
params.type = "playlist";
} else if (cleanKey === "minimized") {
params.defaultMinimized = cleanValue === "true" ? "true" : "false";
} else {
const mapping = this.paramMappings[cleanKey] || `data-${cleanKey}`;
params[cleanKey] = cleanValue;
}
}
createPlayerElement(shortcode) {
const div = document.createElement("div");
div.className = "netease-mini-player";
div.setAttribute("data-shortcode-processed", "true");
if (shortcode.type === "playlist" && shortcode.id) {
div.setAttribute("data-playlist-id", shortcode.id);
} else if (shortcode.id) {
div.setAttribute("data-song-id", shortcode.id);
}
Object.entries(shortcode.params).forEach(([key, value]) => {
if (key === "songId") {
div.setAttribute("data-song-id", value);
} else if (key === "playlistId") {
div.setAttribute("data-playlist-id", value);
} else if (key === "type") {
} else {
const dataKey = this.paramMappings[key] || `data-${key}`;
div.setAttribute(dataKey, value);
}
});
return div;
}
static processDynamicContent(content) {
const tempDiv = document.createElement("div");
tempDiv.innerHTML = content;
window.nmpv2ShortcodeParser.processContainer(tempDiv);
return tempDiv.innerHTML;
}
}
if (typeof window !== "undefined") {
window.nmpv2ShortcodeParser = new NMPv2ShortcodeParser();
window.processNMPv2Shortcodes = function (container) {
if (container instanceof Element) {
window.nmpv2ShortcodeParser.processContainer(container);
} else {
console.warn("processNMPv2Shortcodes requires a DOM element");
}
};
}
if (typeof module !== "undefined" && module.exports) {
module.exports = {
renderShortcodes: function (html) {
return html.replace(/\{nmpv2:([^}]*)\}/g, (match, content) => {
let shortcode = {
type: "song",
id: null,
params: {},
};
if (content.startsWith("playlist=")) {
shortcode.type = "playlist";
const parts = content.split(/,\s*/);
shortcode.id = parts[0].replace("playlist=", "").trim();
parts.slice(1).forEach((part) => {
const [key, value] = part.split("=");
if (key && value) shortcode.params[key.trim()] = value.trim();
});
} else {
const parts = content.split(/,\s*/);
if (parts[0].includes("=")) {
parts.forEach((part) => {
const [key, value] = part.split("=");
if (key && value) shortcode.params[key.trim()] = value.trim();
});
} else {
shortcode.id = parts[0].trim();
parts.slice(1).forEach((part) => {
const [key, value] = part.split("=");
if (key && value) shortcode.params[key.trim()] = value.trim();
});
}
}
if (!shortcode.params.position || shortcode.params.position === "static") {
shortcode.params.embed = shortcode.params.embed ?? "true";
} else if (shortcode.params.embed === undefined) {
shortcode.params.embed = "false";
}
let html = ' {
if (key === "songId") {
html += ` data-song-id="${value}"`;
} else if (key === "playlistId") {
html += ` data-playlist-id="${value}"`;
} else {
const dataKey =
{
position: "data-position",
theme: "data-theme",
lyric: "data-lyric",
embed: "data-embed",
minimized: "data-default-minimized",
autoplay: "data-autoplay",
"idle-opacity": "data-idle-opacity",
"auto-pause": "data-auto-pause",
}[key] || `data-${key}`;
html += ` ${dataKey}="${value}"`;
}
});
html += ">
";
return html;
});
},
};
}
console.log(
[
"版本号 v2.1.0",
"NeteaseMiniPlayer V2 [NMPv2]",
"BHCN STUDIO & 北海的佰川(ImBHCN[numakkiyu])",
"GitHub地址:https://github.com/numakkiyu/NeteaseMiniPlayer",
"基于 Apache 2.0 开源协议发布",
].join("\n")
);