Greasy Fork

Greasy Fork is available in English.

Lucida Downloader

Download music from Spotify, Qobuz, Tidal, Soundcloud, Deezer, Amazon Music and Yandex Music via Lucida. Adds download buttons and floating button.

当前为 2025-01-02 提交的版本,查看 最新版本

// ==UserScript==
// @name         Lucida Downloader
// @description  Download music from Spotify, Qobuz, Tidal, Soundcloud, Deezer, Amazon Music and Yandex Music via Lucida. Adds download buttons and floating button.
// @icon         https://raw.githubusercontent.com/afkarxyz/misc-scripts/refs/heads/main/lucida/lucida.png
// @version      1.7
// @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/*
// @match        https://lucida.to/*
// @match        https://lucida.su/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    const DOMAINS = ['lucida.to', 'lucida.su'];
    const BASE_URL = 'https://raw.githubusercontent.com/afkarxyz/misc-scripts/refs/heads/main/lucida/';
    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 SERVICES = {
        '': { name: 'Disabled', icon: '' },
        'spotify': { 
            name: 'Spotify',
            icon: `${BASE_URL}spotify.png`
        },
        'qobuz': {
            name: 'Qobuz',
            icon: `${BASE_URL}qobuz.png`
        },
        'tidal': {
            name: 'Tidal',
            icon: `${BASE_URL}tidal.svg`
        },
        'soundcloud': {
            name: 'Soundcloud',
            icon: `${BASE_URL}soundcloud.ico`
        },
        'deezer': {
            name: 'Deezer',
            icon: `${BASE_URL}deezer.ico`
        },
        'amazon': {
            name: 'Amazon Music',
            icon: `${BASE_URL}amazon.png`
        }
    };

    GM_addStyle(`
        .lucida-modal *,
        .lucida-modal *::before,
        .lucida-modal *::after {
            all: initial;
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !important;
            font-weight: normal !important;
            font-size: 14px !important;
            color: #333;
        }
    
        .lucida-modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0, 0, 0, 0.7);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 10000;
            font-weight: normal;
        }
        
        .lucida-modal {
            background: #fff;
            padding: 20px;
            border-radius: 8px;
            width: 400px;
            max-width: 90%;
            color: #333;
            font-size: 14px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
            font-weight: normal !important;
        }
        
        .lucida-modal h2 {
            margin: 0 0 20px;
            color: #f42e8d;
            font-size: 18px !important;
            font-weight: 600 !important;
            line-height: 1.4;
        }
        
        .lucida-modal .preference-group {
            margin-bottom: 20px;
            color: #333;
        }
        
        .lucida-modal label {
            display: block;
            margin-top: 20px;
            margin-bottom: 8px;
            font-weight: 600 !important;
            font-size: 14px !important;
            color: #333;
        }

        .lucida-modal .header {
            display: flex;
            align-items: center;
            justify-content: flex-start;
        }

        .lucida-modal .header img {
            width: 64px;
            height: 64px;
            object-fit: contain;
        }

        .lucida-modal .header h2 {
            margin: 0;
        }
    
        .lucida-modal .preference-group label:first-child {
            margin-top: 0;
        }
        
        .lucida-modal select {
            -webkit-appearance: none;
            -moz-appearance: none;
            appearance: none;
            width: 100%;
            padding: 8px 32px 8px 12px;
            border: 1px solid #ddd;
            border-radius: 4px;
            background: #fff 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='%23666' d='M6 8L1 3h10z'/%3E%3C/svg%3E") calc(100% - 12px) center no-repeat;
            cursor: pointer;
            font-size: 14px !important;
            color: #333;
        }
        
        .lucida-modal select:hover {
            border-color: #f42e8d;
        }
        
        .lucida-modal select:focus {
            outline: none;
            border-color: #f42e8d;
            box-shadow: 0 0 0 2px rgba(244, 46, 141, 0.2);
        }
    
        .custom-options {
            scrollbar-width: thin;
            scrollbar-color: #f42e8d #f0f0f0;
            font-size: 14px !important;
        }
    
        .custom-options::-webkit-scrollbar {
            width: 8px;
        }
    
        .custom-options::-webkit-scrollbar-track {
            background: #f0f0f0;
            border-radius: 4px;
        }
    
        .custom-options::-webkit-scrollbar-thumb {
            background: #f42e8d;
            border-radius: 4px;
        }
    
        .custom-options::-webkit-scrollbar-thumb:hover {
            background: #d41d7a;
        }
        
        .service-select-wrapper {
            position: relative;
            margin-bottom: 15px;
        }
        
        .custom-select {
            width: 100%;
            padding: 8px 32px 8px 12px;
            border: 1px solid #ddd;
            border-radius: 4px;
            background: #fff 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='%23666' d='M6 8L1 3h10z'/%3E%3C/svg%3E") calc(100% - 12px) center no-repeat;
            display: flex;
            align-items: center;
            gap: 8px;
            cursor: pointer;
            transition: all 0.2s ease;
            user-select: none;
            font-size: 14px !important;
            color: #333;
        }
    
        .custom-select span {
            font-size: 14px !important;
            color: #333;
        }
        
        .custom-select:hover {
            border-color: #f42e8d;
        }
        
        .custom-options {
            position: absolute;
            top: 100%;
            left: 0;
            right: 0;
            background: white;
            border: 1px solid #ddd;
            border-radius: 4px;
            margin-top: 4px;
            max-height: 200px;
            overflow-y: auto;
            z-index: 1000;
            display: none;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        
        .custom-options.show {
            display: block;
        }
        
        .service-option {
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 8px 12px;
            cursor: pointer;
            transition: background-color 0.2s ease;
            font-weight: normal !important;
            font-size: 14px !important;
            color: #333;
        }
    
        .service-option span {
            font-size: 14px !important;
            color: #333;
        }
        
        .service-option:hover {
            background-color: #f5f5f5;
        }
        
        .service-option img,
        .custom-select img {
            width: 16px;
            height: 16px;
            object-fit: contain;
        }
        
        .lucida-modal .buttons {
            display: flex;
            justify-content: flex-end;
            gap: 10px;
            margin-top: 20px;
        }
        
        .lucida-modal button {
            padding: 8px 16px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-weight: 500;
            transition: all 0.2s ease;
            font-size: 14px !important;
        }
        
        .lucida-modal .save-btn {
            background: linear-gradient(135deg, #f42e8d, #b91c68);
            color: white;
        }
        
        .lucida-modal .save-btn:hover {
            background: linear-gradient(135deg, #ff3d9c, #d02077);
            transform: translateY(-1px);
            box-shadow: 0 2px 8px rgba(244, 46, 141, 0.4);
        }
        
        .lucida-modal .save-btn:active {
            transform: translateY(0);
            box-shadow: 0 1px 4px rgba(244, 46, 141, 0.4);
        }
        
        .lucida-modal .cancel-btn {
            background: #eee;
            color: #333;
        }
        
        .lucida-modal .cancel-btn:hover {
            background: #ddd;
            color: #333;
            transform: translateY(-1px);
        }
        
        .lucida-modal .cancel-btn:active {
            transform: translateY(0);
        }
    
        .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;
        }
    
        [role='grid'] {
            margin-left: 50px;
        }
    
        [data-testid="tracklist-row"] {
            position: relative;
        }
    
        [role="presentation"] > * {
            contain: unset;
        }
    
        .btn {
            width: 40px;
            height: 40px;
            border-radius: 50%;
            border: 0;
            position: relative;
            cursor: pointer;
            transition: all 0.2s ease;
            box-shadow: 0 2px 5px 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 4px 8px 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: -20px;
            margin-right: 10px;
        }
    
        .N7GZp8IuWPJvCPz_7dOg .btn {
            width: 24px;
            height: 24px;
            transform-origin: center;
            position: absolute;
            top: 50%;
            right: 100%;
            margin-top: -12px !important;
            margin-right: 10px;
        }
    
        .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);
        
        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>`;
        }
        
        customSelect.innerHTML = content;
    }
    
    function createPreferencesModal() {
        const existingModal = document.querySelector('.lucida-modal-overlay');
        if (existingModal) {
            existingModal.remove();
        }
    
        const modalHTML = `
            <div class="lucida-modal-overlay">
                <div class="lucida-modal">
                    <div class="header">
                        <h2>Lucida Preferences</h2>
                        <img src="${BASE_URL}lucida.svg" alt="Lucida Icon" class="lucida-icon" style="cursor: pointer; margin-left: auto; display: none;">
                    </div>
                    <div class="preference-group">
                        <label for="domain-select">Domain</label>
                        <select id="domain-select">
                            <option value="random">Random</option>
                            <option value="lucida.to">Lucida.to</option>
                            <option value="lucida.su">Lucida.su</option>
                        </select>
                        
                        <label for="service-select">Service Resolver</label>
                        <div class="service-select-wrapper">
                            <div class="custom-select" id="custom-service-select">
                                <img src="" alt="" style="display: none;">
                                <span>Select a service</span>
                            </div>
                            <div class="custom-options">
                            </div>
                        </div>
                        <input type="hidden" id="service-select">
                        
                        <label for="format-select">Download Format</label>
                        <select id="format-select">
                            <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>
                        </select>

                        <div id="quality-settings-container" style="display: none; margin-top: 20px;">
                            <label for="quality-select" style="margin-top: 0;">Quality Settings</label>
                            <select id="quality-select"></select>
                        </div>

                        <label for="auto-download-select">Auto Download</label>
                        <select id="auto-download-select">
                            <option value="enabled">Enabled</option>
                            <option value="disabled">Disabled</option>
                        </select>

                        <label for="float-select">Float Icon</label>
                        <select id="float-select">
                            <option value="enabled">Enabled</option>
                            <option value="disabled">Disabled</option>
                        </select>
                    </div>
                    <div class="buttons">
                        <button class="cancel-btn">Cancel</button>
                        <button class="save-btn">Save</button>
                    </div>
                </div>
            </div>
        `;
    
        const modalContainer = document.createElement('div');
        modalContainer.innerHTML = modalHTML;
        document.body.appendChild(modalContainer.firstElementChild);
    
        const customSelect = document.getElementById('custom-service-select');
        const customOptions = document.querySelector('.custom-options');
        const serviceSelect = document.getElementById('service-select');
        const domainSelect = document.getElementById('domain-select');
        const floatSelect = document.getElementById('float-select');
        const formatSelect = document.getElementById('format-select');
        const qualityContainer = document.getElementById('quality-settings-container');
        const qualitySelect = document.getElementById('quality-select');
        const autoDownloadSelect = document.getElementById('auto-download-select');
        const lucidaIcon = document.querySelector('.lucida-icon');
    
        lucidaIcon.onload = () => {
            lucidaIcon.style.display = 'inline';
        };
    
        lucidaIcon.onerror = () => {
            lucidaIcon.style.display = 'none';
        };
    
        lucidaIcon.addEventListener('click', () => {
            const domainPref = GM_getValue('domainPreference', 'random');
            let domain = domainPref === 'random' 
                ? DOMAINS[Math.floor(Math.random() * DOMAINS.length)]
                : domainPref;
            window.open(`https://${domain}/stats`, '_blank');
        });
    
        if (domainSelect) domainSelect.value = GM_getValue('domainPreference', 'random');
        if (floatSelect) floatSelect.value = GM_getValue('floatIconEnabled', 'enabled');
        if (formatSelect) formatSelect.value = GM_getValue('formatPreference', 'original');
        if (autoDownloadSelect) autoDownloadSelect.value = GM_getValue('autoDownloadEnabled', 'enabled');
    
        const savedService = GM_getValue('targetService', '');
        if (savedService && SERVICES[savedService]) {
            updateCustomSelect(customSelect, savedService);
            serviceSelect.value = savedService;
        }
    
        function updateQualityOptions(format) {
            qualitySelect.innerHTML = '';
            
            switch(format) {
                case 'flac':
                    qualitySelect.innerHTML = '<option value="16">16-bit 44.1kHz</option>';
                    qualityContainer.style.display = 'block';
                    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';
                    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';
                    break;
                default:
                    qualityContainer.style.display = 'none';
            }
        }
    
        updateQualityOptions(formatSelect.value);
        if (qualitySelect) {
            qualitySelect.value = GM_getValue('qualityPreference', '320');
        }
    
        formatSelect.addEventListener('change', () => {
            updateQualityOptions(formatSelect.value);
        });
    
        Object.entries(SERVICES).forEach(([value, service]) => {
            const option = createServiceOption(value, service);
            customOptions.appendChild(option);
            
            option.addEventListener('click', () => {
                serviceSelect.value = value;
                updateCustomSelect(customSelect, value);
                customOptions.classList.remove('show');
            });
        });
    
        customSelect.addEventListener('click', () => {
            customOptions.classList.toggle('show');
        });
    
        document.addEventListener('click', (e) => {
            if (!e.target.closest('.service-select-wrapper')) {
                customOptions.classList.remove('show');
            }
        });
    
        const saveBtn = document.querySelector('.save-btn');
        if (saveBtn) {
            saveBtn.addEventListener('click', () => {
                if (domainSelect && serviceSelect && floatSelect && formatSelect && 
                    qualitySelect && autoDownloadSelect) {
                    GM_setValue('domainPreference', domainSelect.value);
                    GM_setValue('targetService', serviceSelect.value);
                    GM_setValue('floatIconEnabled', floatSelect.value);
                    GM_setValue('formatPreference', formatSelect.value);
                    GM_setValue('qualityPreference', qualitySelect.value);
                    GM_setValue('autoDownloadEnabled', autoDownloadSelect.value);
                    
                    const floatingButton = document.querySelector('.floating-button');
                    if (floatingButton) {
                        floatingButton.style.display = floatSelect.value === 'enabled' ? 'flex' : 'none';
                    }
                }
                document.querySelector('.lucida-modal-overlay').remove();
            });
        }
    
        const cancelBtn = document.querySelector('.cancel-btn');
        if (cancelBtn) {
            cancelBtn.addEventListener('click', () => {
                document.querySelector('.lucida-modal-overlay').remove();
            });
        }
    
        const modalOverlay = document.querySelector('.lucida-modal-overlay');
        if (modalOverlay) {
            modalOverlay.addEventListener('click', (e) => {
                if (e.target === modalOverlay) {
                    modalOverlay.remove();
                }
            });
        }
    }

    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((mutations) => {
            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((mutations) => {
            clickDownloadButton();
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        clickDownloadButton();
    }
    
    function setupMenuCommand() {
        try {
            GM_registerMenuCommand('Lucida Preferences', () => {
                console.log('Opening preferences modal...');
                createPreferencesModal();
            });
        } catch (error) {
            console.error('Error registering menu command:', error);
        }
    }
    
    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', ''),
        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);
    }
    
    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')) {
            openInLucida();
        }
    });
    
    const isLucidaDomain = window.location.hostname.includes('lucida.');
    
    if (GM_getValue('floatIconEnabled', 'enabled') === 'disabled' || isLucidaDomain) {
        button.style.display = 'none';
    }
    
    document.body.appendChild(button);
    setupMenuCommand();
    requestAnimationFrame(animateLoop);
    autoSelectFormat();
    autoDownload();
})();