您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Draggable red-finished mod menu with playback, volume, shuffle, repeat, like, lyrics toggle, playback speed, captions, hide/show and more for Spotify Web Player https://open.spotify.com/ .
// ==UserScript== // @name Spotify Web Player Mod Menu // @namespace http://tampermonkey.net/ // @version 1.5 // @description Draggable red-finished mod menu with playback, volume, shuffle, repeat, like, lyrics toggle, playback speed, captions, hide/show and more for Spotify Web Player https://open.spotify.com/ . // @author Marley // @match https://open.spotify.com/* // @grant none // ==/UserScript== (function() { 'use strict'; // -- Styles for the mod menu -- const style = document.createElement('style'); style.textContent = ` #spotifyModMenu { position: fixed; top: 100px; left: 20px; width: 280px; max-height: 420px; background: #111; border: 2px solid #b22222; border-radius: 10px; color: #eee; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: 13px; box-shadow: 0 0 12px #b22222aa; z-index: 9999999; display: flex; flex-direction: column; user-select: none; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #b22222 #222; } #spotifyModMenu::-webkit-scrollbar { width: 8px; } #spotifyModMenu::-webkit-scrollbar-track { background: #222; border-radius: 10px; } #spotifyModMenu::-webkit-scrollbar-thumb { background-color: #b22222; border-radius: 10px; } #spotifyModMenu header { background: #b22222; padding: 8px 10px; font-weight: bold; font-size: 16px; cursor: move; border-top-left-radius: 8px; border-top-right-radius: 8px; color: #fff; user-select: none; position: relative; display: flex; justify-content: space-between; align-items: center; } #spotifyModMenu header .header-buttons { display: flex; gap: 8px; } #spotifyModMenu button, #spotifyModMenu input[type=range] { margin: 6px 10px; padding: 6px 10px; background: #222; border: 1.5px solid #b22222; border-radius: 6px; color: #eee; cursor: pointer; transition: background 0.3s ease; font-size: 14px; user-select: none; } #spotifyModMenu button:hover { background: #b22222; color: white; } #spotifyModMenu input[type=range] { -webkit-appearance: none; width: 100%; height: 6px; background: #222; cursor: pointer; user-select: none; } #spotifyModMenu input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; background: #b22222; cursor: pointer; border-radius: 50%; border: none; margin-top: -5px; } #spotifyModMenu input[type=range]::-moz-range-thumb { width: 16px; height: 16px; background: #b22222; cursor: pointer; border-radius: 50%; border: none; } #spotifyModMenu .track-info { padding: 8px 12px; font-size: 13px; color: #eee; min-height: 48px; display: flex; flex-direction: column; justify-content: center; border-top: 1.5px solid #b22222; border-bottom: 1.5px solid #b22222; user-select: text; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #spotifyModMenu .playback-time { font-size: 12px; color: #ccc; padding: 0 10px 6px; user-select: none; } #spotifyModMenu .btn-row { display: flex; flex-wrap: wrap; justify-content: space-between; padding: 6px 10px 10px; gap: 6px; } #spotifyModMenu .btn-row button { flex: 1 1 45%; } #spotifyModMenu .status-row { font-size: 12px; color: #bbb; padding: 0 12px 8px; user-select: none; display: flex; justify-content: space-between; } #spotifyModMenu .toggle-btn { background: none; border: none; color: #eee; font-size: 18px; cursor: pointer; padding: 0 6px; user-select: none; } #spotifyModMenu.light { background: #eee; color: #222; border-color: #b22222; box-shadow: 0 0 12px #b22222aa; } #spotifyModMenu.light header { background: #b22222; color: white; } #spotifyModMenu.light button { background: #f0f0f0; color: #222; border-color: #b22222; } #spotifyModMenu.light button:hover { background: #b22222; color: white; } #spotifyModMenu.light input[type=range] { background: #ccc; } #spotifyModMenu.light input[type=range]::-webkit-slider-thumb { background: #b22222; } #spotifyModMenu.light input[type=range]::-moz-range-thumb { background: #b22222; } `; document.head.appendChild(style); // -- Create UI -- const menu = document.createElement('div'); menu.id = 'spotifyModMenu'; menu.innerHTML = ` <header> Spotify Mod Menu <div class="header-buttons"> <button title="Hide Menu" id="btnHideMenu" class="toggle-btn">👁</button> <button title="Close Menu" id="btnCloseMenu" class="toggle-btn">✕</button> </div> </header> <div class="track-info" title="Track — Artist">Loading track info...</div> <div class="playback-time">00:00 / 00:00</div> <div class="status-row"> <div id="shuffleStatus">Shuffle: Off</div> <div id="repeatStatus">Repeat: Off</div> <div id="speedStatus">Speed: 1x</div> </div> <div class="btn-row"> <button id="btnPlayPause">Play / Pause ▶️⏸️</button> <button id="btnPrev">Previous ⏮️</button> <button id="btnNext">Next ⏭️</button> <button id="btnShuffle">Toggle Shuffle ♻️</button> <button id="btnRepeat">Cycle Repeat 🔁</button> <button id="btnLike">Like ❤️</button> <button id="btnUnlike">Unlike 💔</button> <button id="btnMute">Mute 🔇</button> <button id="btnToggleLyrics">Toggle Lyrics 📝</button> <button id="btnToggleDevices">Toggle Devices 📱</button> <button id="btnRestartTrack">Restart Track 🔄</button> <button id="btnToggleSpeed">Toggle Speed 1x/1.5x ⚡</button> <button id="btnToggleCaptions">Toggle Captions 🗨️</button> </div> <div style="padding: 0 10px 10px;"> <label for="volRange" style="font-size:12px;">Volume:</label> <input type="range" id="volRange" min="0" max="100" value="50" /> </div> <div style="padding: 0 10px 10px;"> <button id="btnDarkMode">Toggle Light/Dark Mode</button> </div> `; document.body.appendChild(menu); // -- Dragging logic -- let isDragging = false, dragOffsetX = 0, dragOffsetY = 0; const header = menu.querySelector('header'); header.addEventListener('mousedown', (e) => { // Avoid dragging when clicking buttons if (e.target.closest('button')) return; isDragging = true; dragOffsetX = e.clientX - menu.offsetLeft; dragOffsetY = e.clientY - menu.offsetTop; document.body.style.userSelect = 'none'; }); document.addEventListener('mouseup', () => { isDragging = false; document.body.style.userSelect = 'auto'; }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; let x = e.clientX - dragOffsetX; let y = e.clientY - dragOffsetY; // Keep menu inside viewport const maxX = window.innerWidth - menu.offsetWidth; const maxY = window.innerHeight - menu.offsetHeight; if (x < 0) x = 0; else if (x > maxX) x = maxX; if (y < 0) y = 0; else if (y > maxY) y = maxY; menu.style.left = x + 'px'; menu.style.top = y + 'px'; }); // -- Buttons -- const btnCloseMenu = menu.querySelector('#btnCloseMenu'); const btnHideMenu = menu.querySelector('#btnHideMenu'); btnCloseMenu.addEventListener('click', () => { menu.remove(); }); btnHideMenu.addEventListener('click', () => { if(menu.style.display !== 'none') { menu.style.display = 'none'; // Add a small fixed show button so user can bring it back addShowButton(); } }); function addShowButton() { if(document.querySelector('#btnShowMenu')) return; // Already exists const btnShow = document.createElement('button'); btnShow.id = 'btnShowMenu'; btnShow.textContent = 'Show Mod Menu'; Object.assign(btnShow.style, { position: 'fixed', top: '20px', left: '20px', zIndex: '99999999', padding: '8px 12px', fontSize: '14px', backgroundColor: '#b22222', color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer', boxShadow: '0 0 12px #b22222aa', userSelect: 'none', }); document.body.appendChild(btnShow); btnShow.addEventListener('click', () => { menu.style.display = 'flex'; btnShow.remove(); }); } // -- Spotify Web Player DOM selectors & helpers -- function getSpotifyButton(ariaLabel) { return document.querySelector(`button[aria-label="${ariaLabel}"]`); } function getPlayPauseBtn() { return getSpotifyButton('Play') || getSpotifyButton('Pause'); } function getNextBtn() { return getSpotifyButton('Next'); } function getPrevBtn() { return getSpotifyButton('Previous'); } function getShuffleBtn() { return getSpotifyButton('Shuffle'); } function getRepeatBtn() { // Repeat button aria-label cycles through "Repeat Off", "Repeat All", "Repeat One" // Sometimes it's "Repeat" return getSpotifyButton('Repeat') || getSpotifyButton('Repeat Off') || getSpotifyButton('Repeat All') || getSpotifyButton('Repeat One'); } function getLikeBtn() { return document.querySelector('button[aria-label="Save to Your Library"], button[aria-label="Remove from Your Library"]'); } function getVolumeSlider() { return document.querySelector('input[type="range"][aria-label="Volume"]'); } // Lyrics panel toggle function getLyricsBtn() { return getSpotifyButton('Lyrics'); } // Devices panel toggle function getDevicesBtn() { return getSpotifyButton('Connect to a device'); } // Audio element to get playback info and control seeking function getAudioElement() { return document.querySelector('audio'); } // Captions / Subtitles button - this might be tricky, try to find a button related to captions or subtitles in the UI function getCaptionsBtn() { // Spotify captions might be in a button with aria-label "Captions" or "Closed captions" return getSpotifyButton('Captions') || getSpotifyButton('Closed captions') || getSpotifyButton('Subtitles'); } // -- Playback control functions -- function playPause() { const btn = getPlayPauseBtn(); if (btn) btn.click(); } function nextTrack() { const btn = getNextBtn(); if (btn) btn.click(); } function prevTrack() { const btn = getPrevBtn(); if (btn) btn.click(); } function toggleShuffle() { const btn = getShuffleBtn(); if (btn) btn.click(); } function cycleRepeat() { const btn = getRepeatBtn(); if (btn) btn.click(); } function toggleLike() { const btn = getLikeBtn(); if (btn && btn.getAttribute('aria-label') === 'Save to Your Library') btn.click(); } function toggleUnlike() { const btn = getLikeBtn(); if (btn && btn.getAttribute('aria-label') === 'Remove from Your Library') btn.click(); } function muteToggle() { const volSlider = getVolumeSlider(); if (!volSlider) return; if (volSlider.value > 0) { volSlider.dataset.lastVolume = volSlider.value; volSlider.value = 0; } else { volSlider.value = volSlider.dataset.lastVolume || 50; } triggerVolumeEvents(volSlider); } function setVolume(value) { const volSlider = getVolumeSlider(); if (!volSlider) return; volSlider.value = value; triggerVolumeEvents(volSlider); } // Helper to trigger input and change events for volume slider reliably function triggerVolumeEvents(elem) { elem.dispatchEvent(new Event('input', { bubbles: true })); elem.dispatchEvent(new Event('change', { bubbles: true })); } function toggleLyrics() { const btn = getLyricsBtn(); if (btn) btn.click(); } function toggleDevices() { const btn = getDevicesBtn(); if (btn) btn.click(); } function restartTrack() { const audio = getAudioElement(); if (audio) audio.currentTime = 0; } // Playback speed toggle (1x or 1.5x) let currentSpeed = 1; function toggleSpeed() { const audio = getAudioElement(); if (!audio) return; if (currentSpeed === 1) currentSpeed = 1.5; else currentSpeed = 1; audio.playbackRate = currentSpeed; } // Toggle captions/subtitles function toggleCaptions() { const btn = getCaptionsBtn(); if (btn) btn.click(); else alert('Captions/Subtitles button not found or not available'); } // -- Update track info and playback time & status -- const trackInfoDiv = menu.querySelector('.track-info'); const playbackTimeDiv = menu.querySelector('.playback-time'); const shuffleStatusDiv = menu.querySelector('#shuffleStatus'); const repeatStatusDiv = menu.querySelector('#repeatStatus'); const speedStatusDiv = menu.querySelector('#speedStatus'); function formatTime(sec) { if (isNaN(sec) || sec === Infinity) return '00:00'; const m = Math.floor(sec / 60); const s = Math.floor(sec % 60); return `${m < 10 ? '0' + m : m}:${s < 10 ? '0' + s : s}`; } function updateStatuses() { // Shuffle status const shuffleBtn = getShuffleBtn(); if (shuffleBtn) { const ariaPressed = shuffleBtn.getAttribute('aria-pressed'); shuffleStatusDiv.textContent = 'Shuffle: ' + (ariaPressed === 'true' ? 'On' : 'Off'); } else { shuffleStatusDiv.textContent = 'Shuffle: N/A'; } // Repeat status - check aria-label or aria-pressed const repeatBtn = getRepeatBtn(); if (repeatBtn) { let repeatText = 'Repeat: Off'; const label = repeatBtn.getAttribute('aria-label'); if (label) { if (label.toLowerCase().includes('off')) repeatText = 'Repeat: Off'; else if (label.toLowerCase().includes('all')) repeatText = 'Repeat: All'; else if (label.toLowerCase().includes('one')) repeatText = 'Repeat: One'; } repeatStatusDiv.textContent = repeatText; } else { repeatStatusDiv.textContent = 'Repeat: N/A'; } // Speed status speedStatusDiv.textContent = `Speed: ${currentSpeed}x`; } function updateTrackInfo() { const trackName = document.querySelector('.Root__now-playing-bar .track-info__name a')?.textContent?.trim(); const artistName = document.querySelector('.Root__now-playing-bar .track-info__artists a')?.textContent?.trim(); if (trackName && artistName) { trackInfoDiv.textContent = `${trackName} — ${artistName}`; } else { trackInfoDiv.textContent = 'No track playing'; } const audio = getAudioElement(); if (audio) { playbackTimeDiv.textContent = `${formatTime(audio.currentTime)} / ${formatTime(audio.duration)}`; } else { playbackTimeDiv.textContent = '00:00 / 00:00'; } updateStatuses(); } // -- Volume slider control -- const volRange = menu.querySelector('#volRange'); volRange.addEventListener('input', (e) => { setVolume(e.target.value); }); // Sync slider with actual volume changes from Spotify function syncVolumeSlider() { const volSlider = getVolumeSlider(); if (!volSlider) return; volRange.value = volSlider.value; } // -- Button events -- menu.querySelector('#btnPlayPause').addEventListener('click', () => { playPause(); }); menu.querySelector('#btnNext').addEventListener('click', () => { nextTrack(); }); menu.querySelector('#btnPrev').addEventListener('click', () => { prevTrack(); }); menu.querySelector('#btnShuffle').addEventListener('click', () => { toggleShuffle(); setTimeout(updateStatuses, 500); }); menu.querySelector('#btnRepeat').addEventListener('click', () => { cycleRepeat(); setTimeout(updateStatuses, 500); }); menu.querySelector('#btnLike').addEventListener('click', () => { toggleLike(); }); menu.querySelector('#btnUnlike').addEventListener('click', () => { toggleUnlike(); }); menu.querySelector('#btnMute').addEventListener('click', () => { muteToggle(); }); menu.querySelector('#btnToggleLyrics').addEventListener('click', () => { toggleLyrics(); }); menu.querySelector('#btnToggleDevices').addEventListener('click', () => { toggleDevices(); }); menu.querySelector('#btnRestartTrack').addEventListener('click', () => { restartTrack(); }); menu.querySelector('#btnToggleSpeed').addEventListener('click', () => { toggleSpeed(); updateStatuses(); }); menu.querySelector('#btnToggleCaptions').addEventListener('click', () => { toggleCaptions(); }); // Dark/Light mode toggle menu.querySelector('#btnDarkMode').addEventListener('click', () => { if(menu.classList.contains('light')) { menu.classList.remove('light'); } else { menu.classList.add('light'); } }); // -- Periodic updates -- setInterval(() => { updateTrackInfo(); syncVolumeSlider(); }, 1000); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址