您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Download music from Spotify, Qobuz, Tidal, SoundCloud & Amazon Music via Lucida.
// ==UserScript== // @name Lucida Downloader // @description Download music from Spotify, Qobuz, Tidal, SoundCloud & Amazon Music via Lucida. // @icon https://raw.githubusercontent.com/afkarxyz/userscripts/refs/heads/main/assets/lucida/lucida.png // @version 2.5 // @author afkarxyz // @namespace https://github.com/afkarxyz/userscripts/ // @supportURL https://github.com/afkarxyz/userscripts/issues // @license MIT // @match https://open.spotify.com/* // @match https://listen.tidal.com/* // @match https://music.amazon.com/* // @match https://soundcloud.com/* // @match https://www.qobuz.com/* // @match https://lucida.to/* // @match https://lucida.su/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_xmlhttpRequest // ==/UserScript== (function() { 'use strict'; const DOMAINS = ['lucida.to', 'lucida.su']; const BASE_URL = 'https://raw.githubusercontent.com/afkarxyz/userscripts/refs/heads/main/assets/lucida/'; const SERVICES = { 'tidal': { name: 'Tidal', icon: `${BASE_URL}tidal.png` }, 'soundcloud': { name: 'Soundcloud', icon: `${BASE_URL}soundcloud.png` }, 'deezer': { name: 'Deezer', icon: `${BASE_URL}deezer.png` }, 'amazon': { name: 'Amazon Music', icon: `${BASE_URL}amazon.png` } }; const SERVICE_STATUS = { tidal: false, soundcloud: false, deezer: false, amazon: false }; const fontLink = document.createElement('link'); fontLink.rel = 'stylesheet'; fontLink.href = 'https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap'; document.head.appendChild(fontLink); GM_addStyle(` .floating-settings-button { position: fixed; top: 10%; right: 0; width: 2.5rem; height: 2.5rem; border-radius: 1.25rem 0 0 1.25rem; background: rgba(60, 60, 60, 0.9); border: none; cursor: pointer; display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 9999; transition: all 0.3s ease; font-family: 'Open Sans', sans-serif; } .floating-settings-button .lucida-icon { width: 1.75rem; height: 1.75rem; object-fit: contain; transition: all 0.3s ease; } .floating-settings-button:hover .button-menu { opacity: 1; visibility: visible; } .button-menu { position: absolute; right: 0; top: calc(100% + 1rem); display: flex; flex-direction: column; background: rgba(60, 60, 60, 0.9); border-radius: 0.75rem 0 0 0.75rem; padding: 0.5rem 0; opacity: 0; visibility: hidden; transition: all 0.3s ease; } .menu-item { width: 2.5rem; height: 2.5rem; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; color: white; font-family: 'Open Sans', sans-serif; font-size: 0.75rem; font-weight: 500; } .menu-item:hover { background: rgba(255, 255, 255, 0.1); color: #f42e8d; } .menu-item:hover svg { fill: #f42e8d; } .menu-item svg { width: 1.25rem; height: 1.25rem; fill: white; } .domain-option { width: 2.5rem; height: 2rem; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; color: white; font-family: 'Open Sans', sans-serif; font-size: 0.75rem; font-weight: 500; } .domain-option:hover { background: rgba(255, 255, 255, 0.1); color: #f42e8d; } .floating-settings-button .lucida-icon.disabled { filter: grayscale(100%); cursor: not-allowed; opacity: 0.3; } .lucida-modal-container { position: fixed; top: calc(10% - 0.25rem); right: 3.5rem; display: block; z-index: 10000; pointer-events: none; } .lucida-modal-container.show { pointer-events: auto; } .lucida-modal { background: #222; color: #fff; border-radius: 0.75rem; width: 20rem; max-width: 90vw; box-shadow: 0 0.25rem 1rem rgba(0, 0, 0, 0.3); opacity: 0; transform: translateX(2rem); transition: opacity 0.3s ease, transform 0.3s ease; pointer-events: none; overflow: hidden; font-family: 'Open Sans', sans-serif; } .lucida-modal-container.show .lucida-modal { opacity: 1; transform: translateX(0); pointer-events: auto; } .lucida-modal * { font-family: 'Open Sans', sans-serif; box-sizing: border-box; } .lucida-modal-header { display: flex; align-items: center; justify-content: space-between; padding: 1rem; border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .lucida-modal-header svg { transition: color 0.2s ease; } .lucida-modal-header svg:hover { color: #f42e8d !important; } .lucida-modal-header h2 { margin: 0; font-size: 1.125rem; font-weight: 600; color: #f42e8d; } .lucida-modal-content { padding: 1rem; } .preference-group { margin-bottom: 1.25rem; } .preference-group:last-child { margin-bottom: 0; } .lucida-modal label { display: block; margin-bottom: 0.5rem; font-weight: 500; font-size: 0.875rem; color: #ccc; } .lucida-modal select { width: 100%; padding: 0.5rem; background: #333; border: 1px solid #444; border-radius: 0.25rem; color: #fff; font-size: 0.875rem; appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23ccc' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: calc(100% - 0.75rem) center; font-family: 'Open Sans', sans-serif; cursor: pointer; transition: border-color 0.2s ease, background-color 0.2s ease; } .lucida-modal select:hover { border-color: #f42e8d; background-color: #3a3a3a; } .lucida-modal select:focus { outline: none; border-color: #f42e8d; } .service-select-wrapper { position: relative; margin-bottom: 1rem; } .custom-select { width: 100%; padding: 0.5rem; background: #333; border: 1px solid #444; border-radius: 0.25rem; font-size: 0.875rem; color: #fff; display: flex; align-items: center; gap: 0.5rem; cursor: pointer; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23ccc' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: calc(100% - 0.75rem) center; font-family: 'Open Sans', sans-serif; transition: border-color 0.2s ease, background-color 0.2s ease; } .custom-select:hover { border-color: #f42e8d; background-color: #3a3a3a; } .custom-options { position: absolute; top: 100%; left: 0; right: 0; background: #333; border: 1px solid #444; border-radius: 0.25rem; margin-top: 0.25rem; max-height: 12.5rem; overflow-y: auto; z-index: 1000; display: none; box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.3); } .custom-options.show { display: block; } .service-status { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-left: 5px; background-color: #888; } .service-status.online { background-color: #4CAF50; } .service-status.offline { background-color: #F44336; } .service-option { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem; cursor: pointer; transition: background-color 0.2s ease; color: #fff; font-size: 0.875rem; font-family: 'Open Sans', sans-serif; } .service-option:hover { background-color: #444; } .service-option img, .custom-select img { width: 1rem; height: 1rem; object-fit: contain; } [role='grid'] { margin-left: 3.125rem; } [data-testid="tracklist-row"] { position: relative; } [role="presentation"] > * { contain: unset; } .btn { width: 2.5rem; height: 2.5rem; border-radius: 50%; border: 0; position: relative; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 0.125rem 0.3125rem rgba(0,0,0,0.2); display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #f42e8d, #b91c68); } .btn:hover { transform: scale(1.1); box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.3); } .btn .icon { width: 50%; height: 50%; background-position: center; background-repeat: no-repeat; background-size: contain; background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="%23ffffff" d="M222.2 319.2c.5 .5 1.1 .8 1.8 .8s1.4-.3 1.8-.8L350.2 187.3c1.2-1.2 1.8-2.9 1.8-4.6c0-3.7-3-6.7-6.7-6.7L288 176c-8.8 0-16-7.2-16-16l0-120c0-4.4-3.6-8-8-8l-80 0c-4.4 0-8 3.6-8 8l0 120c0 8.8-7.2 16-16 16l-57.3 0c-3.7 0-6.7 3-6.7 6.7c0 1.7 .7 3.3 1.8 4.6L222.2 319.2zM224 352c-9.5 0-18.6-3.9-25.1-10.8L74.5 209.2C67.8 202 64 192.5 64 182.7c0-21.4 17.3-38.7 38.7-38.7l41.3 0 0-104c0-22.1 17.9-40 40-40l80 0c22.1 0 40 17.9 40 40l0 104 41.3 0c21.4 0 38.7 17.3 38.7 38.7c0 9.9-3.8 19.3-10.5 26.5L249.1 341.2c-6.5 6.9-15.6 10.8-25.1 10.8zM32 336l0 96c0 26.5 21.5 48 48 48l288 0c26.5 0 48-21.5 48-48l0-96c0-8.8 7.2-16 16-16s16 7.2 16 16l0 96c0 44.2-35.8 80-80 80L80 512c-44.2 0-80-35.8-80-80l0-96c0-8.8 7.2-16 16-16s16 7.2 16 16z"/></svg>'); } [data-testid="tracklist-row"] .btn { position: absolute; top: 50%; right: 100%; margin-top: -1.25rem; margin-right: 0.625rem; } .N7GZp8IuWPJvCPz_7dOg .btn { width: 1.5rem; height: 1.5rem; transform-origin: center; position: absolute; top: 50%; right: 100%; margin-top: -0.75rem !important; margin-right: 0.625rem; } .N7GZp8IuWPJvCPz_7dOg .btn .icon { transform: scale(0.85); width: 65%; height: 65%; } `); function createServiceOption(value, service) { const option = document.createElement('div'); option.className = 'service-option'; option.dataset.value = value; if (service.icon) { const img = document.createElement('img'); img.src = service.icon; img.alt = service.name; img.style.display = 'none'; img.onload = () => { img.style.display = 'inline'; }; option.appendChild(img); } const span = document.createElement('span'); span.textContent = service.name; option.appendChild(span); if (SERVICE_STATUS.hasOwnProperty(value)) { const status = document.createElement('span'); status.className = 'service-status'; status.dataset.service = value; status.classList.add(SERVICE_STATUS[value] ? 'online' : 'offline'); option.appendChild(status); } return option; } function updateCustomSelect(customSelect, value) { const service = SERVICES[value]; let content = `<span>${service.name}</span>`; if (service.icon) { const img = new Image(); img.src = service.icon; img.style.display = 'none'; img.onload = () => { img.style.display = 'inline'; customSelect.querySelector('img')?.style.setProperty('display', 'inline'); }; content = `<img src="${service.icon}" alt="${service.name}" style="display: none;"><span>${service.name}</span>`; } if (SERVICE_STATUS.hasOwnProperty(value)) { const statusClass = SERVICE_STATUS[value] ? 'online' : 'offline'; content += `<span class="service-status ${statusClass}" data-service="${value}"></span>`; } customSelect.innerHTML = content; } function checkServiceStatus() { const domain = GM_getValue('domainPreference', 'random') === 'random' ? DOMAINS[Math.floor(Math.random() * DOMAINS.length)] : GM_getValue('domainPreference'); GM_xmlhttpRequest({ method: 'GET', url: `https://${domain}/api/stats`, onload: function(response) { try { const data = JSON.parse(response.responseText); if (data && data.all && data.all.downloads && data.all.downloads.current && data.all.downloads.current.services) { const services = data.all.downloads.current.services; SERVICE_STATUS.tidal = services.tidal > 0; SERVICE_STATUS.soundcloud = services.soundcloud > 0; SERVICE_STATUS.deezer = services.deezer > 0; SERVICE_STATUS.amazon = services.amazon > 0; updateServiceStatusIndicators(); } } catch (e) { console.error('Error parsing Lucida stats:', e); } }, onerror: function(error) { console.error('Error fetching Lucida stats:', error); } }); } function updateServiceStatusIndicators() { Object.keys(SERVICE_STATUS).forEach(service => { const indicators = document.querySelectorAll(`.service-status[data-service="${service}"]`); indicators.forEach(indicator => { indicator.classList.remove('online', 'offline'); indicator.classList.add(SERVICE_STATUS[service] ? 'online' : 'offline'); }); }); } function createFloatingButton() { const button = document.createElement('div'); button.className = 'floating-settings-button'; const lucidaIcon = document.createElement('img'); lucidaIcon.src = 'https://raw.githubusercontent.com/afkarxyz/userscripts/refs/heads/main/assets/lucida/lucida.png'; lucidaIcon.alt = 'Lucida'; lucidaIcon.className = 'lucida-icon'; const buttonMenu = document.createElement('div'); buttonMenu.className = 'button-menu'; const settingsMenuItem = document.createElement('div'); settingsMenuItem.className = 'menu-item'; settingsMenuItem.innerHTML = `<svg viewBox="0 0 24 24"><path d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z"></path></svg>`; const toOption = document.createElement('div'); toOption.className = 'domain-option'; toOption.textContent = '.to'; const suOption = document.createElement('div'); suOption.className = 'domain-option'; suOption.textContent = '.su'; buttonMenu.appendChild(settingsMenuItem); buttonMenu.appendChild(toOption); buttonMenu.appendChild(suOption); button.appendChild(lucidaIcon); button.appendChild(buttonMenu); document.body.appendChild(button); const modalContainer = document.createElement('div'); modalContainer.className = 'lucida-modal-container'; document.body.appendChild(modalContainer); lucidaIcon.addEventListener('click', (e) => { if (lucidaIcon.classList.contains('disabled')) { e.preventDefault(); e.stopPropagation(); return false; } if (!lucidaIcon.classList.contains('disabled')) { const currentUrl = window.location.href; const domain = GM_getValue('domainPreference', 'random') === 'random' ? DOMAINS[Math.floor(Math.random() * DOMAINS.length)] : GM_getValue('domainPreference'); window.open(`https://${domain}/?url=${encodeURIComponent(currentUrl)}&country=auto`, '_blank'); } e.stopPropagation(); }); settingsMenuItem.addEventListener('click', (e) => { createPreferencesModal(modalContainer); requestAnimationFrame(() => { modalContainer.classList.add('show'); }); e.preventDefault(); e.stopPropagation(); return false; }); toOption.addEventListener('click', (e) => { window.open('https://lucida.to/stats', '_blank'); e.preventDefault(); e.stopPropagation(); return false; }); suOption.addEventListener('click', (e) => { window.open('https://lucida.su/stats', '_blank'); e.preventDefault(); e.stopPropagation(); return false; }); if (window.location.hostname === 'open.spotify.com') { lucidaIcon.classList.add('disabled'); lucidaIcon.addEventListener('click', (e) => { if (lucidaIcon.classList.contains('disabled')) { e.preventDefault(); e.stopPropagation(); return false; } }, true); } if (window.location.hostname.includes('lucida.')) { button.style.display = 'none'; } document.addEventListener('click', (e) => { if (!e.target.closest('.lucida-modal') && !e.target.closest('.menu-item') && modalContainer.classList.contains('show')) { modalContainer.classList.remove('show'); } }); return button; } function createPreferencesModal(modalContainer) { modalContainer.innerHTML = ''; const modal = document.createElement('div'); modal.className = 'lucida-modal'; const modalHeader = document.createElement('div'); modalHeader.className = 'lucida-modal-header'; const modalTitle = document.createElement('h2'); modalTitle.textContent = 'Lucida Preferences'; const closeButton = document.createElement('div'); closeButton.innerHTML = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 6L6 18M6 6l12 12"></path></svg>`; closeButton.style.cursor = 'pointer'; closeButton.style.color = '#ccc'; closeButton.addEventListener('click', () => { modalContainer.classList.remove('show'); }); modalHeader.appendChild(modalTitle); modalHeader.appendChild(closeButton); const modalContent = document.createElement('div'); modalContent.className = 'lucida-modal-content'; const domainGroup = document.createElement('div'); domainGroup.className = 'preference-group'; const domainLabel = document.createElement('label'); domainLabel.textContent = 'Domain'; domainLabel.setAttribute('for', 'domain-select'); const domainSelect = document.createElement('select'); domainSelect.id = 'domain-select'; domainSelect.innerHTML = ` <option value="random">Random</option> <option value="lucida.to">Lucida.to</option> <option value="lucida.su">Lucida.su</option> `; domainGroup.appendChild(domainLabel); domainGroup.appendChild(domainSelect); const serviceGroup = document.createElement('div'); serviceGroup.className = 'preference-group'; const serviceLabel = document.createElement('label'); serviceLabel.textContent = 'Spotify Service Resolver'; const serviceSelectWrapper = document.createElement('div'); serviceSelectWrapper.className = 'service-select-wrapper'; const customSelect = document.createElement('div'); customSelect.className = 'custom-select'; customSelect.id = 'custom-service-select'; customSelect.innerHTML = '<span>Select a service</span>'; const customOptions = document.createElement('div'); customOptions.className = 'custom-options'; const serviceInput = document.createElement('input'); serviceInput.type = 'hidden'; serviceInput.id = 'service-select'; serviceSelectWrapper.appendChild(customSelect); serviceSelectWrapper.appendChild(customOptions); serviceSelectWrapper.appendChild(serviceInput); serviceGroup.appendChild(serviceLabel); serviceGroup.appendChild(serviceSelectWrapper); const formatGroup = document.createElement('div'); formatGroup.className = 'preference-group'; const formatLabel = document.createElement('label'); formatLabel.textContent = 'Download Format'; formatLabel.setAttribute('for', 'format-select'); const formatSelect = document.createElement('select'); formatSelect.id = 'format-select'; formatSelect.innerHTML = ` <option value="original">Original Format (Highest Quality)</option> <option value="flac">FLAC</option> <option value="mp3">MP3</option> <option value="ogg-vorbis">OGG Vorbis</option> <option value="opus">Opus</option> <option value="m4a-aac">M4A AAC</option> <option value="wav">WAV</option> <option value="bitcrush">Bitcrush</option> `; formatGroup.appendChild(formatLabel); formatGroup.appendChild(formatSelect); const qualityContainer = document.createElement('div'); qualityContainer.id = 'quality-settings-container'; qualityContainer.style.display = 'none'; qualityContainer.style.marginTop = '1rem'; const qualityLabel = document.createElement('label'); qualityLabel.textContent = 'Quality Settings'; qualityLabel.setAttribute('for', 'quality-select'); qualityLabel.style.marginTop = '0'; const qualitySelect = document.createElement('select'); qualitySelect.id = 'quality-select'; qualityContainer.appendChild(qualityLabel); qualityContainer.appendChild(qualitySelect); formatGroup.appendChild(qualityContainer); const autoDownloadGroup = document.createElement('div'); autoDownloadGroup.className = 'preference-group'; const autoDownloadLabel = document.createElement('label'); autoDownloadLabel.textContent = 'Auto Download'; autoDownloadLabel.setAttribute('for', 'auto-download-select'); const autoDownloadSelect = document.createElement('select'); autoDownloadSelect.id = 'auto-download-select'; autoDownloadSelect.innerHTML = ` <option value="enabled">Enabled</option> <option value="disabled">Disabled</option> `; autoDownloadGroup.appendChild(autoDownloadLabel); autoDownloadGroup.appendChild(autoDownloadSelect); modalContent.appendChild(domainGroup); modalContent.appendChild(serviceGroup); modalContent.appendChild(formatGroup); modalContent.appendChild(autoDownloadGroup); modal.appendChild(modalHeader); modal.appendChild(modalContent); modalContainer.appendChild(modal); domainSelect.value = GM_getValue('domainPreference', 'random'); formatSelect.value = GM_getValue('formatPreference', 'original'); autoDownloadSelect.value = GM_getValue('autoDownloadEnabled', 'enabled'); Object.entries(SERVICES).forEach(([value, service]) => { const option = createServiceOption(value, service); customOptions.appendChild(option); option.addEventListener('click', () => { serviceInput.value = value; GM_setValue('targetService', value); updateCustomSelect(customSelect, value); customOptions.classList.remove('show'); }); }); const savedService = GM_getValue('targetService', 'tidal'); if (SERVICES[savedService]) { updateCustomSelect(customSelect, savedService); serviceInput.value = savedService; } else { updateCustomSelect(customSelect, 'tidal'); serviceInput.value = 'tidal'; } customSelect.addEventListener('click', () => { customOptions.classList.toggle('show'); }); function updateQualityOptions(format) { qualitySelect.innerHTML = ''; switch(format) { case 'flac': qualitySelect.innerHTML = '<option value="16">16-bit 44.1kHz</option>'; qualityContainer.style.display = 'block'; GM_setValue('qualityPreference', '16'); break; case 'mp3': case 'ogg-vorbis': case 'm4a-aac': qualitySelect.innerHTML = ` <option value="320">320kb/s</option> <option value="256">256kb/s</option> <option value="192">192kb/s</option> <option value="128">128kb/s</option> `; qualityContainer.style.display = 'block'; GM_setValue('qualityPreference', '320'); break; case 'opus': qualitySelect.innerHTML = ` <option value="320">320kb/s</option> <option value="256">256kb/s</option> <option value="192">192kb/s</option> <option value="128">128kb/s</option> <option value="96">96kb/s</option> <option value="64">64kb/s</option> `; qualityContainer.style.display = 'block'; GM_setValue('qualityPreference', '320'); break; default: qualityContainer.style.display = 'none'; GM_setValue('qualityPreference', null); } } updateQualityOptions(formatSelect.value); if (qualitySelect) { qualitySelect.value = GM_getValue('qualityPreference', '320'); qualitySelect.addEventListener('change', () => { GM_setValue('qualityPreference', qualitySelect.value); }); } domainSelect.addEventListener('change', () => { GM_setValue('domainPreference', domainSelect.value); }); formatSelect.addEventListener('change', () => { GM_setValue('formatPreference', formatSelect.value); updateQualityOptions(formatSelect.value); }); autoDownloadSelect.addEventListener('change', () => { GM_setValue('autoDownloadEnabled', autoDownloadSelect.value); }); document.addEventListener('click', (e) => { if (!e.target.closest('.service-select-wrapper')) { customOptions.classList.remove('show'); } }); } function autoSelectFormat() { if (!window.location.hostname.includes('lucida.')) return; const selectFormatAndQuality = () => { const convertSelect = document.getElementById('convert'); if (!convertSelect) return; const format = GM_getValue('formatPreference', 'original'); const quality = GM_getValue('qualityPreference', '320'); convertSelect.value = format; convertSelect.dispatchEvent(new Event('change', { bubbles: true })); const observer = new MutationObserver((mutations, obs) => { const downsettingSelect = document.getElementById('downsetting'); if (downsettingSelect) { downsettingSelect.value = quality; downsettingSelect.dispatchEvent(new Event('change', { bubbles: true })); obs.disconnect(); } }); observer.observe(document.body, { childList: true, subtree: true }); }; if (document.getElementById('convert')) { selectFormatAndQuality(); } const pageObserver = new MutationObserver(() => { if (document.getElementById('convert')) { selectFormatAndQuality(); } }); pageObserver.observe(document.body, { childList: true, subtree: true }); } function autoDownload() { if (!window.location.hostname.includes('lucida.')) return; if (GM_getValue('autoDownloadEnabled', 'enabled') !== 'enabled') return; const clickDownloadButton = () => { const button = document.querySelector('.d1-track button') || document.querySelector('button[class*="download-button"]'); if (button) { button.click(); } }; const observer = new MutationObserver(() => { clickDownloadButton(); }); observer.observe(document.body, { childList: true, subtree: true }); clickDownloadButton(); } function openInLucida(trackUrl) { const currentUrl = encodeURIComponent(trackUrl || window.location.href); const prefs = getPreferences(); let domain = prefs.domainPreference === 'random' ? DOMAINS[Math.floor(Math.random() * DOMAINS.length)] : prefs.domainPreference; let url = `https://${domain}/?url=${currentUrl}&country=auto`; if (prefs.targetService) { url += `&to=${prefs.targetService}`; } window.open(url, '_blank'); } const getPreferences = () => ({ targetService: GM_getValue('targetService', 'tidal'), domainPreference: GM_getValue('domainPreference', 'random') }); function addButton(el) { const button = document.createElement('button'); button.className = 'btn'; const icon = document.createElement('div'); icon.className = 'icon'; button.appendChild(icon); el.appendChild(button); return button; } function addNowPlayingButton() { const downloadButton = document.createElement('button'); downloadButton.className = 'Lucida-Button-sc-1dqy6lx-0 dmdXQN'; downloadButton.innerHTML = '<span aria-hidden="true" class="IconWrapper__Wrapper-sc-16usrgb-0 hYdsxw"><svg data-encore-id="icon" role="img" aria-hidden="true" viewBox="0 0 448 512" class="Svg-sc-ytk21e-0 dYnaPI" width="24" height="24" fill="currentColor"><path d="M114.2 192L224 302 333.8 192 280 192c-13.3 0-24-10.7-24-24l0-120-64 0 0 120c0 13.3-10.7 24-24 24l-53.8 0zM224 352c-11.5 0-22.5-4.6-30.6-12.7L77.6 223.2C68.9 214.5 64 202.7 64 190.4c0-25.6 20.8-46.4 46.4-46.4l33.6 0 0-96c0-26.5 21.5-48 48-48l64 0c26.5 0 48 21.5 48 48l0 96 33.6 0c25.6 0 46.4 20.8 46.4 46.4c0 12.3-4.9 24.1-13.6 32.8L254.6 339.3c-8.1 8.1-19.1 12.7-30.6 12.7zM48 344l0 80c0 22.1 17.9 40 40 40l272 0c22.1 0 40-17.9 40-40l0-80c0-13.3 10.7-24 24-24s24 10.7 24 24l0 80c0 48.6-39.4 88-88 88L88 512c-48.6 0-88-39.4-88-88l0-80c0-13.3 10.7-24 24-24s24 10.7 24 24z"/></svg></span>'; downloadButton.style.cssText = 'background:transparent;border:none;color:#f42e8d;cursor:pointer;padding:8px;margin:0 4px;transition:transform .2s ease'; downloadButton.onmouseover = () => downloadButton.style.transform = 'scale(1.1)'; downloadButton.onmouseout = () => downloadButton.style.transform = 'scale(1)'; downloadButton.onclick = () => { const link = document.querySelector('a[href*="spotify:track:"]'); if (link) { const match = link.getAttribute('href').match(/spotify:track:([a-zA-Z0-9]+)/); if (match) { const trackUrl = `https://open.spotify.com/track/${match[1]}`; openInLucida(trackUrl); } } }; const container = document.querySelector('.snFK6_ei0caqvFI6As9Q')?.querySelector('.deomraqfhIAoSB3SgXpu'); if (container && !container.querySelector('.Lucida-Button-sc-1dqy6lx-0')) { container.appendChild(downloadButton); } } function animate() { const currentUrl = window.location.href; const urlParts = currentUrl.split('/'); const type = urlParts[3]; addNowPlayingButton(); if (type === 'track') { const actionBarRow = document.querySelector('.eSg4ntPU2KQLfpLGXAww[data-testid="action-bar-row"]'); if (actionBarRow && !actionBarRow.hasButtons) { const downloadButton = addButton(actionBarRow); downloadButton.onclick = function() { const spotifyId = urlParts[4].split('?')[0]; openInLucida(`https://open.spotify.com/track/${spotifyId}`); } actionBarRow.hasButtons = true; } } if (type === 'artist') { const tracks = document.querySelectorAll('[role="gridcell"]'); tracks.forEach(track => { if (!track.hasButtons) { const downloadButton = addButton(track); downloadButton.onclick = function() { const btn = track.querySelector('[data-testid="more-button"]'); if (btn) { btn.click(); setTimeout(() => { const highlightEl = document.querySelector('#context-menu a[href*="highlight"]'); if (highlightEl) { const highlight = highlightEl.href.match(/highlight=(.+)/)[1]; document.dispatchEvent(new MouseEvent('mousedown')); const spotifyId = highlight.split(':')[2]; openInLucida(`https://open.spotify.com/track/${spotifyId}`); } }, 1); } } track.hasButtons = true; } }); } if (type === 'album' || type === 'playlist' || type === 'track') { const tracks = document.querySelectorAll('[data-testid="tracklist-row"]'); tracks.forEach(track => { if (!track.hasButtons) { const downloadButton = addButton(track); downloadButton.onclick = function() { const trackLink = track.querySelector('a[href^="/track"]'); if (trackLink) { openInLucida(trackLink.href); } else { const btn = track.querySelector('[data-testid="more-button"]'); if (btn) { btn.click(); setTimeout(() => { const highlightEl = document.querySelector('#context-menu a[href*="highlight"]'); if (highlightEl) { const highlight = highlightEl.href.match(/highlight=(.+)/)[1]; document.dispatchEvent(new MouseEvent('mousedown')); const spotifyId = highlight.split(':')[2]; openInLucida(`https://open.spotify.com/track/${spotifyId}`); } }, 1); } } } track.hasButtons = true; } }); } } function animateLoop() { if (window.location.hostname === 'open.spotify.com') { animate(); } requestAnimationFrame(animateLoop); } function initialize() { const floatingButton = createFloatingButton(); requestAnimationFrame(animateLoop); autoSelectFormat(); autoDownload(); checkServiceStatus(); setInterval(checkServiceStatus, 5000); const isLucidaDomain = window.location.hostname.includes('lucida.'); if (GM_getValue('floatIconEnabled', 'enabled') === 'disabled' || isLucidaDomain) { floatingButton.style.display = 'none'; } } initialize(); })();