Greasy Fork

Greasy Fork is available in English.

Spotify Web - Download Cover

Adds a button to download the full size cover art from Spotify Web Player

当前为 2024-06-25 提交的版本,查看 最新版本

// ==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.5.1
// @match                https://open.spotify.com/album/*
// @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
// ==/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,<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120.59 122.88"><defs><style>.cls-1{fill-rule:evenodd;}</style></defs><title>download-image</title><path class="cls-1" d="M0,0H120.59V93.72H87.84V83.64H109.5V10.08H11.1V83.64H32.2V93.72H0V0ZM51.64,101.3V83.63H68.51V101.3H80.73L60.31,122.88,39.85,101.3ZM33.92,24.93a7.84,7.84,0,1,1-7.85,7.84,7.85,7.85,0,0,1,7.85-7.84Zm33,33.66L82.62,31.46,99.29,73.62H21.5V68.39L28,68.07l6.53-16,3.27,11.44h9.8l8.5-21.89,10.79,17Z"/></svg>');
            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 findAlbumCover() {
        const albumPageSection = document.querySelector('section[data-testid="album-page"]');
        if (!albumPageSection) {
            return null;
        }

        let coverElement = albumPageSection.querySelector('img[srcset*="i.scdn.co/image/"][sizes]');

        if (!coverElement) {
            const albumLinks = albumPageSection.querySelectorAll('a[href^="/album/"]');
            for(let link of albumLinks) {
                const albumName = link.textContent.trim();
                coverElement = link.closest('section').querySelector('img[srcset*="i.scdn.co/image/"][sizes]');
                if (coverElement) {
                    break;
                }
            }
        }

        if (!coverElement) {
            const modalButton = albumPageSection.querySelector('button[class*="osiFNXU9Cy1X0CYaU9Z"]');
            if (modalButton) {
                coverElement = modalButton.querySelector('img[src*="i.scdn.co/image/"]');
            }
        }

        if (coverElement && coverElement.width >= 200) {
            return coverElement;
        }

        return null;
    }

    function getFullSizeCoverUrl() {
        const coverElement = findAlbumCover();

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