RSS: FreshRSS or Webpage Read Aloud

Read aloud the current article in FreshRSS or the text from a webpage using a custom TTS API

目前为 2025-02-10 提交的版本。查看 最新版本

// ==UserScript==
// @name         RSS: FreshRSS or Webpage Read Aloud
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  Read aloud the current article in FreshRSS or the text from a webpage using a custom TTS API
// @author       Your Name
// @match        http://192.168.1.2:1030/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// ==/UserScript==

// @match        *://*/*
(function() {
    'use strict';

    // Add minimal styles for the audio element
    GM_addStyle(`
        #tts-audio-container {
            position: fixed;
            bottom: 75px;
            left: 20px;
            z-index: 1000;
            display: flex;
            align-items: center;
            gap: 0;
        }
        #tts-audio {
            width: 250px;
            height: 20px;
            background: transparent;
        }
        #tts-audio::-webkit-media-controls-panel {
            background: white;
        }
        #tts-audio::-webkit-media-controls-timeline {
            cursor: pointer;
        }
        #tts-clear-button {
            height: 20px;
            width: 24px;
            padding: 0;
            background: white;
            border: 1px solid #ddd;
            border-left: none;
            border-radius: 0 2px 2px 0;
            cursor: pointer;
            font-size: 12px;
            color: #666;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        #tts-clear-button:hover {
            background: #f0f0f0;
            color: #333;
        }
        #tts-clear-button:active {
            background: #e0e0e0;
        }
    `);

    // Function to extract text from the webpage or FreshRSS article
    function extractText() {
        const isFreshRSS = document.querySelector('.flux_content') !== null;
        if (isFreshRSS) {
            const articleContent = document.querySelector('.flux.active.current .flux_content .text');
            if (articleContent) {
                let text = articleContent.innerText.trim();

                // Remove "Summarize" from the beginning and end
                if (text.startsWith('Summarize')) {
                    text = text.substring('Summarize'.length).trim();
                }
                if (text.endsWith('Summarize')) {
                    text = text.substring(0, text.length - 'Summarize'.length).trim();
                }

                return text;
            }
        } else {
            const elementsToRemove = document.querySelectorAll('script, style');
            elementsToRemove.forEach(el => el.remove());
            return document.body.innerText.trim();
        }
        return null;
    }

    // Function to fetch and set audio source
    async function fetchAudioSource() {
        const text = extractText();
        if (!text) {
            throw new Error('No text content found on the webpage.');
        }

        const audioPlayer = document.getElementById('tts-audio');
        const apiUrl = `http://192.168.1.2:1202/api/tts?download=true&shardLength=500&thread=500&text=${encodeURIComponent(text)}`;

        return new Promise((resolve, reject) => {
            const xhr = new XMLHttpRequest();
            xhr.open('GET', apiUrl, true);
            xhr.responseType = 'blob';

            xhr.onload = function() {
                if (xhr.status === 200) {
                    const blob = new Blob([xhr.response], { type: 'audio/mpeg' });
                    const url = URL.createObjectURL(blob);

                    // Clean up old URL if it exists
                    if (audioPlayer.dataset.blobUrl) {
                        URL.revokeObjectURL(audioPlayer.dataset.blobUrl);
                    }
                    audioPlayer.dataset.blobUrl = url;
                    audioPlayer.src = url;
                    resolve();
                } else {
                    reject(new Error(`HTTP error! status: ${xhr.status}`));
                }
            };

            xhr.onerror = () => reject(new Error('Network request failed'));
            xhr.send();
        });
    }

    // Initialize the audio player
    function initializeAudioPlayer() {
        const container = document.createElement('div');
        container.id = 'tts-audio-container';

        const audioPlayer = document.createElement('audio');
        audioPlayer.id = 'tts-audio';
        audioPlayer.controls = true;
        audioPlayer.innerHTML = 'Your browser does not support the audio element.';

        const clearButton = document.createElement('button');
        clearButton.id = 'tts-clear-button';
        clearButton.innerHTML = '✕';
        clearButton.title = 'Clear audio';

        let isInitialPlay = true;

        clearButton.addEventListener('click', () => {
            audioPlayer.pause();
            if (audioPlayer.dataset.blobUrl) {
                URL.revokeObjectURL(audioPlayer.dataset.blobUrl);
                delete audioPlayer.dataset.blobUrl;
            }
            isInitialPlay = true;
        });

        audioPlayer.addEventListener('play', async (e) => {
            if (isInitialPlay) {
                e.preventDefault();
                audioPlayer.pause();
                try {
                    await fetchAudioSource();
                    isInitialPlay = false;
                    audioPlayer.play();
                } catch (error) {
                    console.error('Error fetching audio:', error);
                    isInitialPlay = true;
                }
            }
        });

        audioPlayer.addEventListener('ended', () => {
            isInitialPlay = true;
        });

        container.appendChild(audioPlayer);
        container.appendChild(clearButton);
        document.body.appendChild(container);
    }

    // Initialize the audio player
    initializeAudioPlayer();
})();