// ==UserScript== // @name Spotify Web - Download Cover // @name:es Spotify Web - Descargar portada // @name:pt Spotify Web - Baixar capa // @name:it Spotify Web - Scarica copertina // @name:fr Spotify Web - Télécharger pochette // @name:de Spotify Web - Cover herunterladen // @name:ru Spotify Web - Скачать обложку // @name:zh Spotify Web - 下载封面 // @name:ja Spotify Web - カバーをダウンロード // @namespace http://tampermonkey.net/ // @description Adds a button to download the full size cover art from Spotify Web Player // @description:es Agrega un botón para descargar la portada en tamaño completo del reproductor web de Spotify // @description:pt Adiciona um botão para baixar a capa em tamanho completo do reproductor web do Spotify // @description:it Aggiunge un pulsante per scaricare la copertina a dimensione piena dal lettore web di Spotify // @description:fr Ajoute un bouton pour télécharger la pochette en taille réelle depuis le lecteur web Spotify // @description:de Fügt eine Schaltfläche zum Herunterladen des Covers in voller Größe vom Spotify Web Player hinzu // @description:ru Добавляет кнопку для скачивания обложки в полном размере из веб-плеера Spotify // @description:zh 为Spotify网页播放器添加下载全尺寸封面艺术的按钮 // @description:ja Spotify Web Playerからフルサイズのカバーアートをダウンロードするボタンを追加します // @version 1.4 // @match https://open.spotify.com/* // @author Levi Somerset // @license MIT // @icon https://www.google.com/s2/favicons?sz=64&domain=spotify.com // @grant GM_download // @grant GM_xmlhttpRequest // @connect i.scdn.co // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js // @downloadURL none // ==/UserScript== /* global saveAs */ (function() { 'use strict'; const CONFIG = { COVER_BASE: 'https://i.scdn.co/image/', SIZES: ['ab67616d0000b273', 'ab67616d00001e02', 'ab67616d00004851'], FULL_SIZE: 'ab67616d000082c1' }; const translations = { en: ['Download Cover', 'Cover downloaded: %s', 'Failed to download cover'], es: ['Descargar portada', 'Portada descargada: %s', 'No se pudo descargar la portada'], pt: ['Baixar capa', 'Capa baixada: %s', 'Falha ao baixar a capa'], it: ['Scarica copertina', 'Copertina scaricata: %s', 'Impossibile scaricare la copertina'], fr: ['Télécharger pochette', 'Pochette téléchargée: %s', 'Échec du téléchargement de la pochette'], de: ['Cover herunterladen', 'Cover heruntergeladen: %s', 'Cover konnte nicht heruntergeladen werden'], ru: ['Скачать обложку', 'Обложка скачана: %s', 'Не удалось скачать обложку'], zh: ['下载封面', '封面已下载: %s', '下载封面失败'], ja: ['カバーをダウンロード', 'カバーがダウンロードされました: %s', 'カバーのダウンロードに失敗しました'] }; let [buttonText, downloadedText, errorText] = translations.en; // Set language based on browser settings const lang = navigator.language.split('-')[0]; if (lang in translations) { [buttonText, downloadedText, errorText] = translations[lang]; } const style = document.createElement('style'); style.innerText = ` .cover-download-btn { width: 32px; height: 32px; border-radius: 50%; border: 0; background-color: #1fdf64; background-image: url('data:image/svg+xml;utf8,download-image'); background-size: 20px 20px; background-position: center; background-repeat: no-repeat; cursor: pointer; margin-left: 10px; } .cover-download-btn:hover { transform: scale(1.1); } `; document.body.appendChild(style); function downloadImage(imageSrc, fileName) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: imageSrc, responseType: 'blob', onload: function(response) { try { var blob = response.response; saveAs(blob, fileName); showInfo(downloadedText.replace('%s', fileName), 'success'); resolve(); } catch (error) { console.error('Error saving file:', error); reject(error); } }, onerror: function(error) { console.error('Error downloading image:', error); reject(error); } }); }); } const albumInfoCache = new Map(); function getAlbumInfo() { const cacheKey = window.location.href; if (albumInfoCache.has(cacheKey)) { return albumInfoCache.get(cacheKey); } let artistName, albumName; // Desktop version const desktopArtistElements = document.querySelectorAll('div.Fb61sprjhh75aOITDnsJ a[data-testid="creator-link"]'); if (desktopArtistElements.length > 0) { const artistNames = Array.from(desktopArtistElements).map(el => el.textContent.trim()); artistName = artistNames.join(', '); albumName = document.querySelector('h1.encore-text.encore-text-headline-large.encore-internal-color-text-base[data-encore-id="text"]')?.textContent; } // Mobile version else { artistName = document.querySelector('.b81TNrTkVyPCOH0aDdLG a')?.textContent; albumName = document.querySelector('.gj6rSoF7K4FohS2DJDEm')?.textContent; } const info = { artist: artistName || 'Unknown Artist', album: albumName ? albumName.trim() : 'Unknown Album' }; albumInfoCache.set(cacheKey, info); return info; } function getFullSizeCoverUrl() { const images = document.querySelectorAll('img'); let coverElement = null; for (const img of images) { if (img.src.includes(CONFIG.COVER_BASE) && (!coverElement || img.width > coverElement.width)) { coverElement = img; } } if (!coverElement) { console.log('Cover element not found'); return null; } const coverUrl = coverElement.src; for (const size of CONFIG.SIZES) { if (coverUrl.includes(size)) { return coverUrl.replace(size, CONFIG.FULL_SIZE); } } console.log('Failed to extract cover hash'); return null; } function debounce(func, delay) { let timeoutId; return function (...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), delay); }; } const debouncedAddDownloadButton = debounce(addDownloadButton, CONFIG.DEBOUNCE_DELAY); function addDownloadButton() { let actionBarRow = document.querySelector('[data-testid="action-bar-row"]'); // If desktop version's action bar is not found, try mobile version if (!actionBarRow) { actionBarRow = document.querySelector('.jXbmfyIkvfBoDgVxAaDD'); } if (actionBarRow && !actionBarRow.querySelector('.cover-download-btn')) { const downloadButton = document.createElement('button'); downloadButton.classList.add('cover-download-btn'); downloadButton.title = buttonText; downloadButton.addEventListener('click', async function() { const fullSizeCoverUrl = getFullSizeCoverUrl(); if (fullSizeCoverUrl) { try { const albumInfo = getAlbumInfo(); const fileName = `${albumInfo.artist} - ${albumInfo.album}.jpg`; await downloadImage(fullSizeCoverUrl, fileName); } catch (error) { showInfo(errorText, 'error'); } } else { showInfo(errorText, 'error'); } }); actionBarRow.appendChild(downloadButton); } } function showInfo(str, type = 'success') { const infoDiv = document.createElement('div'); infoDiv.style.cssText = ` position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background-color: ${type === 'success' ? '#1db954' : '#ff4444'}; color: white; padding: 10px 20px; border-radius: 5px; z-index: 9999; `; infoDiv.textContent = str; document.body.appendChild(infoDiv); setTimeout(() => infoDiv.remove(), 3000); } function init() { const observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.type === 'childList') { debouncedAddDownloadButton(); } }); }); observer.observe(document.body, { childList: true, subtree: true }); } init(); })();