Deez Notes

Add label, UPC, genres, release dates, and tracklist copy to Deezer album pages

Versión del día 18/10/2025. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Deez Notes
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  Add label, UPC, genres, release dates, and tracklist copy to Deezer album pages
// @author       waiter7
// @contributors ilikepeaches
// @match        https://www.deezer.com/*
// @license      MIT
// @grant        none
// ==/UserScript==
    
(function() {
    'use strict';
    
    // Debug mode - set to true to enable console logging
    const DEBUG = false;
    
    function waitForElement(selector, timeout = 10000) {
        return new Promise((resolve, reject) => {
            const element = document.querySelector(selector);
            if (element) {
                resolve(element);
                return;
            }
    
            const observer = new MutationObserver((mutations, obs) => {
                const element = document.querySelector(selector);
                if (element) {
                    obs.disconnect();
                    resolve(element);
                }
            });
    
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
    
            setTimeout(() => {
                observer.disconnect();
                reject(new Error(`Element '${selector}' not found within timeout`));
            }, timeout);
        });
    }
    
    function formatDate(dateString) {
        if (!dateString || dateString === '0000-00-00') return 'N/A';
        try {
            const date = new Date(dateString);
            return date.toLocaleDateString('en-US', {
                year: 'numeric',
                month: '2-digit',
                day: '2-digit',
                timeZone: 'UTC'
            });
        } catch (e) {
            return dateString;
        }
    }
    
    function extractAlbumId() {
        const path = window.location.pathname;
        const match = path.match(/\/album\/(\d+)/);
        return match ? match[1] : null;
    }
    
    
    
    function addTracklistCopyIcon(apiData) {
        // Find the track count element
        const trackCountElement = document.querySelector('li.dMJfv');
        if (!trackCountElement || trackCountElement.querySelector('.tracklist-copy-icon')) {
            return; // Element not found or icon already added
        }
        
        // Create clipboard icon
        const copyIcon = document.createElement('span');
        copyIcon.className = 'tracklist-copy-icon';
        copyIcon.innerHTML = '⧉';
        copyIcon.title = 'Copy Tracklist';
        copyIcon.style.cssText = `
            margin-left: 8px;
            cursor: pointer;
            opacity: 0.7;
            transition: opacity 0.2s ease;
            font-size: 14px;
            color: #666;
            background: rgba(255, 255, 255, 0.9);
            border-radius: 3px;
            padding: 2px 4px;
        `;
        
        // Add hover effect
        copyIcon.addEventListener('mouseenter', () => {
            copyIcon.style.opacity = '1';
        });
        
        copyIcon.addEventListener('mouseleave', () => {
            copyIcon.style.opacity = '0.7';
        });
        
        // Add click handler
        copyIcon.addEventListener('click', async () => {
            // Show loading state
            const originalText = copyIcon.innerHTML;
            copyIcon.innerHTML = '⏳';
            copyIcon.style.cursor = 'wait';
            
            try {
                // Format tracklist from API data
                const tracklist = apiData.tracks?.data?.length 
                    ? apiData.tracks.data.map((track, index) => {
                        const minutes = Math.floor(track.duration / 60);
                        const seconds = track.duration % 60;
                        const formattedDuration = `${minutes}:${seconds.toString().padStart(2, '0')}`;
                        return `${index + 1}. ${track.title} (${formattedDuration})`;
                    }).join('\n')
                    : 'No tracks available';
                
                await navigator.clipboard.writeText(tracklist);
                
                // Show success feedback
                copyIcon.innerHTML = '✓';
                copyIcon.style.color = '#4CAF50';
                
                setTimeout(() => {
                    copyIcon.innerHTML = originalText;
                    copyIcon.style.color = '#666';
                    copyIcon.style.cursor = 'pointer';
                }, 1500);
                
                if (DEBUG) console.log('Tracklist copied to clipboard');
            } catch (error) {
                console.error('Error copying tracklist:', error);
                
                // Show error feedback
                copyIcon.innerHTML = '✗';
                copyIcon.style.color = '#f44336';
                
                setTimeout(() => {
                    copyIcon.innerHTML = originalText;
                    copyIcon.style.color = '#666';
                    copyIcon.style.cursor = 'pointer';
                }, 1500);
            }
        });
        
        // Add the icon to the track count element
        trackCountElement.appendChild(copyIcon);
    }
    
    
    function copyToClipboard(text, element) {
        navigator.clipboard.writeText(text).then(() => {
            const copiedMsg = element.querySelector('.copied-message');
            if (copiedMsg) {
                copiedMsg.style.opacity = '1';
                copiedMsg.style.visibility = 'visible';
                
                setTimeout(() => {
                    copiedMsg.style.opacity = '0';
                    setTimeout(() => copiedMsg.style.visibility = 'hidden', 200);
                }, 1000);
            }
        }).catch(err => {
            if (DEBUG) console.error('Failed to copy text:', err);
        });
    }
    
    function createCopyableCell(content, isApiCell = false, isGenresCell = false) {
        const td = document.createElement('td');
        
        if (isApiCell) {
            // Handle API cell differently - no copy functionality
            td.style.cssText = `
                padding: 12px;
                vertical-align: top;
                background: #ffffff;
                text-align: center;
                border-top: 1px solid #dee2e6;
                border-bottom: 1px solid #dee2e6;
            `;
            
            const apiLink = document.createElement('a');
            apiLink.href = content;
            apiLink.target = '_blank';
            apiLink.textContent = 'View API';
            apiLink.style.cssText = `
                color: #a238ff; text-decoration: none; font-weight: 500;
                padding: 4px 8px; border-radius: 4px; background: #f8f5ff;
                border: 1px solid #a238ff; display: inline-block; transition: all 0.2s ease;
            `;
            
            // Add hover effects for API link
            apiLink.addEventListener('mouseenter', () => {
                apiLink.style.background = '#a238ff';
                apiLink.style.color = '#ffffff';
            });
            apiLink.addEventListener('mouseleave', () => {
                apiLink.style.background = '#f8f5ff';
                apiLink.style.color = '#a238ff';
            });
            
            td.appendChild(apiLink);
            
        } else {
            // Handle regular data cells
            td.textContent = content || 'N/A';
            td.style.cssText = `
                padding: 12px;
                border-right: 1px solid #dee2e6;
                border-top: 1px solid #dee2e6;
                border-bottom: 1px solid #dee2e6;
                vertical-align: top;
                background: #ffffff;
                position: relative;
                cursor: pointer;
                transition: background-color 0.2s ease;
            `;
            
            // Make "Requires Page Refresh" italic and add refresh functionality
            const isRefreshCell = content === 'Requires Page Refresh';
            if (isRefreshCell) {
                td.style.fontStyle = 'italic';
                td.style.color = '#666';
            }
            
            // Add copy/refresh icon
            const actionIcon = document.createElement('span');
            actionIcon.innerHTML = isRefreshCell ? '🔄' : '⧉';
            actionIcon.title = isRefreshCell ? 'Refresh Page' : 'Copy to Clipboard';
            actionIcon.style.cssText = `
                position: absolute;
                top: 8px;
                right: 8px;
                cursor: pointer;
                opacity: 0;
                transition: opacity 0.2s ease;
                font-size: 14px;
                color: #666;
                background: rgba(255, 255, 255, 0.9);
                border-radius: 3px;
                padding: 2px 4px;
            `;
            
            // Add copied message
            const copiedMessage = document.createElement('span');
            copiedMessage.className = 'copied-message';
            copiedMessage.textContent = 'Copied';
            copiedMessage.style.cssText = `
                position: absolute;
                bottom: 8px;
                right: 8px;
                color: #666;
                font-size: 10px;
                font-style: italic;
                opacity: 0;
                visibility: hidden;
                transition: opacity 0.2s ease;
                white-space: nowrap;
                z-index: 1000;
                pointer-events: none;
            `;
            
            td.appendChild(actionIcon);
            td.appendChild(copiedMessage);
            
            // Show/hide action icon on hover
            td.addEventListener('mouseenter', () => {
                actionIcon.style.opacity = '0.7';
                td.style.backgroundColor = '#f8f9fa';
            });
            
            td.addEventListener('mouseleave', () => {
                actionIcon.style.opacity = '0';
                td.style.backgroundColor = '#ffffff';
            });
            
            // Click functionality - refresh or copy
            td.addEventListener('click', () => {
                if (isRefreshCell) {
                    location.reload();
                } else {
                    let copyText = content || 'N/A';
                    if (isGenresCell && copyText !== 'N/A' && copyText !== 'Loading...') {
                        // Format genres for copying: lowercase, trim spaces around commas, replace spaces within genres with periods
                        copyText = copyText.toLowerCase()
                            .split(',')
                            .map(genre => genre.trim().replace(/\s+/g, '.'))
                            .join(',');
                    }
                    copyToClipboard(copyText, td);
                }
            });
        }
        
        return td;
    }
    
    async function addMetadata() {
        try {
            // Check if metadata table already exists
            if (document.querySelector('.custom-metadata-table')) {
                if (DEBUG) console.log('Metadata table already exists, skipping...');
                return true;
            }

            const albumId = extractAlbumId();
            if (!albumId) {
                if (DEBUG) console.log('Could not extract album ID from URL');
                return false;
            }

            // Find the play button container for positioning
            const playButtonContainer = document.querySelector('[data-testid="play"]');
            if (!playButtonContainer) {
                if (DEBUG) console.log('Play button container not found');
                return false;
            }
            const buttonStrip = playButtonContainer.parentElement.parentElement;

            // Always fetch from API first to get all available data
            if (DEBUG) console.log('Fetching album data from API...');
            let apiData;
            try {
                const response = await fetch(`https://api.deezer.com/album/${albumId}`);
                apiData = await response.json();
            } catch (error) {
                if (DEBUG) console.log('API fetch failed, will retry...');
                return false;
            }
    
            // Extract data from API
            const label = apiData.label || 'N/A';
            const upc = apiData.upc || 'N/A';
            const genres = apiData.genres?.data?.length 
                ? apiData.genres.data.map(genre => genre.name).join(', ')
                : 'N/A';
            const originalReleaseDate = formatDate(apiData.release_date);

            // Get additional date fields from app state (only works on fresh page loads)
            let digitalReleaseDate, physicalReleaseDate;
            if (isFreshPageLoad) {
                const appState = window.__DZR_APP_STATE__;
                if (appState?.DATA) {
                    if (DEBUG) console.log('Fresh page load - using app state for additional dates...');
                    digitalReleaseDate = formatDate(appState.DATA.ORIGINAL_RELEASE_DATE);
                    physicalReleaseDate = formatDate(appState.DATA.PHYSICAL_RELEASE_DATE);
                } else {
                    if (DEBUG) console.log('Fresh page load but no app state, using refresh message...');
                    digitalReleaseDate = physicalReleaseDate = 'Requires Page Refresh';
                }
            } else {
                if (DEBUG) console.log('SPA navigation detected, using refresh message for missing dates...');
                digitalReleaseDate = physicalReleaseDate = 'Requires Page Refresh';
            }

            if (DEBUG) {
                console.log('Album metadata:', {
                    label,
                    upc,
                    genres,
                    digitalReleaseDate,
                    physicalReleaseDate,
                    originalReleaseDate
                });
            }
    
            // Create a clean table for metadata
            const tableContainer = document.createElement('div');
            tableContainer.className = 'custom-metadata-table';
            tableContainer.style.cssText = `
                margin-top: 19px;
                margin-bottom: 19px;
                background: #f8f9fa;
                border-radius: 8px;
                padding: 14px;
                border: 1px solid #e9ecef;
            `;
    
            const table = document.createElement('table');
            table.style.cssText = `
                width: 100%;
                border-collapse: separate;
                border-spacing: 0;
                font-size: 13px;
                color: #495057;
            `;
    
            // Create table header
            const thead = document.createElement('thead');
            const headerRow = document.createElement('tr');
            headerRow.style.cssText = `
                border-top: 1px solid #dee2e6;
                border-bottom: 2px solid #dee2e6;
            `;
    
            const headers = ['Label', 'UPC', 'Genres', 'Digital Release', 'Physical Release', 'Original Release', 'API'];
            headers.forEach((headerText, index) => {
                const th = document.createElement('th');
                th.textContent = headerText;
                th.style.cssText = `
                    text-align: left;
                    padding: 8px 12px;
                    font-weight: 600;
                    color: #343a40;
                    background: #e9ecef;
                    border-right: 1px solid #dee2e6;
                    border-top: 1px solid #dee2e6;
                    border-bottom: 1px solid #dee2e6;
                `;
                if (index === 0) th.style.borderLeft = '1px solid #dee2e6';
                if (headerText === 'API') th.style.borderRight = '1px solid #dee2e6';
                headerRow.appendChild(th);
            });
            thead.appendChild(headerRow);
            table.appendChild(thead);
    
            // Create table body
            const tbody = document.createElement('tbody');
            const dataRow = document.createElement('tr');
            dataRow.style.cssText = `
                background-color: #ffffff;
                border-bottom: 1px solid #dee2e6;
            `;
    
            const values = [label, upc, genres, digitalReleaseDate, physicalReleaseDate, originalReleaseDate];
    
            // Add data cells with copy functionality
            values.forEach((value, index) => {
                const isGenresCell = index === 2; // genres is the 3rd item (index 2)
                const td = createCopyableCell(value, false, isGenresCell);
                if (index === 0) td.style.borderLeft = '1px solid #dee2e6';
                dataRow.appendChild(td);
            });
    
            // Add API link cell with copy functionality
            const apiUrl = `https://api.deezer.com/album/${albumId}`;
            const apiTd = createCopyableCell(apiUrl, true);
            apiTd.style.borderRight = '1px solid #dee2e6';
            dataRow.appendChild(apiTd);
    
            tbody.appendChild(dataRow);
            table.appendChild(tbody);
            tableContainer.appendChild(table);
    
            // Insert the table after the button strip
            buttonStrip.parentNode.insertBefore(tableContainer, buttonStrip.nextSibling);
            
            // Add tracklist copy icon (uses the same apiData we already fetched)
            addTracklistCopyIcon(apiData);
    
            if (DEBUG) console.log('Metadata added successfully');
            return true;
    
        } catch (error) {
            console.error('Error adding metadata:', error);
            return false;
        }
    }
    
    function init() {
        if (DEBUG) console.log('Deez Notes: Starting...');
        
        // Check if we're on an album page
        if (!location.href.includes('/album/')) {
            if (DEBUG) console.log('Not on an album page, skipping...');
            return;
        }
    
        // Wait for the page to load and the play button to be available
        waitForElement('[data-testid="play"]')
            .then(() => {
                if (DEBUG) console.log('Play button found, attempting to add custom metadata...');
    
                // Try to add metadata with multiple retries for SPA navigation
                let attempts = 0;
                const maxAttempts = 5;
                
                async function tryAddMetadata() {
                    attempts++;
                    try {
                        const success = await addMetadata();
                        if (success) {
                            if (DEBUG) console.log('Metadata added successfully on attempt', attempts);
                            return;
                        }
                    } catch (error) {
                        if (DEBUG) console.log('Error in addMetadata:', error);
                    }
                    
                    if (attempts < maxAttempts) {
                        if (DEBUG) console.log(`Attempt ${attempts} failed, retrying in ${attempts}s...`);
                        setTimeout(tryAddMetadata, attempts * 1000); // 1s, 2s, 3s, 4s delays
                    } else {
                        if (DEBUG) console.log('Max attempts reached, giving up');
                    }
                }
                
                tryAddMetadata();
            })
            .catch(error => {
                console.error('Failed to find play button:', error);
            });
    }
    
    // Enhanced navigation detection for SPA
        let lastUrl = location.href;
    if (DEBUG) console.log('Initial URL:', lastUrl);
    
    // Track if this is a fresh page load or SPA navigation
    let isFreshPageLoad = true;
    
    // Simple navigation detection like the working old code
    if (DEBUG) console.log('Setting up MutationObserver...');
        new MutationObserver(() => {
        // Only check URL on mutations, don't log every trigger
            const url = location.href;
            if (url !== lastUrl) {
            if (DEBUG) console.log(`Navigation detected: ${lastUrl} → ${url}`);
                lastUrl = url;
            
                if (url.includes('/album/')) {
                 if (DEBUG) console.log('Album page detected, initializing...');
                 // Mark as SPA navigation for subsequent runs
                 isFreshPageLoad = false;
                 // Use the same approach as the working old code
                    setTimeout(init, 1000);
             } else {
                 if (DEBUG) console.log('Not an album page, skipping...');
            }
    }
    }).observe(document.body, { 
        subtree: true, 
        childList: true 
    });
    
    // Start when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();