YouTube Music Spinning Album Art

Spins album artwork with grooves, shine, and spindle. Syncs with play/pause state on YouTube Music.

// ==UserScript==
// @name         YouTube Music Spinning Album Art
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  Spins album artwork with grooves, shine, and spindle. Syncs with play/pause state on YouTube Music.
// @author       Chesley
// @match        https://music.youtube.com/*
// @license      MIT
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const styleId = 'spinning-cropped-art-style';
    let initialized = false;
    let lastSrc = null;

    const addStyles = () => {
        if (document.getElementById(styleId)) return;

        const style = document.createElement('style');
        style.id = styleId;
        style.textContent = `
            @keyframes spinAlbum {
                from { transform: rotate(0deg); }
                to { transform: rotate(360deg); }
            }

            @keyframes shineMove {
                from { transform: rotate(0deg); }
                to { transform: rotate(360deg); }
            }

            ytmusic-player {
                background-color: transparent;
            }

            ytmusic-player #thumbnail {
                display: flex;
                align-items: center;
                justify-content: center;
                overflow: hidden;
                border-radius: 50%;
                width: 90%;
                height: 90%;
                margin: auto;
                position: relative;
                border: 2px solid rgba(255, 255, 255, 0.2);
                box-shadow: 0 0 20px rgba(255, 255, 255, 0.2);
            }


            ytmusic-player #thumbnail img {
                width: 100%;
                height: 100%;
                object-fit: cover;
                animation: spinAlbum 8s linear infinite;
                animation-play-state: running;
                border-radius: 50%;
                box-shadow: 0 0 15px rgba(255, 255, 255, 0.5);
                z-index: 2;
            }

            ytmusic-player #thumbnail.paused img,
            ytmusic-player #thumbnail.paused .shine {
                animation-play-state: paused;
            }

            ytmusic-player #thumbnail .grooves {
                position: absolute;
                width: 100%;
                height: 100%;
                border-radius: 50%;
                background: repeating-radial-gradient(
                    circle,
                    rgba(255, 255, 255, 0.1),
                    rgba(255, 255, 255, 0.1) 15px,
                    rgba(0, 0, 0, 0.3) 15.5px,
                    rgba(0, 0, 0, 0) 18px
                );
                pointer-events: none;
                z-index: 3;
            }

            /* 🔥 Updated Shine Style */
            ytmusic-player #thumbnail .shine {
                position: absolute;
                width: 100%;
                height: 100%;
                border-radius: 50%;
                background: linear-gradient(
                    120deg,
                    rgba(255, 255, 255, 0.50) 0%,
                    rgba(255, 255, 255, 0.1) 50%,
                    rgba(255, 255, 255, 0) 80%
                );
                animation: shineMove 6s linear infinite;
                pointer-events: none;
                z-index: 100;
            }

            ytmusic-player #thumbnail::after {
                content: "";
                position: absolute;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                width: 28px;
                height: 28px;
                background: radial-gradient(circle, #444 30%, #000 100%);
                border-radius: 50%;
                z-index: 5;
                pointer-events: none;
            }
        `;
        document.head.appendChild(style);
    };

    const applyDecorations = () => {
        const thumbnail = document.querySelector('ytmusic-player #thumbnail');
        const img = thumbnail?.querySelector('img');

        if (!img) return;

        const currentSrc = img.src;
        if (currentSrc === lastSrc) return;
        lastSrc = currentSrc;

        if (!thumbnail.querySelector('.grooves')) {
            const grooves = document.createElement('div');
            grooves.className = 'grooves';
            thumbnail.appendChild(grooves);
        }

        if (!thumbnail.querySelector('.shine')) {
            const shine = document.createElement('div');
            shine.className = 'shine';
            thumbnail.appendChild(shine);
        }
    };

    const syncWithPlayback = () => {
        const video = document.querySelector('video');
        const thumbnail = document.querySelector('ytmusic-player #thumbnail');
        if (!video || !thumbnail) return;

        const updateState = () => {
            if (video.paused) {
                thumbnail.classList.add('paused');
            } else {
                thumbnail.classList.remove('paused');
            }
        };

        video.addEventListener('play', updateState);
        video.addEventListener('pause', updateState);
        updateState();
    };

    const watchSongChanges = () => {
        const player = document.querySelector('ytmusic-player');
        if (!player) return;

        const songObserver = new MutationObserver(() => {
            applyDecorations();
            syncWithPlayback();
        });

        songObserver.observe(player, {
            childList: true,
            subtree: true,
        });
    };

    const init = () => {
        const img = document.querySelector('ytmusic-player #thumbnail img');
        if (!img) return;

        if (!initialized) {
            addStyles();
            initialized = true;
        }

        applyDecorations();
        syncWithPlayback();
        watchSongChanges();
    };

    const bootstrap = new MutationObserver(() => {
        const ready = document.querySelector('ytmusic-player #thumbnail img');
        if (ready) {
            init();
            bootstrap.disconnect();
        }
    });

    bootstrap.observe(document.body, {
        childList: true,
        subtree: true
    });
})();