您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
为Spotify网页播放器添加下载全尺寸封面艺术的按钮
// ==UserScript== // @name Spotify Web - Download Cover // @name:es Spotify Web - Descargar portada // @name:pt Spotify Web - Descarregar capa // @name:pt-BR 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-CN 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 descarregar a capa em tamanho completo do reproductor web do Spotify // @description:pt-BR Adiciona um botão para baixar a capa em tamanho real do Spotify Web Player // @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-CN 为Spotify网页播放器添加下载全尺寸封面艺术的按钮 // @description:ja Spotify Web Playerからフルサイズのカバーアートをダウンロードするボタンを追加します // @version 1.7.2 // @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 // @grant window.onurlchange // @connect i.scdn.co // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js // ==/UserScript== (function () { 'use strict'; const CONFIG = { COVER_BASE: 'https://i.scdn.co/image/', SIZES: ['ab67616d0000b273', 'ab67616d00001e02', 'ab67616d00004851'], FULL_SIZE: 'ab67616d000082c1', DEBOUNCE_DELAY: 300 }; 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: ['Descarregar capa', 'Capa descarregada: %s', 'Falha ao descarregar a capa'], "pt-BR": ['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-CN": ['下载封面', '封面已下载: %s', '下载封面失败'], ja: ['カバーをダウンロード', 'カバーがダウンロードされました: %s', 'カバーのダウンロードに失敗しました'] }; function initScript(saveAs) { function log(...args) { if (CONFIG.DEBUG) { console.log('[Spotify Cover Downloader]', ...args); } } function detectLanguage() { const htmlLang = document.documentElement.lang; if (htmlLang) { log('Language detected from HTML:', htmlLang); return htmlLang.split('-')[0]; } const urlMatch = window.location.pathname.match(/\/intl-([a-z]{2})\//); if (urlMatch) { log('Language detected from URL:', urlMatch[1]); return urlMatch[1]; } log('Language detected from browser:', navigator.language); return navigator.language.split('-')[0]; } let language = detectLanguage(); let [buttonText, downloadedText, errorText] = translations.en; if (language in translations) { [buttonText, downloadedText, errorText] = translations[language]; log('Using translations for language:', language); } else { log('No translations for language:', language, 'using English'); } const style = document.createElement('style'); style.innerText = ` .cover-download-btn { background-color: transparent; border: none; color: #b3b3b3; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; margin-left: 0px; padding: 0px; position: relative; text-align: center; touch-action: manipulation; transition-duration: 33ms; transition-property: background-color, border-color, color, box-shadow, filter, transform; transition-timing-function: cubic-bezier(.3,0,.4,1); user-select: none; vertical-align: middle; } .cover-download-btn:hover { color: #ffffff; transform: scale(1.04); } .cover-download-btn:active { transform: scale(1); } .cover-download-btn svg { fill: currentColor; } `; document.head.appendChild(style); function downloadImage(imageSrc, fileName) { return new Promise((resolve, reject) => { log('Downloading image:', imageSrc, 'as', fileName); GM.xmlHttpRequest({ method: 'GET', url: imageSrc, responseType: 'blob', onload: function (response) { try { const blob = response.response; saveAs(blob, fileName); showInfo(downloadedText.replace('%s', fileName), 'success'); log('Download successful'); resolve(); } catch (error) { console.error('Error saving file:', error); log('Error saving file:', error); reject(error); } }, onerror: function (error) { console.error('Error downloading image:', error); log('Error downloading image:', error); reject(error); } }); }); } const albumInfoCache = new Map(); function getAlbumInfo() { const cacheKey = window.location.href; if (albumInfoCache.has(cacheKey)) { log('Using cached album info for:', cacheKey); return albumInfoCache.get(cacheKey); } let artistName, albumName; const artistElements = document.querySelectorAll('a[data-testid="creator-link"]'); if (artistElements.length > 0) { const artistNames = Array.from(artistElements).map(el => el.textContent.trim()); artistName = artistNames.join(', '); log('Found artists:', artistName); } const albumTitleSelectors = [ 'span[data-testid="entityTitle"] h1[data-encore-id="text"]', 'h1[data-encore-id="text"]', '.gj6rSoF7K4FohS2DJDEm', 'span[data-testid="entityTitle"]', '.rEN7ncpaUeSGL9z0NGQR h1' ]; for (const selector of albumTitleSelectors) { const element = document.querySelector(selector); if (element) { albumName = element.textContent.trim(); log('Found album name using selector:', selector, ':', albumName); break; } } if (!artistName) { const artistFallbackSelectors = [ '.b81TNrTkVyPCOH0aDdLG a', '.rEN7ncpaUeSGL9z0NGQR', '.RANLXG3qKB61Bh33I0r2 a' ]; for (const selector of artistFallbackSelectors) { const element = document.querySelector(selector); if (element) { artistName = element.textContent.trim(); log('Found artist using fallback selector:', selector, ':', artistName); break; } } } const info = { artist: artistName || 'Unknown Artist', album: albumName || 'Unknown Album' }; log('Final album info:', info); albumInfoCache.set(cacheKey, info); return info; } function findAlbumCover() { const selectors = [ 'section[data-testid="album-page"] img[srcset*="i.scdn.co/image/"][sizes]', 'section[data-testid="album-page"] img[src*="i.scdn.co/image/"]', 'button[class*="_osiFNXU9Cy1X0CYaU9Z"] img[src*="i.scdn.co/image/"]', '.CmkY1Ag0tJDfnFXbGgju img[src*="i.scdn.co/image/"]', 'img[srcset*="i.scdn.co/image/"][sizes]', 'img[src*="i.scdn.co/image/"]' ]; for (const selector of selectors) { const element = document.querySelector(selector); if (element && element.width >= 100) { log('Found album cover using selector:', selector); return element; } } log('Album cover not found'); return null; } function getFullSizeCoverUrl() { const coverElement = findAlbumCover(); if (!coverElement) { log('Cover element not found'); return null; } const coverUrl = coverElement.src; for (const size of CONFIG.SIZES) { if (coverUrl.includes(size)) { const fullSizeUrl = coverUrl.replace(size, CONFIG.FULL_SIZE); log('Generated full size URL:', fullSizeUrl); return fullSizeUrl; } } 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); }; } function shouldShowButton() { const isAlbumPage = window.location.pathname.includes('/album/'); log('Is album page:', isAlbumPage, 'Path:', window.location.pathname); return isAlbumPage; } function findActionBarRow() { const selectors = [ '[data-testid="action-bar-row"]', '.eSg4ntPU2KQLfpLGXAww', '.action-bar-row' ]; for (const selector of selectors) { const element = document.querySelector(selector); if (element) { log('Found action bar using selector:', selector); return element; } } log('Action bar not found, trying alternative approach'); const moreButton = document.querySelector('[data-testid="more-button"]'); if (moreButton) { log('Found more button, using its parent as action bar'); return moreButton.parentNode; } log('Action bar not found'); return null; } function createDownloadButton() { const button = document.createElement('button'); button.className = 'Button-sc-1dqy6lx-0 iwlsM e-9890-overflow-wrap-anywhere e-9890-button-tertiary--icon-only e-9890-button-tertiary--condensed cover-download-btn'; button.setAttribute('data-encore-id', 'buttonTertiary'); button.setAttribute('title', buttonText); button.setAttribute('aria-label', buttonText); button.innerHTML = ` <span aria-hidden="true" class="e-9890-button__icon-wrapper"> <svg data-encore-id="icon" role="img" aria-hidden="true" class="e-9890-icon e-9890-baseline" viewBox="0 0 24 24" style="--encore-icon-height: var(--encore-graphic-size-decorative-larger); --encore-icon-width: var(--encore-graphic-size-decorative-larger);"> <path d="M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM1 12C1 5.925 5.925 1 12 1s11 4.925 11 11-4.925 11-11 11S1 18.075 1 12z"></path> <path d="M12 6.05a1 1 0 0 1 1 1v7.486l1.793-1.793a1 1 0 1 1 1.414 1.414L12 18.364l-4.207-4.207a1 1 0 1 1 1.414-1.414L11 14.536V7.05a1 1 0 0 1 1-1z"></path> </svg> </span> `; return button; } function addDownloadButton() { if (!shouldShowButton()) return; let actionBarRow = findActionBarRow(); if (!actionBarRow) return; if (actionBarRow.querySelector('.cover-download-btn')) return; const moreButton = actionBarRow.querySelector('[data-testid="more-button"]'); if (!moreButton) { log('More button not found, adding at the end of action bar'); const downloadButton = createDownloadButton(); actionBarRow.appendChild(downloadButton); 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'); } }); return; } log('Found more button, inserting download button after it'); const downloadButton = createDownloadButton(); moreButton.parentNode.insertBefore(downloadButton, moreButton.nextSibling); 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'); } }); } function removeDownloadButton() { const existingButton = document.querySelector('.cover-download-btn'); if (existingButton) { log('Removing existing download button'); existingButton.remove(); } } function addOrRemoveButton() { if (shouldShowButton()) { log('Adding download button'); addDownloadButton(); } else { log('Removing download button'); removeDownloadButton(); } } 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; font-family: spotify-circular, Helvetica, Arial, sans-serif; font-weight: 700; `; infoDiv.textContent = str; document.body.appendChild(infoDiv); setTimeout(() => infoDiv.remove(), 3000); } function watchForActionBar() { log('Starting aggressive DOM observation'); const observer = new MutationObserver(() => { if (shouldShowButton()) { const actionBar = findActionBarRow(); if (actionBar && !actionBar.querySelector('.cover-download-btn')) { log('Action bar found during observation, adding button'); addDownloadButton(); } } }); observer.observe(document.body, { childList: true, subtree: true }); const intervalId = setInterval(() => { if (shouldShowButton()) { log('Interval check: trying to add button'); addDownloadButton(); } else { log('Interval check: not an album page, clearing interval'); clearInterval(intervalId); } }, 1000); setTimeout(() => { log('Clearing interval after timeout'); clearInterval(intervalId); }, 10000); return observer; } const debouncedAddOrRemoveButton = debounce(addOrRemoveButton, CONFIG.DEBOUNCE_DELAY); function init() { log('Initializing script'); addOrRemoveButton(); const observer = watchForActionBar(); if ('onurlchange' in window) { log('URL change event supported, adding listener'); window.addEventListener('urlchange', (e) => { log('URL changed:', e.url); debouncedAddOrRemoveButton(); }); } const regularObserver = new MutationObserver(() => { debouncedAddOrRemoveButton(); }); regularObserver.observe(document.body, { childList: true, subtree: true }); const originalPushState = history.pushState; history.pushState = function () { log('History pushState detected'); originalPushState.apply(this, arguments); debouncedAddOrRemoveButton(); }; window.addEventListener('popstate', () => { log('Popstate event detected'); debouncedAddOrRemoveButton(); }); log('Initialization complete'); } init(); } if (typeof saveAs === 'undefined') { const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js'; script.onload = () => initScript(saveAs); document.head.appendChild(script); } else { initScript(saveAs); } })();