Greasy Fork

Lucida Downloader

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.

目前为 2025-01-01 提交的版本。查看 最新版本

// ==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
// ==/UserScript==

(function() {
    'use strict';

    const DOMAINS = ['lucida.to', 'lucida.su'];
    const SERVICES = ['spotify', 'qobuz', 'tidal', 'soundcloud', 'deezer', 'amazon'];
    
    const LOGO_SVG = `<svg xml:space="preserve" width="48" height="28" viewBox="0 0 213.86 126.117" xmlns="http://www.w3.org/2000/svg"><g style="display:inline" transform="translate(-92.77 -153.171)"><ellipse class="st17" cx="199.7" cy="211.95" rx="103.93" ry="51" style="fill:#f42e8d;stroke:#fff;stroke-width:6;stroke-miterlimit:10"/><ellipse class="st18" cx="199.97" cy="211.95" rx="93.24" ry="41" style="fill:#f42e8d;stroke:#fff;stroke-width:3;stroke-miterlimit:10"/></g><path class="st19" style="fill:#fff;stroke:#fff;stroke-width:6;stroke-miterlimit:10" transform="translate(-92.77 -153.171)" d="M216.68 222.27v-8.79l2.1-2.21 5.4.25v10.75zM248.83 222.27v-8.79l2.1-2.21 5.4.25v10.75z"/><path class="st19" style="fill:#fff;stroke:#fff;stroke-width:6;stroke-miterlimit:10" transform="translate(-92.77 -153.171)" d="M216.68 223.56v-8.79l2.1-2.21 5.4.25v10.75zM125.12 237.48v-54.5l3.78-4.75h9.47v59.25zM139.86 204.18l3.75-3.28h7.36l-.18 16.25h9.01l.28-16.25h9.53l.25 36.58h-30zM171.18 204.4l3.65-3.5h23.6v12.19l-15.25.21v10.47l15.25-.05v13.76h-27.25zM199.97 200.9h12.5v36.59h-12.5z"/><path class="st19" style="fill:#fff;stroke:#fff;stroke-width:6;stroke-miterlimit:10" d="m214.09 204.41 4-3.51h14.44l-.25-17.14 3-5.53h9.92l.14 22.67v36.58h-31.25zM251.42 200.9h19.99l.25 4.25 3.49-4.25h6.35v36.58H247l-.25-32.08zM116.91 237.48v-54.5l3.78-4.75h9.47v59.25zM131.64 204.18l3.75-3.28h7.36l-.18 16.25h9.01l.29-16.25h9.52l.25 36.58h-30zM162.97 204.4l3.64-3.5h23.61v12.19l-15.25.21v10.47l15.25-.05v13.76h-27.25zM191.76 200.9h12.5v36.59h-12.5z" transform="translate(-92.77 -153.171)"/><path class="st19" style="fill:#fff;stroke:#fff;stroke-width:6;stroke-miterlimit:10" d="m205.88 204.41 4-3.51h14.43l-.25-17.14 3-5.53h9.93l.14 22.67v36.58h-31.25zM243.21 200.9h19.99l.24 4.25 3.5-4.25h6.34v36.58h-34.5l-.25-32.08zM162.97 204.4l3.64-3.5h23.61v12.19l-15.25.21v10.47l15.25-.05v13.76h-27.25zM131.64 204.18l3.75-3.28h7.36l-.18 16.25h9.01l.29-16.25h9.52l.25 36.58h-30z" transform="translate(-92.77 -153.171)"/><g transform="translate(-92.77 -153.171)"><path class="st19" style="fill:#fff;stroke:#fff;stroke-width:6;stroke-miterlimit:10" d="M216.68 222.27v-8.79l2.1-2.21 5.4.25v10.75zM248.83 222.27v-8.79l2.1-2.21 5.4.25v10.75z"/><path class="st19" style="fill:#fff;stroke:#fff;stroke-width:6;stroke-miterlimit:10" d="M216.68 223.56v-8.79l2.1-2.21 5.4.25v10.75z"/><circle class="st21" cx="279.8" cy="173.88" r="4.56" style="stroke:#fff;stroke-width:6;stroke-miterlimit:10"/><path class="st21" style="stroke:#fff;stroke-width:6;stroke-miterlimit:10" d="m132.88 255.72 2.83 4.71 5.1-1.26-3.34 4.17 2.83 4.71-4.9-2.13-3.35 4.17.32-5.49-4.91-2.14 5.1-1.25z"/><circle class="st21" cx="199.97" cy="236.84" r="5.62" style="stroke:#fff;stroke-width:6;stroke-miterlimit:10"/><ellipse class="st21" cx="184.25" cy="245.48" rx="3.38" ry="3.14" style="stroke:#fff;stroke-width:6;stroke-miterlimit:10"/><path class="st21" style="stroke:#fff;stroke-width:6;stroke-miterlimit:10" d="M216.68 223.77v-8.79l2.1-2.21 5.4.25v10.75zM248.83 223.77v-8.79l2.1-2.21 5.4.25v10.75zM194.48 175.23l4.06 3.92 4.72-2.6-2.21 5.03 4.06 3.92-5.43-.82-2.22 5.02-1.14-5.52-5.43-.82 4.73-2.6z"/></g><path class="st22" d="M21.66 110.899c-6.48-10.44 27.24-43.11 75.33-72.98 48.09-29.87 86.75-41.62 93.23-31.18 6.48 10.44-21.67 39.11-69.76 68.98-48.09 29.87-92.32 45.62-98.8 35.18z" style="fill:none;stroke:#fff;stroke-width:6;stroke-miterlimit:10"/><path class="st7" style="stroke:#000;stroke-width:3;stroke-miterlimit:10" d="M125.12 237.48v-54.5l3.78-4.75h9.47v59.25zM139.86 204.18l3.75-3.28h7.36l-.18 16.25h9.01l.28-16.25h9.53l.25 36.58h-30zM171.18 204.4l3.65-3.5h23.6v12.19l-15.25.21v10.47l15.25-.05v13.76h-27.25zM199.97 200.9h12.5v36.59h-12.5zM214.09 204.41l4-3.51h14.44l-.25-17.14 3-5.53h9.92l.14 22.67v36.58h-31.25zM251.42 200.9h19.99l.25 4.25 3.49-4.25h6.35v36.58H247l-.25-32.08z" transform="translate(-92.77 -153.171)"/><path class="st23" style="fill:#fff;stroke:#000;stroke-width:3;stroke-miterlimit:10" transform="translate(-92.77 -153.171)" d="m131.64 204.18 3.75-3.28h7.36l-.18 16.25h9.01l.29-16.25h9.52l.25 36.58h-30zM162.97 204.4l3.64-3.5h23.61v12.19l-15.25.21v10.47l15.25-.05v13.76h-27.25z"/><path class="st23" style="fill:#fff;stroke:#000;stroke-width:3;stroke-miterlimit:10" d="M98.99 47.729h12.5v36.59h-12.5z"/><path class="st23" style="fill:#fff;stroke:#000;stroke-width:3;stroke-miterlimit:10" transform="translate(-92.77 -153.171)" d="m205.88 204.41 4-3.51h14.43l-.25-17.14 3-5.53h9.93l.14 22.67v36.58h-31.25zM243.21 200.9h19.99l.24 4.25 3.5-4.25h6.34v36.58h-34.5l-.25-32.08z"/><circle class="st7" cx="190.62" cy="7.149" r="2.38" style="stroke:#000;stroke-width:3;stroke-miterlimit:10"/><circle class="st7" cx="187.03" cy="20.709" r="4.56" style="stroke:#000;stroke-width:3;stroke-miterlimit:10"/><path class="st7" style="stroke:#000;stroke-width:3;stroke-miterlimit:10" transform="translate(-92.77 -153.171)" d="m132.88 255.72 2.83 4.71 5.1-1.26-3.34 4.17 2.83 4.71-4.9-2.13-3.35 4.17.32-5.49-4.91-2.14 5.1-1.25z"/><circle class="st7" cx="107.2" cy="83.669" r="5.62" style="stroke:#000;stroke-width:3;stroke-miterlimit:10"/><ellipse class="st7" cx="91.48" cy="92.309" rx="3.38" ry="3.14" style="stroke:#000;stroke-width:3;stroke-miterlimit:10"/><path class="st7" style="stroke:#000;stroke-width:3;stroke-miterlimit:10" transform="translate(-92.77 -153.171)" d="m194.48 175.23 4.06 3.92 4.72-2.6-2.21 5.03 4.06 3.92-5.43-.82-2.22 5.02-1.14-5.52-5.43-.82 4.73-2.6zM248.83 223.77v-8.79l2.1-2.21 5.4.25v10.75zM216.68 223.77v-8.79l2.1-2.21 5.4.25v10.75z"/><path class="st9" d="M21.66 110.899c-6.48-10.44 27.24-43.11 75.33-72.98 48.09-29.87 86.75-41.62 93.23-31.18 6.48 10.44-21.67 39.11-69.76 68.98-48.09 29.87-92.32 45.62-98.8 35.18z" style="fill:none;stroke:#000;stroke-width:3;stroke-miterlimit:10"/><path class="st23" style="fill:#fff;stroke:#000;stroke-width:3;stroke-miterlimit:10" transform="translate(-92.77 -153.171)" d="m162.97 204.4 3.64-3.5h23.61v12.19l-15.25.21v10.47l15.25-.05v13.76h-27.25zM131.64 204.18l3.75-3.28h7.36l-.18 16.25h9.01l.29-16.25h9.52l.25 36.58h-30zM116.91 237.48v-54.5l3.78-4.75h9.47v59.25z"/></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();
})();