Greasy Fork is available in English.

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

// ==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);

})();
OSZAR »