您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Download music from Spotify, Qobuz, Tidal, Soundcloud, Deezer, Amazon Music and Yandex Music via Lucida. Adds download buttons and floating button.
当前为
- // ==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.6
- // @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="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 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');
- 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) {
- 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);
- 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 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();
- }
- });
- if (GM_getValue('floatIconEnabled', 'enabled') === 'disabled') {
- button.style.display = 'none';
- }
- document.body.appendChild(button);
- setupMenuCommand();
- requestAnimationFrame(animateLoop);
- autoSelectFormat();
- })();