Google Cloud Text-to-Speech AI Downloader

Add a Download button for Google Cloud Text-to-Speech AI.

// ==UserScript==
// @name         Google Cloud Text-to-Speech AI Downloader
// @description  Add a Download button for Google Cloud Text-to-Speech AI.
// @icon         https://www.google.com/s2/favicons?sz=64&domain=cloud.google.com
// @version      1.0
// @author       afkarxyz
// @namespace    https://github.com/afkarxyz/misc-scripts/
// @supportURL   https://github.com/afkarxyz/misc-scripts/issues
// @license      MIT
// @match        https://www.gstatic.com/cloud-site-ux/text_to_speech/text_to_speech.min.html
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    let lastResponse = null;
    let lastPayload = null;

    const originalOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(method, url) {
        this.customURL = url;
        if (url.includes('texttospeech.googleapis.com/v1beta1/text:synthesize')) {
            this.addEventListener('readystatechange', function() {
                if (this.readyState === 4) {
                    try {
                        const response = JSON.parse(this.responseText);
                        lastResponse = response.audioContent;
                    } catch (e) {}
                }
            });
        }
        originalOpen.apply(this, arguments);
    };

    const originalSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = function(data) {
        if (this.customURL && this.customURL.includes('texttospeech.googleapis.com/v1beta1/text:synthesize')) {
            try {
                lastPayload = typeof data === 'string' ? JSON.parse(data) : data;
            } catch (e) {}
        }
        originalSend.apply(this, arguments);
    };

    const base64ToArrayBuffer = base64 => {
        const binary = atob(base64);
        const buffer = new Uint8Array(binary.length);
        for (let i = 0; i < binary.length; i++) {
            buffer[i] = binary.charCodeAt(i);
        }
        return buffer.buffer;
    };

    const showCustomPopup = (message, type = 'success') => {
        const existingPopup = document.getElementById('custom-popup');
        if (existingPopup) {
            existingPopup.remove();
        }

        const popup = document.createElement('div');
        popup.id = 'custom-popup';
        popup.style.cssText = `
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%) translateY(-100%);
            z-index: 9999;
            background-color: ${type === 'success' ? '#4CAF50' : '#f44336'};
            color: white;
            padding: 0.938em 1.563em;
            border-radius: 0.313em;
            opacity: 0;
            transition: all 0.2s ease-in-out;
            text-align: center;
            max-width: 80%;
        `;
        popup.textContent = message;

        document.body.appendChild(popup);

        requestAnimationFrame(() => {
            popup.style.opacity = '1';
            popup.style.transform = 'translate(-50%, -50%)';
        });

        setTimeout(() => {
            popup.style.opacity = '0';
            popup.style.transform = 'translate(-50%, -50%) translateY(-100%)';
            
            setTimeout(() => {
                popup.remove();
            }, 300);
        }, 1000);
    };

    const downloadAudio = () => {
        if (!lastResponse || !lastPayload) {
            showCustomPopup('Generate audio first!', 'error');
            return;
        }
    
        const now = new Date();
        const timestamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`;
        
        const truncatedText = lastPayload.input.text.substring(0, 25) + '...';
        const filename = `${timestamp}_${lastPayload.voice.name}_${truncatedText}.wav`;
    
        const blob = new Blob([base64ToArrayBuffer(lastResponse)], { type: 'audio/wav' });
        const link = document.createElement('a');
        link.href = URL.createObjectURL(blob);
        link.download = filename;
        link.click();
        URL.revokeObjectURL(link.href);
        
        showCustomPopup('Audio downloaded successfully!');
        
        lastResponse = null;
        lastPayload = null;
    };

    const createDownloadButton = () => {
        const button = document.createElement('paper-button');
        button.id = 'downloadButton';
        button.className = 'state-ready';
        button.setAttribute('role', 'button');
        button.tabIndex = 0;

        const iconSpan = document.createElement('span');
        iconSpan.className = 'icon';
        iconSpan.style.width = '1.125em';
        iconSpan.style.height = '1.125em';

        const ironIcon = document.createElement('iron-icon');
        ironIcon.setAttribute('icon', 'file-download');
        ironIcon.style.width = '1.125em';
        ironIcon.style.height = '1.125em';
        iconSpan.appendChild(ironIcon);

        const labelSpan = document.createElement('span');
        labelSpan.className = 'label';
        labelSpan.style.paddingLeft = '0.25em';

        const readySpan = document.createElement('span');
        readySpan.className = 'ready';
        readySpan.textContent = 'Download';
        labelSpan.appendChild(readySpan);

        const buttonInner = document.createElement('span');
        buttonInner.className = 'button-inner';
        buttonInner.style.padding = '0 0.625em';
        buttonInner.appendChild(iconSpan);
        buttonInner.appendChild(labelSpan);

        button.appendChild(buttonInner);

        button.style.marginLeft = '0.625em';
        button.style.background = 'var(--google-blue-500)';
        button.style.color = '#fff';

        button.addEventListener('click', downloadAudio);
        return button;
    };

    const addDownloadButton = () => {
        const app = document.querySelector('ts-app');
        if (!app) {
            return;
        }

        const shadowRoot = app.shadowRoot;
        if (!shadowRoot) {
            return;
        }

        const controlPlayback = shadowRoot.querySelector('.control-playback');
        if (!controlPlayback) {
            return;
        }

        if (controlPlayback.querySelector('#downloadButton')) {
            return;
        }

        const existingButton = controlPlayback.querySelector('ts-button');
        if (existingButton) {
            existingButton.parentNode.insertBefore(createDownloadButton(), existingButton.nextSibling);
        }
    };

    const checkPageReadiness = () => {
        const app = document.querySelector('ts-app');
        if (app && app.shadowRoot) {
            addDownloadButton();
        } else {
            requestAnimationFrame(checkPageReadiness);
        }
    };

    checkPageReadiness();

    const observer = new MutationObserver(addDownloadButton);
    observer.observe(document.body, { childList: true, subtree: true });
})();