[AI Translations] Spotify Web Player Floating Lyrics

Synced lyrics with translation/romanization resizable/draggable panel, themed, opacity control. Translations are provided by Gemini 2.0 Flash and 1.5 Flash via the Google AI Studio API (Accessed via a remote server).

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         [AI Translations] Spotify Web Player Floating Lyrics
// @namespace    http://tampermonkey.net/
// @version      2.5.4
// @description  Synced lyrics with translation/romanization resizable/draggable panel, themed, opacity control. Translations are provided by Gemini 2.0 Flash and 1.5 Flash via the Google AI Studio API (Accessed via a remote server).
// @author       jayxdcode
// @license      All Rights Reserved
// @match        https://open.spotify.com/*
// @grant        GM_log
// @grant        unsafeWindow
// @grant        GM_xmlhttpRequest
// @connect      lrclib.net
// @connect      src-backend.onrender.com
// @connect      genius.com
// @connect      google.com
// @copyright    2025, jayxdcode
// @sandbox      JavaScript
// ==/UserScript==

(function() {
    'use strict';

// -- begin --
    const mobileDebug = true; // only set to true if you have eruda.

    const BACKEND_URL = "https://src-backend.onrender.com/api/translate";

    const POLL_INTERVAL = 1000;
    const STORAGE_KEY = 'tm-lyrics-panel-position';
    const SIZE_KEY = 'tm-lyrics-panel-size';
    const THEME_KEY = 'tm-lyrics-theme';
    const OPACITY_KEY = 'tm-lyrics-opacity';
    const CONFIG_KEY = 'tm-lyrics-config';

    let lyricsConfig = JSON.parse(localStorage.getItem(CONFIG_KEY) || '{}');
    let lastCandidates = [];
    let currentTrackId = null;
    let currentTrackDur = null;
    let currInf = null;
    let syncIntervalId = null;
    let lyricsData = null;
    let observer = null;
    let isDragging = false;
    let dragLocked = false;
    let isResizing = false;
    let currentOpacity = parseFloat(localStorage.getItem(OPACITY_KEY)) || 0.85;
    let currentTheme = localStorage.getItem(THEME_KEY) || 'dark';
    let lastRenderedIdx = -1;

    let logVisible = false;

    // --- Utility Functions ---
    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    // --- Panel viewport adjustment logic ---
    function handleViewportChange() {
        const panel = document.getElementById('tm-lyrics-panel');
        if (!panel) return;

        const rect = panel.getBoundingClientRect();
        const winWidth = window.innerWidth;
        const winHeight = window.innerHeight;

        const isOutOfBounds =
              rect.left < 0 ||
              rect.top < 0 ||
              rect.right > winWidth ||
              rect.bottom > winHeight;

        const isTooLarge =
              rect.width > winWidth ||
              rect.height > winHeight;

        if (isOutOfBounds || isTooLarge) {
            debug('Panel is out of bounds or too large for viewport. Adjusting...');

            // Clamp size to fit viewport with a small margin
            const newWidth = Math.min(rect.width, winWidth - 20);
            const newHeight = Math.min(rect.height, winHeight - 20);
            panel.style.width = newWidth + 'px';
            panel.style.height = newHeight + 'px';

            // Re-check rect after resize
            const newRect = panel.getBoundingClientRect();

            // Clamp position to keep the panel fully inside the viewport
            const newLeft = Math.max(10, Math.min(newRect.left, winWidth - newRect.width - 10));
            const newTop = Math.max(10, Math.min(newRect.top, winHeight - newRect.height - 10));
            panel.style.left = newLeft + 'px';
            panel.style.top = newTop + 'px';

            localStorage.setItem(STORAGE_KEY, JSON.stringify({ left: panel.style.left, top: panel.style.top }));
            localStorage.setItem(SIZE_KEY, JSON.stringify({ width: panel.style.width, height: panel.style.height }));
        }
    }

    // --- Manual Lyrics Menu ---
    function showManualLyricsMenu(trackKey) {

        // Ensure we have candidates
        if (!lastCandidates || !lastCandidates.length) {
            const manualQuery = prompt('No lyric candidates available. Search manually:');
            if (manualQuery && manualQuery.trim() !== '') {
                loadLyrics('', '', '', currentTrackDur, (parsed) => {
                    lyricsData = parsed;
                    renderLyrics(0);
                    setupProgressSync(currInf.bar, currInf.duration);
                }, { flag: true, query: manualQuery });
            }
            return;
        }

        // Add blur overlay
        const existingOverlay = document.getElementById('tm-manual-overlay');
        if (existingOverlay) existingOverlay.remove();
        const overlay = document.createElement('div');
        overlay.id = 'tm-manual-overlay';
        Object.assign(overlay.style, {
            position: 'fixed',
            top: 0,
            left: 0,
            width: '100vw',
            height: '100vh',
            background: 'rgba(0,0,0,0.5)',
            backdropFilter: 'blur(5px)',
            zIndex: 9999
        });
        overlay.onclick = () => {
            overlay.remove();
            menu.remove();
        };
        document.body.appendChild(overlay);

        // Remove any existing menu
        document.getElementById('tm-manual-menu')?.remove();

        // Container
        const menu = document.createElement('div');
        menu.id = 'tm-manual-menu';
        Object.assign(menu.style, {
            position: 'fixed',
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%)',
            width: '90vw',
            maxWidth: '600px',
            maxHeight: '70vh',
            background: '#2a2a2a',
            color: '#fff',
            borderRadius: '12px',
            overflow: 'hidden',
            display: 'flex',
            flexDirection: 'column',
            zIndex: 10000,
            boxShadow: '0 4px 20px rgba(0,0,0,0.5)'
        });
        document.body.appendChild(menu);

        // Header with title & close
        const header = document.createElement('div');
        header.textContent = 'Choose Lyrics Source';
        Object.assign(header.style, { padding: '12px 16px', fontWeight: 'bold', borderBottom: '1px solid #444', display: 'flex', justifyContent: 'space-between', alignItems: 'center' });
        const closeBtn = document.createElement('button');
        closeBtn.textContent = '×';
        Object.assign(closeBtn.style, { background: 'none', border: 'none', color: '#fff', fontSize: '20px', cursor: 'pointer' });
        closeBtn.onclick = () => {
            overlay.remove();
            menu.remove();
        };
        header.appendChild(closeBtn);
        menu.appendChild(header);

        // Scrollable list
        const list = document.createElement('div');
        Object.assign(list.style, { flex: '1', overflowY: 'auto', padding: '8px' });
        menu.appendChild(list);

        lastCandidates.forEach((c, idx) => {
            const panel = document.createElement('div');
            Object.assign(panel.style, { background: '#333', borderRadius: '8px', marginBottom: '8px', overflow: 'hidden' });

            // Summary row
            const summary = document.createElement('div');
            Object.assign(summary.style, { padding: '10px 12px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', cursor: 'pointer' });
            summary.innerHTML = `<span>Candidate ${idx+1}</span><span style="font-size:12px; opacity:.7;">▼</span>`;
            panel.appendChild(summary);

            // 3-line preview
            const preview = document.createElement('pre');
            const lines = c.syncedLyrics ? c.syncedLyrics.trim().split('\n').slice(0, 3) : c.plainLyrics.trim().split('\n').slice(0, 3);
            preview.textContent = lines.join('\n');
            Object.assign(preview.style, { margin: '0 12px 8px', padding: '0', fontSize: '12px', lineHeight: '1.2', color: '#ccc' });
            panel.appendChild(preview);

            // Body (hidden full lyrics)
            const body = document.createElement('pre');
            body.textContent = c.syncedLyrics ? c.syncedLyrics.trim() : c.plainLyrics.trim();
            Object.assign(body.style, { margin: 0, padding: '8px 12px', fontSize: '13px', lineHeight: '1.4', whiteSpace: 'pre-wrap', display: 'none', background: '#2b2b2b' });
            panel.appendChild(body);

            // Toggle on click
            summary.onclick = () => {
                const isOpen = body.style.display === 'block';
                body.style.display = isOpen ? 'none' : 'block';
                summary.querySelector('span:last-child').textContent = isOpen ? '▼' : '▲';
            };

            list.appendChild(panel);
        });

        // Footer with offset input + buttons
        const footer = document.createElement('div');
        Object.assign(footer.style, { padding: '12px 16px', borderTop: '1px solid #444', display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' });

        // Offset
        const offLabel = document.createElement('label');
        offLabel.textContent = 'Offset (ms):';
        Object.assign(offLabel.style, { fontSize: '14px' });
        const offInput = document.createElement('input');
        offInput.type = 'number';
        offInput.value = lyricsConfig[trackKey]?.offset || 0;
        Object.assign(offInput.style, { width: '60px', padding: '4px', borderRadius: '4px', border: '1px solid #555', background: '#444', color: '#fff' });
        footer.appendChild(offLabel);
        footer.appendChild(offInput);

        // Manual Search button
        const searchBtn = document.createElement('button');
        searchBtn.textContent = 'Manual Search';
        Object.assign(searchBtn.style, { padding: '6px 12px', background: 'none', color: '#fff', border: '2px solid #555', borderRadius: '4px', cursor: 'pointer' });
        searchBtn.onclick = () => {
            const manualQuery = prompt('Enter manual search query (e.g., song title and artist):');
            if (manualQuery && manualQuery.trim() !== '') {
                overlay.remove();
                menu.remove();
                loadLyrics('', '', '', currentTrackDur, (parsed) => {
                    lyricsData = parsed;
                    renderLyrics(0);
                    if (currInf) { setupProgressSync(currInf.bar, currInf.duration); }
                }, { flag: true, query: manualQuery });
            }
        };
        footer.appendChild(searchBtn);

        // Reset Pick button
        const resetBtn = document.createElement('button');
        resetBtn.textContent = 'Reset Pick';
        Object.assign(resetBtn.style, { padding: '6px 12px', background: 'none', color: '#fff', border: '2px solid #555', borderRadius: '4px', cursor: 'pointer' });
        resetBtn.onclick = () => {
            delete lyricsConfig[trackKey];
            localStorage.setItem(CONFIG_KEY, JSON.stringify(lyricsConfig));
        };
        footer.appendChild(resetBtn);

        // Use Selected button
        const useBtn = document.createElement('button');
        useBtn.textContent = 'Use Selected';
        Object.assign(useBtn.style, { padding: '6px 12px', background: 'none', color: '#fff', border: '2px solid #333', borderRadius: '4px', cursor: 'pointer' });
        useBtn.onclick = () => {
            const openBodies = Array.from(list.children)
            .filter(p => p.querySelector('pre:last-of-type').style.display === 'block');
            let rawLrc = openBodies.length ?
                openBodies[0].querySelector('pre:last-of-type').textContent :
            lastCandidates[0].syncedLyrics;
            const offset = parseInt(offInput.value, 10) || 0;

            lyricsConfig[trackKey] = { manualLrc: rawLrc, offset };
            localStorage.setItem(CONFIG_KEY, JSON.stringify(lyricsConfig));
            overlay.remove();
            menu.remove();

            const [t, a] = trackKey.split('|');
            loadLyrics(t, a, '', 0, parsed => {
                lyricsData = parsed;
                renderLyrics(0);
                setupProgressSync(null, 0);
            });
        };
        footer.appendChild(useBtn);

        menu.appendChild(footer);
    }

    // --- Panel creation and drag/resize logic ---
    function createPanel() {
        document.getElementById('tm-lyrics-overlay')?.remove();
        const overlay = document.createElement('div');
        overlay.id = 'tm-lyrics-overlay';
        Object.assign(overlay.style, { position: 'fixed', top: '0', left: '0', width: '100vw', height: '100vh', zIndex: 9998, pointerEvents: 'none' });
        const panel = document.createElement('div');
        panel.id = 'tm-lyrics-panel';
        Object.assign(panel.style, { position: 'fixed', width: '470px', height: '390px', minWidth: '470px', minHeight: '390px', boxShadow: '0 4px 20px rgba(0,0,0,0.4)', borderRadius: '10px', fontSize: '25px', lineHeight: '1.6', padding: '0', overflow: 'hidden', pointerEvents: 'auto', userSelect: 'none', zIndex: 9999, border: '2px solid #333', display: 'flex', flexDirection: 'column' });
        const defaultPos = { left: '100px', top: '100px' };
        const savedPos = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null');
        panel.style.left = (savedPos && savedPos.left) ? savedPos.left : defaultPos.left;
        panel.style.top = (savedPos && savedPos.top) ? savedPos.top : defaultPos.top;
        const savedSize = JSON.parse(localStorage.getItem(SIZE_KEY) || 'null');
        if (savedSize && savedSize.width && savedSize.height) {
            panel.style.width = savedSize.width;
            panel.style.height = savedSize.height;
        }
        const header = document.createElement('div');
        header.id = 'tm-lyrics-header';
        Object.assign(header.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '7px 14px', cursor: 'move', userSelect: 'none', borderTopLeftRadius: '10px', borderTopRightRadius: '10px', flexShrink: 0 });
        const title = document.createElement('span');
        title.id = 'tm-header-title';
        title.innerHTML = dragLocked ? '<b>Lyrics (Locked)</b>' : '<b>Lyrics</b>';
        header.appendChild(title);

        detectLongClick(title, toggleLogVisibility, null, 1000);

        const controls = document.createElement('div');
        Object.assign(controls.style, { display: 'flex', gap: '8px', alignItems: 'center' });
        const opDown = document.createElement('button');
        opDown.textContent = '- Opacity';
        opDown.addEventListener('click', () => {
            currentOpacity = Math.max(0.2, parseFloat((currentOpacity - 0.1).toFixed(2)));
            localStorage.setItem(OPACITY_KEY, currentOpacity);
            applyTheme(panel);
        });
        const opUp = document.createElement('button');
        opUp.textContent = '+ Opacity';
        opUp.addEventListener('click', () => {
            currentOpacity = Math.min(1, parseFloat((currentOpacity + 0.1).toFixed(2)));
            localStorage.setItem(OPACITY_KEY, currentOpacity);
            applyTheme(panel);
        });
        const manualBtn = document.createElement('button');
        manualBtn.textContent = 'Manual LRC';
        manualBtn.onclick = () => {
            const trackKey = currentTrackId;
            showManualLyricsMenu(trackKey);
        };
        const ghIcon = document.createElement('div');
        Object.assign(ghIcon.style, { display: 'flex', alignItems: 'center', paddingTop: '5px', fontSize: '14px' });
        ghIcon.innerHTML = `<a href="https://github.com/jayxdcode" target="_blank" title="View on GitHub" style="opacity:0.8; color:white"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"/></svg></a>`;
        controls.append(manualBtn, opDown, opUp, ghIcon);
        header.appendChild(controls);
        controls.querySelectorAll('button').forEach(btn => Object.assign(btn.style, { background: 'transparent', color: '#fff', border: '2px solid #333', borderRadius: '4px', padding: '6px 10px', fontSize: '14px', cursor: 'pointer', transition: 'opacity 0.2s' }));
        const content = document.createElement('div');
        content.id = 'tm-lyrics-lines';
        Object.assign(content.style, { padding: '12px', overflowY: 'auto', scrollBehavior: 'smooth', flex: '1 1 auto', minHeight: '0' });
        content.innerHTML = '<em>Lyrics will appear here</em>';
        const resizeHandle = document.createElement('div');
        resizeHandle.id = 'tm-lyrics-resize';
        Object.assign(resizeHandle.style, { position: 'absolute', right: '1px', bottom: '.5px', width: '18px', height: '18px', cursor: 'nwse-resize', background: 'linear-gradient(135deg,transparent 60%,#888 60%)', opacity: 1 });
        panel.appendChild(header);
        panel.appendChild(content);
        panel.appendChild(resizeHandle);
        overlay.appendChild(panel);
        document.body.appendChild(overlay);

        applyTheme(panel);

        // Drag logic
        let dragX = 0,
            dragY = 0;
        header.addEventListener('mousedown', e => {
            if (dragLocked) return;
            isDragging = true;
            dragX = e.clientX - panel.offsetLeft;
            dragY = e.clientY - panel.offsetTop;
            document.body.style.userSelect = 'none';
        });
        document.addEventListener('mousemove', e => {
            if (!isDragging) return;
            let x = e.clientX - dragX;
            let y = e.clientY - dragY;
            x = Math.min(Math.max(0, x), window.innerWidth - panel.offsetWidth);
            y = Math.min(Math.max(0, y), window.innerHeight - panel.offsetHeight);
            panel.style.left = x + 'px';
            panel.style.top = y + 'px';
        });
        document.addEventListener('mouseup', () => {
            if (!isDragging) return;
            isDragging = false;
            document.body.style.userSelect = '';
            localStorage.setItem(STORAGE_KEY, JSON.stringify({ left: panel.style.left, top: panel.style.top }));
        });

        // Touch drag
        header.addEventListener('touchstart', e => {
            if (dragLocked) return;
            const t = e.touches[0];
            isDragging = true;
            dragX = t.clientX - panel.offsetLeft;
            dragY = t.clientY - panel.offsetTop;
            document.body.style.userSelect = 'none';
        }, { passive: false });
        document.addEventListener('touchmove', e => {
            if (!isDragging) return;
            const t = e.touches[0];
            let x = t.clientX - dragX;
            let y = t.clientY - dragY;
            x = Math.min(Math.max(0, x), window.innerWidth - panel.offsetWidth);
            y = Math.min(Math.max(0, y), window.innerHeight - panel.offsetHeight);
            panel.style.left = x + 'px';
            panel.style.top = y + 'px';
        }, { passive: false });
        document.addEventListener('touchend', () => {
            if (!isDragging) return;
            isDragging = false;
            document.body.style.userSelect = '';
            localStorage.setItem(STORAGE_KEY, JSON.stringify({ left: panel.style.left, top: panel.style.top }));
        });

        // Resize logic
        let startW, startH, startX, startY;
        resizeHandle.addEventListener('mousedown', e => {
            isResizing = true;
            startW = panel.offsetWidth;
            startH = panel.offsetHeight;
            startX = e.clientX;
            startY = e.clientY;
            e.preventDefault();
            e.stopPropagation();
        });
        document.addEventListener('mousemove', e => {
            if (!isResizing) return;
            let w = Math.max(200, startW + e.clientX - startX);
            let h = Math.max(120, startH + e.clientY - startY);
            w = Math.min(w, window.innerWidth - panel.offsetLeft);
            h = Math.min(h, window.innerHeight - panel.offsetTop);
            panel.style.width = w + 'px';
            panel.style.height = h + 'px';
        });
        document.addEventListener('mouseup', () => {
            if (!isResizing) return;
            isResizing = false;
            localStorage.setItem(SIZE_KEY, JSON.stringify({ width: panel.style.width, height: panel.style.height }));
        });
        resizeHandle.addEventListener('touchstart', e => {
            const t = e.touches[0];
            isResizing = true;
            startW = panel.offsetWidth;
            startH = panel.offsetHeight;
            startX = t.clientX;
            startY = t.clientY;
            e.preventDefault();
            e.stopPropagation();
        }, { passive: false });
        document.addEventListener('touchmove', e => {
            if (!isResizing) return;
            const t = e.touches[0];
            let w = Math.max(200, startW + t.clientX - startX);
            let h = Math.max(120, startH + t.clientY - startY);
            w = Math.min(w, window.innerWidth - panel.offsetLeft);
            h = Math.min(h, window.innerHeight - panel.offsetTop);
            panel.style.width = w + 'px';
            panel.style.height = h + 'px';
        }, { passive: false });
        document.addEventListener('touchend', () => {
            if (!isResizing) return;
            isResizing = false;
            localStorage.setItem(SIZE_KEY, JSON.stringify({ width: panel.style.width, height: panel.style.height }));
        });
    }


    function applyTheme(panel) {
        const header = panel.querySelector('#tm-lyrics-header');
        if (currentTheme === 'light') {
            panel.style.background = `rgba(245, 245, 245, ${currentOpacity})`;
            panel.style.color = '#000';
            if (header) header.style.background = `rgba(220, 220, 220, ${currentOpacity})`;
        } else {
            panel.style.background = `rgba(0, 0, 0, ${currentOpacity})`;
            panel.style.color = '#fff';
            if (header) header.style.background = `rgba(33, 33, 33, ${currentOpacity})`;
        }
    }

    function gmFetch(url, headers = {}) {
        if (typeof GM_xmlhttpRequest === 'function') {
            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url,
                    headers,
                    onload: res => resolve(res),
                    onerror: err => reject(err),
                    ontimeout: () => reject(new Error('Request timed out'))
                });
            });
        } else {
            // Use native fetch if GM_xmlhttpRequest is not available
            return fetch(url, { headers })
                .then(response => {
                if (!response.ok) {
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                return response;
            })
                .catch(error => {
                throw new Error(`Fetch failed: ${error.message}`);
            });
        }
    }

    function toggleLogVisibility() {
        const logs = document.getElementById('tm-logs');
        logs.style.display = logVisible ? 'block' : 'none';
        logVisible = logVisible ? false : true;
    }

    /**
 * Attaches a long click detection to a DOM element.
 *
 * @param {HTMLElement} element The DOM element to attach the listener to.
 * @param {function} onLongClick Callback function to execute when a long click is detected.
 * @param {function} [onShortClick] Optional callback for a short click. If not provided,
 * only long clicks will trigger a callback.
 * @param {number} [longClickThreshold=500] The duration in milliseconds to consider a click "long".
 */
    function detectLongClick(element, onLongClick, onShortClick, longClickThreshold = 500) {
        let pressTimer;
        let isLongClickTriggered = false; // Flag to prevent short click after long click

        if (!element || typeof onLongClick !== 'function') {
            console.error("detectLongClick: Invalid element or onLongClick callback provided.");
            return;
        }

        const startTimer = () => {
            isLongClickTriggered = false; // Reset flag for new press
            pressTimer = setTimeout(() => {
                isLongClickTriggered = true;
                onLongClick();
            }, longClickThreshold);
        };

        const clearTimer = () => {
            clearTimeout(pressTimer);
        };

        // --- Mouse Events ---
        element.addEventListener('mousedown', (event) => {
            // Prevent right-click from triggering long-click for mouse events
            if (event.button === 2) {
                return;
            }
            startTimer();
        });

        element.addEventListener('mouseup', () => {
            clearTimer();
            // Only trigger short click if long click wasn't triggered
            if (!isLongClickTriggered && typeof onShortClick === 'function') {
                onShortClick();
            }
        });

        // If mouse leaves the element while pressed (important to clear timer)
        element.addEventListener('mouseleave', () => {
            clearTimer();
            // Reset long click flag if mouse leaves, preventing accidental short click if re-entered
            isLongClickTriggered = false;
        });

        // --- Touch Events ---
        // Using passive: true for better scroll performance. If you need to prevent default
        // browser behavior (like scrolling/zooming on touch), set to false and handle `event.preventDefault()`.
        element.addEventListener('touchstart', (event) => {
            // event.preventDefault(); // Uncomment if you need to prevent default touch behaviors
            startTimer();
        }, { passive: true });

        element.addEventListener('touchend', () => {
            clearTimer();
            if (!isLongClickTriggered && typeof onShortClick === 'function') {
                onShortClick();
            }
        }, { passive: true });

        element.addEventListener('touchcancel', () => {
            clearTimer();
            isLongClickTriggered = false; // Reset if touch is interrupted (e.g., phone call)
        }, { passive: true });
    }



    async function parseAl(url = null) {
        try { if (!url) return ''; const res = await fetch(url); const html = await res.text(); const doc = new DOMParser().parseFromString(html, 'text/html'); const titleText = doc.querySelector('title')?.textContent; if (titleText) { const match = titleText.match(/^(.*?) - Album by .*? \| Spotify$/); if (match && match.length > 1) return match[1]; } } catch (e) { debug('parseAl error:', e); }
        return '';
    }

    /**
     * [RESTORED] Fetch up to 3 Genius English Translation links via strict Google search.
     */
    async function fetchStrictGeniusLinks(title, artist) {
        if (!title || !artist) return [];
        const query = `site:genius.com ${title} ${artist} "(english translation)"`;
        const googleSearchUrl = `https://www.google.com/search?q=${encodeURIComponent(query)}`;
        debug(`🔍 Google search for Genius link: ${googleSearchUrl}`);
        try {
            const searchRes = await gmFetch(googleSearchUrl, { 'User-Agent': 'Mozilla/5.0' });
            const doc = new DOMParser().parseFromString(searchRes.responseText, 'text/html');
            const anchors = Array.from(doc.querySelectorAll('a[href^="/url?q=https://genius.com/Genius-english-translations-"]'));
            const geniusLinks = [];
            for (let a of anchors) {
                if (geniusLinks.length >= 3) break;
                const href = a.getAttribute('href');
                const match = href.match(/\/url\?q=(https:\/\/genius\.com\/Genius-english-translations-[^&]+)/i);
                if (match && match[1]) {
                    const decoded = decodeURIComponent(match[1]);
                    if (!geniusLinks.includes(decoded)) geniusLinks.push(decoded);
                }
            }
            debug('✅ Found Genius links via Google:', geniusLinks);
            return geniusLinks;
        } catch (err) {
            debug('[⚠️ WARNING] ❌ Failed to fetch Google search results:', err);
            return [];
        }
    }

    /**
     * [NEW & FIXED] Scrapes the lyrics from a given Genius URL.
     */
    async function scrapeGeniusUrl(url) {
        if (!url) return null;
        debug(`📄 Scraping Genius URL: ${url}`);
        try {
            const pageRes = await gmFetch(url, { 'User-Agent': 'Mozilla/5.0' });
            const doc = new DOMParser().parseFromString(pageRes.responseText, 'text/html');
            const containers = doc.querySelectorAll('div[data-lyrics-container="true"]');
            if (!containers.length) throw new Error('No lyrics containers found on page.');

            const blocks = [];
            containers.forEach(div => {
                const clone = div.cloneNode(true);
                clone.querySelectorAll('[data-exclude-from-selection="true"]').forEach(e => e.remove());
                blocks.push(clone.innerText.trim());
            });

            const lyrics = blocks.join('\n\n').trim();
            debug('✅ Successfully scraped lyrics from Genius page.');
            return lyrics;
        } catch (err) {
            debug(`[⚠️ WARNING] ❌ Failed to scrape Genius URL ${url}:`, err);
            return null;
        }
    }

    async function fetchTranslations(lrcText, geniusTr, title, artist) {
        try {
            const response = await fetch(BACKEND_URL, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ lrcText, geniusLyrics: geniusTr, title, artist }), // Changed key to match backend
            });
            if (!response.ok) {
                const errorBody = await response.text();
                debug('[❗ERROR] Backend server returned an error:', response.status, errorBody);
                return { rom: "", transl: "" };
            }
            const data = await response.json();
            debug('Received backend data:', data);
            return data;
        } catch (error) {
            debug('[❗ERROR] Failed to fetch from backend server:', error);
            return { rom: "", transl: "" };
        }
    }

    function parseLRCToArray(lrc) {
        if (!lrc) return [];
        const lines = [];
        const regex = /\[(\d+):(\d+)(?:\.(\d+))?\](.*)/g;
        for (const raw of lrc.split('\n')) {
            let matches, l = raw;
            while ((matches = regex.exec(l)) !== null) {
                const time = parseInt(matches[1], 10) * 60000 + parseInt(matches[2], 10) * 1000 + (matches[3] ? parseInt(matches[3].padEnd(3, '0'), 10) : 0);
                lines.push({ time, text: l.replace(/\[\d+:\d+(?:\.\d+)?\]/g, '').trim() });
            }
            regex.lastIndex = 0;
        }
        lines.sort((a, b) => a.time - b.time);
        if (lines.length && lines[0].time !== 0) lines.unshift({ time: 0, text: '' });
        return lines;
    }

    function mergeLRC(origArr, romArr, transArr) {
        const romMap = new Map(romArr.map(r => [r.time, r.text]));
        const transMap = new Map(transArr.map(t => [t.time, t.text]));
        return origArr.map(o => ({ time: o.time, text: o.text, roman: romMap.get(o.time) || '', trans: transMap.get(o.time) || '' }));
    }

    function parseLRC(lrc, romLrc, translLrc) {
        return mergeLRC(parseLRCToArray(lrc), parseLRCToArray(romLrc), parseLRCToArray(translLrc));
    }

    async function loadLyrics(title, artist, album, duration, onTransReady, manual = { flag: false, query: "" }) {
        if (!manual.flag) debug('Searching for lyrics:', title, artist, album, duration);
        else debug(`Manually searching lyrics: using user prompt "${manual.query}"...`);

        const trackKey = `${title}|${artist}`;
        let geniusLyrics = null;

        try {
            // --- 0) Attempt to get Genius lyrics first ---
            /*  PLACEHOLDER
            if (!manual.flag) {
                const geniusLinks = await fetchStrictGeniusLinks(title, artist);
                if (geniusLinks.length > 0) {
                    geniusLyrics = await scrapeGeniusUrl(geniusLinks[0]);
                }
            }
            */

            // --- 1) Manual override check ---
            if (lyricsConfig[trackKey]?.manualLrc && !manual.flag) {
                const { manualLrc, offset = 0 } = lyricsConfig[trackKey];
                onTransReady(parseLRC(manualLrc, '', '').map(l => ({ ...l, time: l.time + offset })));
                const { rom, transl } = await fetchTranslations(manualLrc, geniusLyrics, title, artist);
                onTransReady(parseLRC(manualLrc, rom, transl).map(l => ({ ...l, time: l.time + offset })));
                const searchRes = await fetch(`https://lrclib.net/api/search?q=${encodeURIComponent([title, artist, album].join(' '))}`);
                if (searchRes.ok) lastCandidates = await searchRes.json();
                return;
            }

            // --- 2) Fetch from lrclib (with fallback) ---
            const primaryMetadata = manual.flag ? manual.query : [title, artist, album].filter(Boolean).join(' ');
            let searchRes = await fetch(`https://lrclib.net/api/search?q=${encodeURIComponent(primaryMetadata)}`);
            if (!searchRes.ok) throw new Error('lrclib search failed');
            let searchData = await searchRes.json();

            if (!Array.isArray(searchData) || !searchData.some(c => c.syncedLyrics)) {
                if (!manual.flag && album) {
                    debug('Retrying lrclib search without album.');
                    const fallbackRes = await fetch(`https://lrclib.net/api/search?q=${encodeURIComponent([title, artist].join(' '))}`);
                    if (fallbackRes.ok) searchData = await fallbackRes.json();
                }
            }
            lastCandidates = Array.isArray(searchData) ? searchData : [];

            // --- 3) Pick best candidate ---
            let candidate = null,
                minDelta = Infinity;
            lastCandidates.filter(c => c.syncedLyrics).forEach(c => {
                const delta = Math.abs(Number(c.duration) - duration);
                if (delta < minDelta && delta < 8000) {
                    candidate = c;
                    minDelta = delta;
                }
            });
            if (!candidate && lastCandidates.length > 0) candidate = lastCandidates[0];

            if (!candidate || (!candidate.syncedLyrics && !candidate.plainLyrics)) {
                onTransReady([{ time: 0, text: 'Failed to find any lyrics for this track.', roman: '', trans: '' }]);
                return;
            }

            // --- 4) Process candidate and get translations ---
            const rawLrc = candidate.syncedLyrics || `[00:00.01] ${candidate.plainLyrics}`;
            onTransReady(parseLRC(rawLrc, '', '')); // Render original lyrics immediately
            const { rom, transl } = await fetchTranslations(rawLrc, geniusLyrics, title, artist);
            onTransReady(parseLRC(rawLrc, rom, transl));

        } catch (e) {
            // alert(`Error while displaying lrc: ${e} \n\n\n Please report this to \n\nhttps://github.com/jayxdcode/src-backend/issues\n\nalongside with a screenshot of this alert.`);
            debug('[❗ERROR] [Lyrics] loadLyrics error:', `${e}`);
            onTransReady([{ time: 0, text: 'An error occurred while loading lyrics.', roman: '', trans: '' }]);
        }
    }

    function parseTimeString(str) {
        if (!str) return 0;
        const parts = str.split(':').map(Number);
        return parts.length === 2 ? (parts[0] * 60 + parts[1]) * 1000 : (parts.length === 3 ? (parts[0] * 3600 + parts[1] * 60 + parts[2]) * 1000 : 0);
    }

    function addTimeJumpListener() {
        const lyricLinesWithTimestamp = document.querySelectorAll('#tm-lyrics-lines [timestamp]');

        lyricLinesWithTimestamp.forEach(element => {
            const timestampValue = element.getAttribute('timestamp');
            if (timestampValue) {
                element.addEventListener('click', () => timeJump(timestampValue));
            }
        });
    }

    async function getTrackInfo() {
        const bar = document.querySelector('[data-testid="now-playing-bar"], [data-testid="main-view-player-bar"], [data-testid="bottom-bar"], footer');
        if (!bar) return null;
        const titleEl = bar.querySelector('[data-testid="context-item-info-title"] [data-testid="context-item-link"], [data-testid="nowplaying-track-link"], [data-testid="now-playing-widget-title"] a, .track-info__name a');
        const artistEl = bar.querySelector('[data-testid="context-item-info-artist"], [data-testid="nowplaying-artist"], [data-testid="now-playing-widget-artist"] a, .track-info__artists a');
        const title = titleEl?.textContent.trim() || '';
        const artist = artistEl?.textContent.trim() || '';
        const album = titleEl?.href ? await parseAl(titleEl.href) : '';
        const durationEl = bar.querySelector('[data-testid="playback-duration"], [data-testid="playback_duration"]');
        const duration = durationEl ? parseTimeString(durationEl.textContent.trim()) : null;
        currentTrackDur = duration;
        return { id: title + '|' + artist, title, artist, album, duration, bar };
    }

    function getProgressBarTimeMs(bar, durationMs) {
        if (!bar || !durationMs) return 0;
        const pbar = bar.querySelector('[data-testid="progress-bar"]');
        if (pbar?.style) {
            const match = pbar.style.cssText.match(/--progress-bar-transform:\s*([\d.]+)%/);
            if (match?.[1]) return durationMs * parseFloat(match[1]) / 100;
        }
        const slider = bar.querySelector('div[role="slider"][aria-valuenow]');
        if (slider) return Number(slider.getAttribute('aria-valuenow'));
        const input = bar.querySelector('input[type="range"]');
        if (input) return durationMs * Number(input.value) / Number(input.max);
        const posEl = bar.querySelector('[data-testid="player-position"]');
        if (posEl) return parseTimeString(posEl.textContent.trim());
        return 0;
    }

    function renderLyrics(currentIdx) {
        const linesDiv = document.getElementById('tm-lyrics-lines');
        if (!linesDiv) return;
        let html = '';
        const color = currentTheme === 'light' ? '#000' : '#fff';
        const subColor = currentTheme === 'light' ? '#555' : '#ccc';
        const start = Math.max(0, currentIdx - 70);
        const end = Math.min(lyricsData.length - 1, currentIdx + 70);

        for (let i = start; i <= end; i++) {
            const ln = lyricsData[i];

            if (!ln.text && !ln.roman && !ln.trans) {
                html += `<div class="tm-lyric-line" style="min-height:1.6em;"> </div>`;
                continue;
            }
            const lineClass = i === currentIdx ? `tm-lrc-${i} tm-lyric-current` : `tm-lrc-${i} tm-lyric-line`;
            const lineStyle = `white-space: pre-wrap; color:${color}; ${i === currentIdx ? "font-weight:bold;" : "opacity:.7;"} margin:10px 0; min-height:1.6em; display:block;`;
            html += `<div class="${lineClass}" style="${lineStyle}" timestamp=${ln.time}>${ln.text || ' '}`;
            if (ln.roman && ln.text.trim() !== ln.roman.trim()) html += `<div style="font-size:.75em; color:${subColor}; margin-top:2px;">${ln.roman}</div>`;
            if (ln.trans && ln.text.trim() !== ln.trans.trim()) html += `<div style="font-size:.75em; color:${subColor}; margin-top:2px;">${ln.trans}</div>`;
            html += `</div>`;
        }
        linesDiv.innerHTML = html;

        const currElem = linesDiv.querySelector('.tm-lyric-current');
        if (currElem) {
            linesDiv.scrollTop = currElem.offsetTop - linesDiv.clientHeight / 2 + currElem.offsetHeight / 2;
        }

        addTimeJumpListener();
    }

    function syncLyrics(bar, durationMs) {
        if (!bar || !lyricsData || lyricsData.length === 0) return;
        let t = getProgressBarTimeMs(bar, durationMs);
        if (lyricsData.length === 1) {
            if (lastRenderedIdx !== 0) {
                renderLyrics(0);
                lastRenderedIdx = 0;
            }
            return;
        }
        let idx = lyricsData.findIndex((line, i) => i === lyricsData.length - 1 || (line.time <= t && t < lyricsData[i + 1].time));
        if (idx === -1) idx = lyricsData.length - 1;
        if (idx !== lastRenderedIdx) {
            renderLyrics(idx);
            lastRenderedIdx = idx;
        }
    }

    function setupProgressSync(bar, durationMs) {
        if (!bar) return;
        if (observer) observer.disconnect();
        if (syncIntervalId) clearInterval(syncIntervalId);
        const pbar = bar.querySelector('[data-testid="progress-bar"], div[role="slider"]');
        if (pbar) {
            observer = new MutationObserver(() => syncLyrics(bar, durationMs));
            observer.observe(pbar, { attributes: true, attributeFilter: ['style', 'aria-valuenow'] });
        }
        syncIntervalId = setInterval(() => syncLyrics(bar, durationMs), 300);
    }

    /*
       LRC time jump logic
    */
    // window.timeJump =
    function timeJump(timestamp) {
        const progressInput = document.querySelector("[data-testid='playback-progressbar'] input");

        const minOffset = 100;
        const maxOffset = 200;

        const randOffset = Math.random() * (maxOffset - minOffset) + minOffset;

        const seekTo = Math.min(timestamp, progressInput.max);
        progressInput.value = seekTo - randOffset;

        progressInput.dispatchEvent(new Event('input', { bubbles: true }));
        progressInput.dispatchEvent(new Event('change', { bubbles: true }));
    };

    async function poller() {
        try {
            const info = await getTrackInfo();
            if (!info || !info.title || !info.artist) return;
            if (info.id !== currentTrackId) {
                debug('Track changed:', info.title, '-', info.artist);
                currentTrackId = info.id;
                currInf = info;
                lyricsData = null;
                lastRenderedIdx = -1;
                const lines = document.getElementById('tm-lyrics-lines');
                if (lines) lines.innerHTML = '<em>Loading lyrics...</em>';
                await loadLyrics(info.title, info.artist, info.album, info.duration, (parsed) => {
                    lyricsData = parsed;
                    renderLyrics(0);
                    setupProgressSync(info.bar, info.duration);
                });
            }
        } catch (e) {
            debug('[❗ERROR] [Poller Error]', e);
        }
    }

    /**
     * Creates and appends a hidden <div> to the page to serve as a log container.
     * This is called once during the script's initialization.
     */
    function setupLogElement() {
        // Create the main container for logs
        const logs = document.createElement('div');
        logs.id = 'tm-logs';

        // Style it to be hidden by default but available for inspection
        Object.assign(logs.style, {
            position: 'fixed',
            bottom: '10px',
            right: '10px',
            width: '400px',
            height: '300px',
            background: 'rgba(0, 0, 0, 0.8)',
            color: '#0f0',
            fontFamily: 'monospace',
            fontSize: '12px',
            zIndex: '10001',
            overflowY: 'scroll',
            padding: '10px',
            border: '1px solid #333',
            borderRadius: '5px',
            display: 'none' // Hidden by default
        });

        // Add it to the page
        document.body.appendChild(logs);

        console.log('[Lyrics] Log element created. To view it, run this in the console:');
        console.log("document.getElementById('tm-logs').style.display = 'block';");
    }

    // Example of how to call it when your script starts:
    // setupLogElement();

    // function debug(...args) { console.log('[Lyrics]', ...args); }

    /**
     * Logs messages to the console and a dedicated <div> for on-page debugging.
     * @param {...any} args - The values to log.
     */
    function debug(...args) {
        // Also log to the standard developer console
        console.log('[Lyrics]', ...args);

        // Find the log container element on the page
        const logs = document.body.querySelector('#tm-logs');
        if (logs) {
            // Format arguments for HTML display, handling objects with JSON.stringify
            const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg).join(' ');
            // Append the new log message
            logs.innerHTML += `<div style='margin: .75em;'>${message}</div>`;
            // Auto-scroll to the bottom
            logs.scrollTop = logs.scrollHeight;
        }
    }

    function init() {
        setupLogElement();

        debug('Initializing Lyrics Panel');
        createPanel();
        window.addEventListener('resize', debounce(handleViewportChange, 250));
        setInterval(poller, POLL_INTERVAL);
    }

    // Wait for the main UI to be available before initializing
    const readyObserver = new MutationObserver((mutations, obs) => {
        if (document.querySelector('[data-testid="now-playing-bar"], [data-testid="main-view-player-bar"]')) {
            obs.disconnect();
            init();
        }
    });
    readyObserver.observe(document.body, { childList: true, subtree: true });
// -- end --
})();