Spotify Web Enhancer (Full Suite)

All-in-one enhancement: UI tweaks, draggable menu, playlist tools, device detection, lyrics, ratings & more!

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         Spotify Web Enhancer (Full Suite)
// @namespace    Eliminater74
// @version      1.2.0
// @description  All-in-one enhancement: UI tweaks, draggable menu, playlist tools, device detection, lyrics, ratings & more!
// @author       Eliminater74
// @license      MIT
// @match        https://open.spotify.com/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function () {
    'use strict';

    const save = (k, v) => GM_setValue(k, v);
    const load = (k, d) => GM_getValue(k, d);

    const settings = {
        compactMode: load('compactMode', false),
        autoResume: load('autoResume', true),
        lyricsToggle: load('lyricsToggle', true),
        wideMode: load('wideMode', false),
        ratings: JSON.parse(load('ratings', '{}')),
        iconX: load('iconX', '20px'),
        iconY: load('iconY', '20px')
    };

    const css = `
#swe-gear {
    position:fixed;
    width:42px;height:42px;
    border-radius:50%;
    background:#1db954;color:#fff;
    display:flex;align-items:center;justify-content:center;
    font-size:22px;font-weight:bold;
    box-shadow:0 0 10px #000;cursor:grab;
    z-index:99999;
    left:${settings.iconX};top:${settings.iconY};
    user-select:none;
}
#swe-menu {
    position:fixed;
    background:#181818;color:white;
    border-radius:10px;
    padding:12px;font-size:13px;
    z-index:99998;top:70px;left:20px;
    display:none;box-shadow:0 0 10px black;
    max-width:250px
}
#swe-menu label {display:block;margin:5px 0;}
#swe-lyrics {
    position:fixed;bottom:10px;right:10px;
    width:400px;height:300px;
    resize:both;overflow:auto;
    z-index:99997;
    border:2px solid #555;
    display:none;
}
.swe-rating-bar {
    display:inline-block;margin-left:5px;
}
.swe-star {cursor:pointer;color:gray;font-size:16px;}
.swe-star.active {color:gold;}
body.swe-wide .Root__main-view-wrapper {
    max-width: 100% !important;
}`;

    GM_addStyle(css);

    const gear = document.createElement('div');
    gear.id = 'swe-gear';
    gear.innerText = '⚙️';
    document.body.appendChild(gear);

    const menu = document.createElement('div');
    menu.id = 'swe-menu';
    menu.innerHTML = `
    <b>Spotify Enhancer</b><br>
    <label><input type="checkbox" id="swe-compact"> Compact Mode</label>
    <label><input type="checkbox" id="swe-resume"> Auto Resume</label>
    <label><input type="checkbox" id="swe-lyrics"> Lyrics Panel</label>
    <label><input type="checkbox" id="swe-wide"> Wide Mode</label>
    <button id="swe-export">Export Playlist</button>
    <button id="swe-dupes">Remove Duplicates</button>
    <div id="swe-device">Device: Unknown</div>
    `;
    document.body.appendChild(menu);

    const lyrics = document.createElement('iframe');
    lyrics.id = 'swe-lyrics';
    document.body.appendChild(lyrics);

    gear.addEventListener('click', () => menu.style.display = menu.style.display === 'none' ? 'block' : 'none');

    // Draggable gear icon
    gear.onmousedown = function (e) {
        gear.style.cursor = 'grabbing';
        let shiftX = e.clientX - gear.getBoundingClientRect().left;
        let shiftY = e.clientY - gear.getBoundingClientRect().top;

        function moveAt(pageX, pageY) {
            gear.style.left = pageX - shiftX + 'px';
            gear.style.top = pageY - shiftY + 'px';
            save('iconX', gear.style.left);
            save('iconY', gear.style.top);
        }
        function onMouseMove(e) {
            moveAt(e.pageX, e.pageY);
        }
        document.addEventListener('mousemove', onMouseMove);
        document.onmouseup = function () {
            document.removeEventListener('mousemove', onMouseMove);
            gear.onmouseup = null;
            gear.style.cursor = 'grab';
        };
    };

    // Sync checkboxes
    document.getElementById('swe-compact').checked = settings.compactMode;
    document.getElementById('swe-resume').checked = settings.autoResume;
    document.getElementById('swe-lyrics').checked = settings.lyricsToggle;
    document.getElementById('swe-wide').checked = settings.wideMode;

    document.getElementById('swe-compact').onchange = () => { save('compactMode', event.target.checked); location.reload(); };
    document.getElementById('swe-resume').onchange = () => save('autoResume', event.target.checked);
    document.getElementById('swe-lyrics').onchange = () => {
        save('lyricsToggle', event.target.checked);
        lyrics.style.display = event.target.checked ? 'block' : 'none';
    };
    document.getElementById('swe-wide').onchange = () => {
        save('wideMode', event.target.checked);
        document.body.classList.toggle('swe-wide', event.target.checked);
    };

    // Wide mode init
    if (settings.wideMode) document.body.classList.add('swe-wide');

    // Lyrics toggle
    if (settings.lyricsToggle) lyrics.style.display = 'block';

    // Lyrics iframe loader
    function loadLyrics() {
        const track = document.querySelector('[data-testid="nowplaying-track-link"]')?.textContent;
        if (track) lyrics.src = `https://www.musixmatch.com/search/${encodeURIComponent(track)}`;
    }
    setInterval(loadLyrics, 15000);

    // Auto-resume logic
    if (settings.autoResume && location.pathname === '/') {
        const lastUri = load('lastUri', '');
        if (lastUri) setTimeout(() => location.href = lastUri, 1500);
    }
    setInterval(() => {
        const uri = window.location.pathname.includes('/track/') ? location.href : '';
        if (uri) save('lastUri', uri);
    }, 5000);

    // Export Playlist
    document.getElementById('swe-export').onclick = () => {
        const rows = [...document.querySelectorAll('[data-testid="tracklist-row"]')];
        const lines = rows.map(row => {
            const title = row.querySelector('div span a')?.textContent || 'Unknown';
            const artist = row.querySelector('span[class*="artists-albums"]')?.textContent || 'Unknown';
            return `${title} - ${artist}`;
        });
        const blob = new Blob([lines.join('\n')], { type: 'text/plain' });
        const a = document.createElement('a');
        a.href = URL.createObjectURL(blob);
        a.download = 'playlist.txt';
        a.click();
    };

    // Remove Duplicates
    document.getElementById('swe-dupes').onclick = () => {
        const map = new Map();
        document.querySelectorAll('[data-testid="tracklist-row"]').forEach(row => {
            const title = row.querySelector('div span a')?.textContent;
            const artist = row.querySelector('span[class*="artists-albums"]')?.textContent;
            const key = `${title}-${artist}`;
            if (map.has(key)) row.style.display = 'none';
            else map.set(key, true);
        });
        alert('Duplicates removed.');
    };

    // Keyboard controls
    document.addEventListener('keydown', e => {
        if (e.target.tagName.match(/INPUT|TEXTAREA/)) return;
        const play = document.querySelector('[data-testid="control-button-playpause"]');
        const next = document.querySelector('[data-testid="control-button-skip-forward"]');
        const prev = document.querySelector('[data-testid="control-button-skip-back"]');
        const heart = document.querySelector('[data-testid="control-button-heart"]');
        switch (e.key.toLowerCase()) {
            case ' ': play?.click(); e.preventDefault(); break;
            case 'arrowright': next?.click(); break;
            case 'arrowleft': prev?.click(); break;
            case 'l': heart?.click(); break;
        }
    });

    // Device Detector
    function updateDeviceName() {
        const txt = document.querySelector('[data-testid="device-picker"]')?.innerText;
        if (txt) document.getElementById('swe-device').innerText = 'Device: ' + txt.split('\n')[0];
    }
    setInterval(updateDeviceName, 10000);
})();