// ==UserScript==
// @name Lucida Downloader
// @description Download music from Spotify, Qobuz, Tidal, SoundCloud & Amazon Music via Lucida. Inserts a download button on Spotify and a floating Lucida button in the bottom right corner.
// @icon https://raw.githubusercontent.com/afkarxyz/misc-scripts/refs/heads/main/lucida/lucida.png
// @version 2.2
// @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.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
// ==/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 SERVICES = {
'qobuz': {
name: 'Qobuz',
icon: `${BASE_URL}qobuz.png`
},
'tidal': {
name: 'Tidal',
icon: `${BASE_URL}tidal.svg`
},
'soundcloud': {
name: 'Soundcloud',
icon: `${BASE_URL}soundcloud.ico`
},
'amazon': {
name: 'Amazon Music',
icon: `${BASE_URL}amazon.png`
}
};
GM_addStyle(`
.gear {
transition: transform 0.1s ease;
}
.gear:hover {
transform: rotate(45deg);
}
.floating-settings-button {
position: fixed;
bottom: 6.25rem;
right: -0.25rem;
width: 3.75rem;
height: 3rem;
border-radius: 1.5rem 0 0 1.5rem;
background: rgba(255, 255, 255, 0.8);
border: 0.0625rem solid rgba(0, 0, 0, 0.1);
border-right: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-around;
box-shadow: -0.125rem 0.125rem 0.625rem rgba(0, 0, 0, 0.1);
z-index: 9999;
transition: all 0.3s ease;
padding: 0 1rem;
opacity: 0.3;
overflow: hidden;
}
.floating-settings-button .lucida-icon {
transition: transform 0.1s ease;
}
.floating-settings-button .lucida-icon:hover {
transform: scale(1.1);
}
.floating-settings-button .gear {
opacity: 0;
width: 0;
transition: all 0.3s ease;
margin-left: -0.5rem;
}
.floating-settings-button:hover {
right: 0;
width: 7.5rem;
background: rgba(255, 255, 255, 1);
box-shadow: -0.25rem 0.25rem 0.9375rem rgba(0, 0, 0, 0.15);
opacity: 1;
}
.floating-settings-button:hover .gear {
opacity: 1;
width: 2rem;
margin-left: 0.5rem;
}
.floating-settings-button img,
.floating-settings-button svg {
width: 2rem;
height: 2rem;
object-fit: contain;
opacity: 0.7;
transition: opacity 0.3s ease;
}
.floating-settings-button:hover img,
.floating-settings-button:hover svg {
opacity: 1;
}
.floating-settings-button:hover svg.gear {
animation: gearRotate 4s linear infinite;
transform-origin: center;
}
.floating-settings-button .lucida-icon.disabled {
filter: grayscale(100%);
cursor: not-allowed;
opacity: 0.3;
}
.floating-settings-button svg.gear {
fill: #666;
transition: all 0.3s ease;
}
.floating-settings-button:hover svg.gear {
fill: #333;
}
.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: 0.875rem !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: 1.25rem 1.25rem 2.5rem 1.25rem;
border-radius: 0.5rem;
width: 18.75rem;
max-width: 90%;
color: #333;
font-size: 0.875rem;
box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.15);
font-weight: normal !important;
}
.lucida-modal h2 {
margin: 0 0 1.25rem;
color: #f42e8d;
font-size: 1.125rem !important;
font-weight: 600 !important;
line-height: 1.4;
}
.lucida-modal .preference-group {
margin-bottom: 1.25rem;
color: #333;
}
.lucida-modal label {
display: block;
margin-top: 1.25rem;
margin-bottom: 0.5rem;
font-weight: 600 !important;
font-size: 0.875rem !important;
color: #333;
}
.lucida-modal .header {
display: flex;
align-items: center;
justify-content: flex-start;
}
.lucida-modal .header img {
width: 4rem;
height: 4rem;
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: 0.5rem 2rem 0.5rem 0.75rem;
border: 0.0625rem solid #ddd;
border-radius: 0.25rem;
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% - 0.75rem) center no-repeat;
cursor: pointer;
font-size: 0.875rem !important;
color: #333;
}
.lucida-modal select:hover {
border-color: #f42e8d;
}
.lucida-modal select:focus {
outline: none;
border-color: #f42e8d;
box-shadow: 0 0 0 0.125rem rgba(244, 46, 141, 0.2);
}
.service-select-wrapper {
position: relative;
margin-bottom: 0.9375rem;
}
.custom-select {
width: 100%;
padding: 0.5rem 2rem 0.5rem 0.75rem;
border: 0.0625rem solid #ddd;
border-radius: 0.25rem;
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% - 0.75rem) center no-repeat;
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
font-size: 0.875rem !important;
color: #333;
}
.custom-select span {
font-size: 0.875rem !important;
color: #333;
}
.custom-select:hover {
border-color: #f42e8d;
}
.custom-options {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 0.0625rem solid #ddd;
border-radius: 0.25rem;
margin-top: 0.25rem;
max-height: 12.5rem;
overflow-y: auto;
z-index: 1000;
display: none;
box-shadow: 0 0.125rem 0.5rem rgba(0,0,0,0.1);
}
.custom-options.show {
display: block;
}
.service-option {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
cursor: pointer;
transition: background-color 0.2s ease;
font-weight: normal !important;
font-size: 0.875rem !important;
color: #333;
}
.service-option span {
font-size: 0.875rem !important;
color: #333;
}
.service-option:hover {
background-color: #f5f5f5;
}
.service-option img,
.custom-select img {
width: 1rem;
height: 1rem;
object-fit: contain;
}
.lucida-modal button {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
font-size: 0.875rem !important;
}
[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);
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 createFloatingButton() {
const button = document.createElement('div');
button.className = 'floating-settings-button';
const lucidaIcon = document.createElement('img');
lucidaIcon.src = 'https://raw.githubusercontent.com/afkarxyz/misc-scripts/refs/heads/main/lucida/lucida.png';
lucidaIcon.alt = 'Lucida';
lucidaIcon.className = 'lucida-icon';
const gearIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
gearIcon.setAttribute('viewBox', '0 0 512 512');
gearIcon.setAttribute('class', 'gear');
const gearPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
gearPath.setAttribute('d', 'M495.9 166.6c3.2 8.7 .5 18.4-6.4 24.6l-43.3 39.4c1.1 8.3 1.7 16.8 1.7 25.4s-.6 17.1-1.7 25.4l43.3 39.4c6.9 6.2 9.6 15.9 6.4 24.6c-4.4 11.9-9.7 23.3-15.8 34.3l-4.7 8.1c-6.6 11-14 21.4-22.1 31.2c-5.9 7.2-15.7 9.6-24.5 6.8l-55.7-17.7c-13.4 10.3-28.2 18.9-44 25.4l-12.5 57.1c-2 9.1-9 16.3-18.2 17.8c-13.8 2.3-28 3.5-42.5 3.5s-28.7-1.2-42.5-3.5c-9.2-1.5-16.2-8.7-18.2-17.8l-12.5-57.1c-15.8-6.5-30.6-15.1-44-25.4L83.1 425.9c-8.8 2.8-18.6 .3-24.5-6.8c-8.1-9.8-15.5-20.2-22.1-31.2l-4.7-8.1c-6.1-11-11.4-22.4-15.8-34.3c-3.2-8.7-.5-18.4 6.4-24.6l43.3-39.4C64.6 273.1 64 264.6 64 256s.6-17.1 1.7-25.4L22.4 191.2c-6.9-6.2-9.6-15.9-6.4-24.6c4.4-11.9 9.7-23.3 15.8-34.3l4.7-8.1c6.6-11 14-21.4 22.1-31.2c5.9-7.2 15.7-9.6 24.5-6.8l55.7 17.7c13.4-10.3 28.2-18.9 44-25.4l12.5-57.1c2-9.1 9-16.3 18.2-17.8C227.3 1.2 241.5 0 256 0s28.7 1.2 42.5 3.5c9.2 1.5 16.2 8.7 18.2 17.8l12.5 57.1c15.8 6.5 30.6 15.1 44 25.4l55.7-17.7c8.8-2.8 18.6-.3 24.5 6.8c8.1 9.8 15.5 20.2 22.1 31.2l4.7 8.1c6.1 11 11.4 22.4 15.8 34.3zM256 336a80 80 0 1 0 0-160 80 80 0 1 0 0 160z');
gearIcon.appendChild(gearPath);
button.appendChild(lucidaIcon);
button.appendChild(gearIcon);
document.body.appendChild(button);
lucidaIcon.addEventListener('click', (e) => {
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();
});
gearIcon.addEventListener('click', (e) => {
createPreferencesModal();
e.stopPropagation();
});
if (window.location.hostname === 'open.spotify.com') {
lucidaIcon.classList.add('disabled');
}
if (window.location.hostname.includes('lucida.')) {
button.style.display = 'none';
}
return button;
}
function createPreferencesModal() {
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;">
</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">Spotify 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>
</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 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');
domainSelect.addEventListener('change', () => {
GM_setValue('domainPreference', domainSelect.value);
});
}
if (formatSelect) {
formatSelect.value = GM_getValue('formatPreference', 'original');
formatSelect.addEventListener('change', () => {
GM_setValue('formatPreference', formatSelect.value);
updateQualityOptions(formatSelect.value);
});
}
if (autoDownloadSelect) {
autoDownloadSelect.value = GM_getValue('autoDownloadEnabled', 'enabled');
autoDownloadSelect.addEventListener('change', () => {
GM_setValue('autoDownloadEnabled', autoDownloadSelect.value);
});
}
const savedService = GM_getValue('targetService', 'tidal');
if (SERVICES[savedService]) {
updateCustomSelect(customSelect, savedService);
serviceSelect.value = savedService;
} else {
updateCustomSelect(customSelect, 'tidal');
serviceSelect.value = 'tidal';
}
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);
});
}
Object.entries(SERVICES).forEach(([value, service]) => {
const option = createServiceOption(value, service);
customOptions.appendChild(option);
option.addEventListener('click', () => {
serviceSelect.value = value;
GM_setValue('targetService', 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 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 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();
const isLucidaDomain = window.location.hostname.includes('lucida.');
if (GM_getValue('floatIconEnabled', 'enabled') === 'disabled' || isLucidaDomain) {
floatingButton.style.display = 'none';
}
}
initialize();
})();