/** * [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; 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 }; } 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 ? `` : ''}
0:00
0:00
${ICONS.volume}
${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') { document.addEventListener('visibilitychange', () => { if (this.config.autoPauseDisabled === true) { return; } 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 baseUrl = 'https://api.hypcvgm.top/NeteaseMiniPlayer/nmp.php'; const queryString = new URLSearchParams(params).toString(); const url = `${baseUrl}${endpoint}${queryString ? '?' + queryString : ''}`; try { const response = await fetch(url); const data = await response.json(); if (data.code !== 200) { throw new Error(`API错误: ${data.code}`); } return data; } catch (error) { console.error('API请求失败:', error); throw error; } } 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('/playlist/track/all', { id: playlistId, limit: 1000, offset: 0 }); tracks = response.songs; this.setCache(cacheKey, tracks); } this.playlist = tracks.map(song => ({ id: song.id, name: song.name, artists: song.ar.map(ar => ar.name).join(', '), album: song.al.name, picUrl: song.al.picUrl, duration: song.dt })); this.updatePlaylistDisplay(); } async loadSingleSong(songId) { const cacheKey = this.getCacheKey('song', songId); let songData = this.getCache(cacheKey); if (!songData) { try { const response = await this.apiRequest('/song/detail', { ids: songId }); if (response.songs && response.songs.length > 0) { const song = response.songs[0]; songData = { id: song.id, name: song.name, artists: song.ar.map(ar => ar.name).join(', '), album: song.al.name, picUrl: song.al.picUrl, duration: song.dt }; this.setCache(cacheKey, songData); } else { throw new Error('歌曲信息获取失败'); } } catch (error) { console.error('获取歌曲详情失败:', error); songData = { id: songId, name: '歌曲加载失败', artists: '未知艺术家', album: '未知专辑', picUrl: '', duration: 0 }; } } 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.id); if (this.showLyrics) { await this.loadLyrics(song.id); } } 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(songId) { const cacheKey = this.getCacheKey('song_url', songId); let urlData = this.getCache(cacheKey); if (!urlData) { try { const response = await this.apiRequest('/song/url/v1', { id: songId, level: 'exhigh' }); if (response.data && response.data.length > 0) { urlData = response.data[0]; this.setCache(cacheKey, urlData, 30 * 60 * 1000); } } catch (error) { console.error('获取音频URL失败:', error); try { const fallbackResponse = await this.apiRequest('/song/url/v1', { id: songId, level: 'standard' }); if (fallbackResponse.data && fallbackResponse.data.length > 0) { urlData = fallbackResponse.data[0]; } } catch (fallbackError) { console.error('降级获取音频URL也失败:', fallbackError); } } } if (urlData && urlData.url) { const httpsUrl = this.ensureHttps(urlData.url); console.log('设置音频源:', httpsUrl); this.audio.src = httpsUrl; } else { throw new Error('无法获取播放地址'); } } 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(songId) { const cacheKey = this.getCacheKey('lyric', songId); let lyricData = this.getCache(cacheKey); if (!lyricData) { try { const response = await this.apiRequest('/lyric', { id: songId }); lyricData = response; this.setCache(cacheKey, lyricData, 60 * 60 * 1000); } catch (error) { console.error('获取歌词失败:', error); 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; } const lrcLines = lyricData.lrc.lyric.split('\n'); const tlyricLines = lyricData.tlyric && lyricData.tlyric.lyric ? lyricData.tlyric.lyric.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"));