Cigi Spotify Translator

Extract, translate, and display Spotify lyrics with a language selector and manual translation trigger

// ==UserScript==
// @name         Cigi Spotify Translator
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Extract, translate, and display Spotify lyrics with a language selector and manual translation trigger
// @author       Raiwulf
// @match        *://*.spotify.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    const DEFAULT_LANGUAGE = 'en';
    let isTranslating = false;

    const languages = {
        // Popular languages
        en: 'English',
        es: 'Spanish',
        fr: 'French',
        de: 'German',
        it: 'Italian',
        pt: 'Portuguese',
        ru: 'Russian',
        ja: 'Japanese',
        ko: 'Korean',
        zh: 'Chinese',
        ar: 'Arabic',
        hi: 'Hindi',
        tr: 'Turkish',
        
        // Rest in alphabetical order
        af: 'Afrikaans',
        sq: 'Albanian',
        am: 'Amharic',
        hy: 'Armenian',
        az: 'Azerbaijani',
        eu: 'Basque',
        be: 'Belarusian',
        bn: 'Bengali',
        bs: 'Bosnian',
        bg: 'Bulgarian',
        ca: 'Catalan',
        ceb: 'Cebuano',
        co: 'Corsican',
        hr: 'Croatian',
        cs: 'Czech',
        da: 'Danish',
        nl: 'Dutch',
        eo: 'Esperanto',
        et: 'Estonian',
        fi: 'Finnish',
        fy: 'Frisian',
        gl: 'Galician',
        ka: 'Georgian',
        el: 'Greek',
        gu: 'Gujarati',
        ht: 'Haitian Creole',
        ha: 'Hausa',
        haw: 'Hawaiian',
        he: 'Hebrew',
        hmn: 'Hmong',
        hu: 'Hungarian',
        is: 'Icelandic',
        ig: 'Igbo',
        id: 'Indonesian',
        ga: 'Irish',
        jv: 'Javanese',
        kn: 'Kannada',
        kk: 'Kazakh',
        km: 'Khmer',
        rw: 'Kinyarwanda',
        ku: 'Kurdish',
        ky: 'Kyrgyz',
        lo: 'Lao',
        la: 'Latin',
        lv: 'Latvian',
        lt: 'Lithuanian',
        lb: 'Luxembourgish',
        mk: 'Macedonian',
        mg: 'Malagasy',
        ms: 'Malay',
        ml: 'Malayalam',
        mt: 'Maltese',
        mi: 'Maori',
        mr: 'Marathi',
        mn: 'Mongolian',
        my: 'Myanmar (Burmese)',
        ne: 'Nepali',
        no: 'Norwegian',
        ny: 'Nyanja (Chichewa)',
        or: 'Odia (Oriya)',
        ps: 'Pashto',
        fa: 'Persian',
        pl: 'Polish',
        pa: 'Punjabi',
        ro: 'Romanian',
        sm: 'Samoan',
        gd: 'Scots Gaelic',
        sr: 'Serbian',
        st: 'Sesotho',
        sn: 'Shona',
        sd: 'Sindhi',
        si: 'Sinhala',
        sk: 'Slovak',
        sl: 'Slovenian',
        so: 'Somali',
        su: 'Sundanese',
        sw: 'Swahili',
        sv: 'Swedish',
        tl: 'Tagalog (Filipino)',
        tg: 'Tajik',
        ta: 'Tamil',
        tt: 'Tatar',
        te: 'Telugu',
        th: 'Thai',
        tk: 'Turkmen',
        uk: 'Ukrainian',
        ur: 'Urdu',
        ug: 'Uyghur',
        uz: 'Uzbek',
        vi: 'Vietnamese',
        cy: 'Welsh',
        xh: 'Xhosa',
        yi: 'Yiddish',
        yo: 'Yoruba',
        zu: 'Zulu'
    };

    function getSavedLanguage() {
        return localStorage.getItem('spotifyLyricsTranslationLang') || DEFAULT_LANGUAGE;
    }

    function saveLanguage(lang) {
        localStorage.setItem('spotifyLyricsTranslationLang', lang);
    }

    async function translateText(text, targetLang) {
        const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`;
        try {
            const response = await fetch(url);
            const data = await response.json();
            return data[0][0][0];
        } catch (error) {
            console.error('Translation failed:', error);
            return '[Translation Error]';
        }
    }

    async function translateLyrics() {
        if (isTranslating) return;
        isTranslating = true;

        const targetLang = getSavedLanguage();
        const lyricsDivs = document.querySelectorAll('[data-testid="fullscreen-lyric"] div');

        document.querySelectorAll('[data-translated="true"]').forEach(el => el.remove());

        const originalLines = [];
        lyricsDivs.forEach((div, index) => {
            const originalText = div.textContent.trim();
            if (originalText && originalText !== "♪") {
                originalLines.push({ index, text: originalText });
            }
        });

        const translatedLines = await Promise.all(originalLines.map(async (line) => {
            const translatedText = await translateText(line.text, targetLang);
            return { index: line.index, translatedText };
        }));

        translatedLines.forEach(({ index, translatedText }) => {
            const targetDiv = lyricsDivs[index];
            const translationDiv = document.createElement('div');
            translationDiv.style.color = 'gray';
            translationDiv.style.fontStyle = 'italic';
            translationDiv.textContent = translatedText;
            translationDiv.setAttribute('data-translated', 'true');
            targetDiv.parentNode.insertBefore(translationDiv, targetDiv.nextSibling);
        });

        isTranslating = false;
    }

    function observeLyrics() {
        const targetNode = document.querySelector('[data-testid="lyrics-container"]') || document.querySelector('[data-testid="fullscreen-lyric"]');
        if (!targetNode) return;

        const observer = new MutationObserver(() => {
            translateLyrics();
        });

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

    function createHeader() {
        const header = document.createElement('div');
        header.style.cssText = `
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 12px 0;
            background: rgba(40, 40, 40, 0.95);
            width: 100%;
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
            margin: 0;
        `;

        const controlsContainer = document.createElement('div');
        controlsContainer.style.cssText = `
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 15px;
            max-width: 600px;
            width: 90%;
        `;

        const selectContainer = document.createElement('div');
        selectContainer.style.cssText = `
            position: relative;
            flex: 0 1 200px;
            min-width: 120px;
        `;

        const selectButton = document.createElement('button');
        selectButton.style.cssText = `
            width: 100%;
            padding: 8px;
            border: none;
            border-radius: 4px;
            background: rgba(80, 80, 80, 1);
            color: white;
            font-size: 14px;
            text-align: left;
            cursor: pointer;
            display: flex;
            justify-content: space-between;
            align-items: center;
        `;
        selectButton.textContent = languages[getSavedLanguage()];

        const dropdown = document.createElement('div');
        dropdown.style.cssText = `
            display: none;
            position: absolute;
            top: 100%;
            left: 0;
            right: 0;
            background: rgba(40, 40, 40, 0.98);
            border-radius: 4px;
            margin-top: 4px;
            max-height: 300px;
            overflow-y: auto;
            z-index: 1000;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
        `;

        const searchInput = document.createElement('input');
        searchInput.style.cssText = `
            width: calc(100% - 16px);
            margin: 8px;
            padding: 8px;
            border: none;
            border-radius: 4px;
            background: rgba(255, 255, 255, 0.1);
            color: white;
            font-size: 14px;
        `;
        searchInput.placeholder = 'Search language...';

        const optionsContainer = document.createElement('div');
        optionsContainer.style.cssText = `
            padding: 8px 0;
        `;

        function createLanguageOptions(filter = '') {
            optionsContainer.innerHTML = '';
            
            // Separate popular and other languages
            const popularLanguages = [
                'en', 'tr', 'pl', 'es', 'fr', 'de', 'pt', 
                'ja', 'it', 'nl'
            ];
            
            const entries = Object.entries(languages);
            const filteredEntries = entries.filter(([_, name]) => 
                name.toLowerCase().includes(filter.toLowerCase())
            );

            // Separate and sort entries
            const popularEntries = filteredEntries.filter(([code]) => 
                popularLanguages.includes(code)
            ).sort((a, b) => 
                popularLanguages.indexOf(a[0]) - popularLanguages.indexOf(b[0])
            );
            
            const otherEntries = filteredEntries.filter(([code]) => 
                !popularLanguages.includes(code)
            );

            // Create divider if both sections have items
            if (popularEntries.length > 0 && otherEntries.length > 0) {
                const divider = document.createElement('div');
                divider.style.cssText = `
                    padding: 8px 16px;
                    color: #888;
                    font-size: 12px;
                    text-transform: uppercase;
                    letter-spacing: 1px;
                `;
                divider.textContent = 'Other Languages';
                
                // Create and append all options
                [...popularEntries, divider, ...otherEntries].forEach(entry => {
                    if (entry instanceof HTMLElement) {
                        optionsContainer.appendChild(entry);
                        return;
                    }

                    const [code, name] = entry;
                    const option = document.createElement('div');
                    option.style.cssText = `
                        padding: 8px 16px;
                        cursor: pointer;
                        color: white;
                        &:hover {
                            background: rgba(255, 255, 255, 0.1);
                        }
                    `;
                    option.textContent = name;
                    option.addEventListener('click', () => {
                        selectButton.textContent = name;
                        dropdown.style.display = 'none';
                        saveLanguage(code);
                        document.querySelectorAll('[data-translated="true"]').forEach(el => el.remove());
                        translateLyrics();
                    });
                    optionsContainer.appendChild(option);
                });
            }
        }

        selectButton.addEventListener('click', () => {
            dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
            if (dropdown.style.display === 'block') {
                searchInput.focus();
            }
        });

        searchInput.addEventListener('input', (e) => {
            createLanguageOptions(e.target.value);
        });

        document.addEventListener('click', (e) => {
            if (!selectContainer.contains(e.target)) {
                dropdown.style.display = 'none';
            }
        });

        createLanguageOptions();
        dropdown.appendChild(searchInput);
        dropdown.appendChild(optionsContainer);
        selectContainer.appendChild(selectButton);
        selectContainer.appendChild(dropdown);

        const translateButton = document.createElement('button');
        translateButton.textContent = 'Translate';
        translateButton.style.cssText = `
            padding: 8px 16px;
            background-color: #1db954;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            font-weight: 500;
            min-width: 100px;
        `;

        translateButton.addEventListener('click', () => {
            document.querySelectorAll('[data-translated="true"]').forEach(el => el.remove());
            translateLyrics();
        });

        controlsContainer.appendChild(selectContainer);
        controlsContainer.appendChild(translateButton);
        header.appendChild(controlsContainer);

        const mainView = document.querySelector('.main-view-container__scroll-node-child');
        if (mainView) {
            mainView.insertBefore(header, mainView.firstChild);
        }
    }

    function waitForLyrics() {
        const lyricsContainer = document.querySelector('[data-testid="lyrics-container"]') || document.querySelector('[data-testid="fullscreen-lyric"]');
        if (lyricsContainer) {
            createHeader();
            observeLyrics();
            translateLyrics();
        } else {
            setTimeout(waitForLyrics, 1000);
        }
    }

    function checkForTranslation() {
        setInterval(() => {
            if (!document.querySelector('[data-translated="true"]') && !isTranslating) {
                translateLyrics();
            }
        }, 2000);
    }

    window.addEventListener('load', function () {
        waitForLyrics();
        checkForTranslation();
    });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址