// ==UserScript== // @name Lucida Downloader // @description Download music from Spotify, Qobuz, Tidal, Soundcloud, Deezer, Amazon Music and Yandex Music via Lucida. Adds a draggable floating button that opens in Lucida for downloading. // @icon https://lucida.to/favicon.png // @version 1.1 // @author afkarxyz // @namespace https://github.com/afkarxyz/misc-scripts/ // @supportURL https://github.com/afkarxyz/misc-scripts/issues // @license MIT // @match https://open.spotify.com/* // @match https://listen.tidal.com/* // @match https://music.yandex.com/* // @match https://music.amazon.com/* // @match https://www.deezer.com/* // @match https://soundcloud.com/* // @match https://www.qobuz.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @downloadURL none // ==/UserScript== (function() { 'use strict'; const DOMAINS = ['lucida.to', 'lucida.su']; const SERVICES = ['spotify', 'qobuz', 'tidal', 'soundcloud', 'deezer', 'amazon']; const LOGO_SVG = ``; const getPreferences = () => ({ targetService: GM_getValue('targetService', ''), domainPreference: GM_getValue('domainPreference', 'random') }); const setupMenuCommands = () => { GM_registerMenuCommand('🎯 Resolve to a different service?', () => { const currentService = GM_getValue('targetService', ''); const serviceList = ['none', ...SERVICES]; const optionsText = serviceList.map((service, index) => `${index}. ${service === 'none' ? 'No redirect' : service}` ).join('\n'); const choice = prompt( `Select service to redirect to (0-${serviceList.length - 1}):\n${optionsText}`, serviceList.indexOf(currentService) ); if (choice !== null && !isNaN(choice) && choice >= 0 && choice < serviceList.length) { const selectedService = serviceList[choice]; GM_setValue('targetService', selectedService === 'none' ? '' : selectedService); alert(`Service redirect set to: ${selectedService === 'none' ? 'Disabled' : selectedService}`); } }); GM_registerMenuCommand('🌐 Set Domain Preference', () => { const currentPref = GM_getValue('domainPreference', 'random'); const options = ['random', 'lucida.to', 'lucida.su']; const optionsText = options.map((opt, index) => `${index}. ${opt}`).join('\n'); const choice = prompt( `Select domain preference (0-${options.length - 1}):\n${optionsText}`, options.indexOf(currentPref) ); if (choice !== null && !isNaN(choice) && choice >= 0 && choice < options.length) { GM_setValue('domainPreference', options[choice]); alert(`Domain preference set to: ${options[choice]}`); } }); }; const style = document.createElement('style'); style.textContent = ` .floating-button { position: fixed; width: 80px; height: 80px; background-color: transparent; border-radius: 50%; display: flex; justify-content: center; align-items: center; cursor: move; z-index: 9999; opacity: 0.3; transition: opacity 0.3s ease; border: none; } .floating-button:hover { opacity: 1; } .floating-button svg { width: 48px; height: auto; cursor: pointer; } `; document.head.appendChild(style); const button = document.createElement('button'); button.className = 'floating-button'; button.innerHTML = LOGO_SVG; const savedPosition = { left: GM_getValue('buttonLeft', '20'), top: GM_getValue('buttonTop', '20') }; button.style.left = savedPosition.left + 'px'; button.style.top = savedPosition.top + 'px'; let isDragging = false; let startX, startY; button.addEventListener('mousedown', e => { if (e.target.tagName.toLowerCase() !== 'svg') { isDragging = true; startX = e.clientX - button.offsetLeft; startY = e.clientY - button.offsetTop; } }); document.addEventListener('mousemove', e => { if (!isDragging) return; let left = e.clientX - startX; let top = e.clientY - startY; left = Math.max(0, Math.min(window.innerWidth - button.offsetWidth, left)); top = Math.max(0, Math.min(window.innerHeight - button.offsetHeight, top)); button.style.left = left + 'px'; button.style.top = top + 'px'; }); document.addEventListener('mouseup', () => { if (!isDragging) return; isDragging = false; const SNAP = 20; const rect = button.getBoundingClientRect(); if (rect.left < SNAP) button.style.left = '0px'; if (rect.top < SNAP) button.style.top = '0px'; if (window.innerWidth - rect.right < SNAP) button.style.left = (window.innerWidth - rect.width) + 'px'; if (window.innerHeight - rect.bottom < SNAP) button.style.top = (window.innerHeight - rect.height) + 'px'; GM_setValue('buttonLeft', button.style.left.replace('px', '')); GM_setValue('buttonTop', button.style.top.replace('px', '')); }); button.addEventListener('click', e => { if (e.target.closest('svg')) { const currentUrl = encodeURIComponent(window.location.href); const prefs = getPreferences(); let domain; if (prefs.domainPreference === 'random') { domain = DOMAINS[Math.floor(Math.random() * DOMAINS.length)]; } else { domain = prefs.domainPreference; } let url = `https://${domain}/?url=${currentUrl}&country=auto`; if (prefs.targetService) { url += `&to=${prefs.targetService}`; } window.open(url, '_blank'); } }); document.body.appendChild(button); setupMenuCommands(); })();