NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Spotify Enhancer (Full-Sized Cover Art Downloader) // @description Integrates an overlay button in Spotify Web Player to view and download full-sized (2000px) album cover art. // @icon https://raw.githubusercontent.com/exyezed/spotify-enhancer/refs/heads/main/extras/spotify-enhancer.png // @version 1.4 // @author exyezed // @namespace https://github.com/exyezed/spotify-enhancer/ // @supportURL https://github.com/exyezed/spotify-enhancer/issues // @license MIT // @match *://open.spotify.com/* // @grant GM_addStyle // @require https://cdn.jsdelivr.net/npm/@iconify/iconify@3.1.1/dist/iconify.min.js // ==/UserScript== (function() { 'use strict'; const styles = ` .preview-modal-title { color: white; font-size: 16px; margin-bottom: 16px; text-align: center; max-width: 90vw; word-wrap: break-word; } .custom-overlay-button { position: absolute; top: 10px; right: 10px; z-index: 9999 !important; background: rgba(0, 0, 0, 0.7); border: none; border-radius: 50%; width: 30px; height: 30px; cursor: pointer; display: flex; align-items: center; justify-content: center; color: white; transition: all 0.3s ease; pointer-events: all !important; } .custom-overlay-button:hover { background: rgba(0, 0, 0, 0.9); } .custom-overlay-button svg { width: 18px; height: 18px; opacity: 1; transition: opacity 0.3s ease; } .custom-overlay-button .icon-normal { position: absolute; } .custom-overlay-button .icon-hover { position: absolute; opacity: 0; color: #1ed760; } .custom-overlay-button:hover .icon-normal { opacity: 0; } .custom-overlay-button:hover .icon-hover { opacity: 1; } .custom-overlay-button * { pointer-events: none; } .preview-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.85); display: flex; justify-content: center; align-items: center; z-index: 10000; opacity: 0; visibility: hidden; transition: opacity 0.3s ease, visibility 0.3s ease; } .preview-modal.active { opacity: 1; visibility: visible; } .preview-modal-content { position: relative; max-width: 90vw; max-height: 90vh; display: flex; flex-direction: column; align-items: center; transform: scale(0.95); transition: transform 0.3s ease; } .preview-modal.active .preview-modal-content { transform: scale(1); } .preview-modal-content.loading .preview-modal-close, .preview-modal-content.loading .preview-actions { opacity: 0; visibility: hidden; } .preview-modal-content .preview-modal-close, .preview-modal-content .preview-actions { opacity: 1; visibility: visible; transition: opacity 0.3s ease, visibility 0.3s ease; } .preview-modal img { max-width: 100%; max-height: 80vh; object-fit: contain; cursor: pointer; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); } .preview-modal-close { position: absolute; top: -40px; right: 0; background: none; border: none; color: white; cursor: pointer; padding: 8px; display: flex; align-items: center; justify-content: center; opacity: 0.8; transition: all 0.3s ease; } .preview-modal-close:hover { opacity: 1; color: #f3727f; } .preview-actions { margin-top: 16px; } .fetch-button { position: relative; background: none; border: none; color: white; cursor: pointer; padding: 8px; display: flex; align-items: center; justify-content: center; transition: all 0.3s ease; gap: 6px; } .fetch-button:hover { opacity: 1; color: #1ed760; } .fetch-button svg { width: 18px !important; height: 18px !important; } .fetch-button span { font-size: 14px; font-weight: 600; text-transform: uppercase; } `; GM_addStyle(styles); const modal = document.createElement('div'); modal.className = 'preview-modal'; document.body.appendChild(modal); async function downloadImage(url, filename) { try { const response = await fetch(url); const blob = await response.blob(); const blobUrl = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = blobUrl; link.download = filename + '.jpg'; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(blobUrl); } catch (error) { console.error('Download failed:', error); } } function createModal(imageUrl, title) { const content = document.createElement('div'); content.className = 'preview-modal-content loading'; const titleDiv = document.createElement('div'); titleDiv.className = 'preview-modal-title'; const closeButton = document.createElement('button'); closeButton.className = 'preview-modal-close'; closeButton.innerHTML = '<span class="iconify" data-icon="pajamas:close-xs" data-width="24" data-height="24"></span>'; closeButton.onclick = (e) => { e.stopPropagation(); modal.classList.remove('active'); }; const img = document.createElement('img'); img.onload = () => { content.classList.remove('loading'); // Update title with image dimensions after load titleDiv.textContent = `${title} (${img.naturalWidth} x ${img.naturalHeight})`; if (window.Iconify) { window.Iconify.scan(content); } }; img.src = imageUrl; img.alt = title; img.onclick = () => { window.open(imageUrl, '_blank'); }; const actionsDiv = document.createElement('div'); actionsDiv.className = 'preview-actions'; const fetchButton = document.createElement('button'); fetchButton.className = 'fetch-button'; fetchButton.innerHTML = '<span class="iconify" data-icon="mynaui:fat-arrow-down-solid" data-width="18" data-height="18"></span> <span>DOWNLOAD</span>'; fetchButton.onclick = (e) => { e.stopPropagation(); downloadImage(imageUrl, title || 'spotify-cover'); }; actionsDiv.appendChild(fetchButton); content.appendChild(closeButton); content.appendChild(titleDiv); content.appendChild(img); content.appendChild(actionsDiv); modal.innerHTML = ''; modal.appendChild(content); modal.classList.add('active'); modal.onclick = (e) => { if (e.target === modal) { modal.classList.remove('active'); } }; document.addEventListener('keydown', function(e) { if (e.key === 'Escape') { modal.classList.remove('active'); } }); } // Rest of the code remains the same... function getTitleFromElement(element) { const playButton = element.querySelector('button[aria-label^="Play"]'); if (playButton) { const ariaLabel = playButton.getAttribute('aria-label'); if (ariaLabel) { return ariaLabel.replace('Play ', ''); } } const entityTitle = element.querySelector('[data-testid="entityTitle"] h1'); if (entityTitle) { return entityTitle.textContent.trim(); } const img = element.querySelector('img[alt]:not([alt=""])'); if (img && img.alt) { return img.alt; } return 'spotify-cover'; } function getFullsizeUrl(originalUrl) { const resolutionPatterns = { 'ab67616d00004851': 'ab67616d000082c1', 'ab67616d0000b273': 'ab67616d000082c1', 'ab67616d00001e02': 'ab67616d000082c1', 'ab67616100005174': 'ab6761610000e5eb', 'ab6761610000f174': 'ab6761610000e5eb', 'ab676161000051748': 'ab6761610000e5eb', 'ab67616100000000': 'ab6761610000e5eb' }; let newUrl = originalUrl; for (const [pattern, replacement] of Object.entries(resolutionPatterns)) { if (originalUrl.includes(pattern)) { newUrl = originalUrl.replace(pattern, replacement); break; } } return newUrl; } function addOverlayButton(cardElement) { if (cardElement && !cardElement.querySelector('.custom-overlay-button')) { const imgElement = cardElement.querySelector('img'); if (!imgElement) return; const button = document.createElement('button'); button.className = 'custom-overlay-button'; button.setAttribute('tabindex', '0'); const iconNormal = document.createElement('span'); iconNormal.className = 'iconify icon-normal'; iconNormal.setAttribute('data-icon', 'mdi:image-size-select-large'); iconNormal.setAttribute('data-width', '18'); iconNormal.setAttribute('data-height', '18'); const iconHover = document.createElement('span'); iconHover.className = 'iconify icon-hover'; iconHover.setAttribute('data-icon', 'mdi:image-size-select-actual'); iconHover.setAttribute('data-width', '18'); iconHover.setAttribute('data-height', '18'); button.appendChild(iconNormal); button.appendChild(iconHover); const handleClick = (e) => { e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation(); const title = getTitleFromElement(cardElement); const fullsizeUrl = getFullsizeUrl(imgElement.src); createModal(fullsizeUrl, title); return false; }; button.addEventListener('click', handleClick, true); button.addEventListener('mousedown', (e) => e.stopPropagation(), true); button.addEventListener('mouseup', (e) => e.stopPropagation(), true); button.addEventListener('touchstart', (e) => e.stopPropagation(), true); button.addEventListener('touchend', (e) => e.stopPropagation(), true); const buttonWrapper = document.createElement('div'); buttonWrapper.style.cssText = ` position: absolute; top: 0; right: 0; z-index: 9999; pointer-events: none; padding: 5px; `; buttonWrapper.appendChild(button); cardElement.style.position = 'relative'; cardElement.appendChild(buttonWrapper); } } const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === 1) { const cards = [ ...Array.from(node.classList?.contains('xBV4XgMq0gC5lQICFWY_') ? [node] : node.querySelectorAll('.xBV4XgMq0gC5lQICFWY_')), ...Array.from(node.classList?.contains('CmkY1Ag0tJDfnFXbGgju') ? [node] : node.querySelectorAll('.CmkY1Ag0tJDfnFXbGgju')) ]; cards.forEach(addOverlayButton); } }); }); }); function initialize() { const existingCards = document.querySelectorAll('.xBV4XgMq0gC5lQICFWY_, .CmkY1Ag0tJDfnFXbGgju'); existingCards.forEach(addOverlayButton); observer.observe(document.body, { childList: true, subtree: true }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initialize); } else { initialize(); } })();