Spotify Web Player Mod Menu

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/ .

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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">&#128065;</button>
        <button title="Close Menu" id="btnCloseMenu" class="toggle-btn">&#10005;</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);

})();