Spotify Web Enhancer (Full Suite)

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

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