RSS: FreshRSS Read Aloud

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

  1. // ==UserScript==
  2. // @name RSS: FreshRSS Read Aloud
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.6
  5. // @description Read aloud the current article in FreshRSS or the text from a webpage using a custom TTS API
  6. // @author Your Name
  7. // @homepage https://greasyfork.org/en/scripts/526473
  8. // @match http://192.168.1.2:1030/*
  9. // @grant GM_xmlhttpRequest
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. // Function to extract text from the webpage or FreshRSS article
  16. function extractText() {
  17. const isFreshRSS = document.querySelector('.flux_content') !== null;
  18. if (isFreshRSS) {
  19. const articleContent = document.querySelector('.flux.active.current .flux_content .text');
  20. if (articleContent) {
  21. let text = articleContent.innerText.trim();
  22.  
  23. // Remove "Summarize" from the beginning and end
  24. if (text.startsWith('✨Summarize')) {
  25. text = text.substring('✨Summarize'.length).trim();
  26. }
  27. if (text.endsWith('✨Summarize')) {
  28. text = text.substring(0, text.length - '✨Summarize'.length).trim();
  29. }
  30.  
  31. return text;
  32. }
  33. } else {
  34. const elementsToRemove = document.querySelectorAll('script, style');
  35. elementsToRemove.forEach(el => el.remove());
  36. return document.body.innerText.trim();
  37. }
  38. return null;
  39. }
  40.  
  41. // Function to check if the text contains Chinese characters
  42. function containsChinese(text) {
  43. const chineseRegex = /[\u4e00-\u9fa5]/;
  44. return chineseRegex.test(text);
  45. }
  46.  
  47. // Function to fetch and set audio source
  48. async function fetchAudioSource() {
  49. const text = extractText();
  50. if (!text) {
  51. throw new Error('No text content found on the webpage.');
  52. }
  53.  
  54. const audioPlayer = document.getElementById('tts-audio');
  55. let apiUrl = `http://192.168.1.2:1209/api/tts?download=true&shardLength=10000&thread=1000&text=${encodeURIComponent(text)}`;
  56.  
  57. // Add voiceName parameter only if the text does NOT contain Chinese characters
  58. if (!containsChinese(text)) {
  59. apiUrl += '&voiceName=en-US-AndrewNeural';
  60. }
  61.  
  62. return new Promise((resolve, reject) => {
  63. GM_xmlhttpRequest({
  64. method: 'GET',
  65. url: apiUrl,
  66. responseType: 'blob',
  67. onload: function(response) {
  68. if (response.status === 200) {
  69. const blob = new Blob([response.response], { type: 'audio/mpeg' });
  70. const url = URL.createObjectURL(blob);
  71.  
  72. // Clean up old URL if it exists
  73. if (audioPlayer.dataset.blobUrl) {
  74. URL.revokeObjectURL(audioPlayer.dataset.blobUrl);
  75. }
  76. audioPlayer.dataset.blobUrl = url;
  77. audioPlayer.src = url;
  78. resolve();
  79. } else {
  80. reject(new Error(`HTTP error! status: ${response.status}`));
  81. }
  82. },
  83. onerror: function(error) {
  84. reject(new Error('Network request failed: ' + error.message));
  85. }
  86. });
  87. });
  88. }
  89.  
  90. // Function to display a message at the center of the screen
  91. function showMessage(message) {
  92. const existingMessage = document.getElementById('tts-message');
  93. if (existingMessage) {
  94. existingMessage.textContent = message;
  95. return existingMessage;
  96. }
  97.  
  98. const messageElement = document.createElement('div');
  99. messageElement.id = 'tts-message';
  100. messageElement.textContent = message;
  101. messageElement.style.cssText = `
  102. position: fixed;
  103. top: 50%;
  104. left: 50%;
  105. transform: translate(-50%, -50%);
  106. background-color: rgba(0, 0, 0, 0.8);
  107. color: white;
  108. padding: 10px 20px;
  109. border-radius: 5px;
  110. font-size: 16px;
  111. z-index: 10000;
  112. opacity: 1;
  113. transition: opacity 0.5s ease-out;
  114. `;
  115. document.body.appendChild(messageElement);
  116. return messageElement;
  117. }
  118.  
  119. // Function to remove the message
  120. function removeMessage() {
  121. const messageElement = document.getElementById('tts-message');
  122. if (messageElement) {
  123. messageElement.style.opacity = '0';
  124. setTimeout(() => {
  125. if (messageElement.parentNode) {
  126. messageElement.parentNode.removeChild(messageElement);
  127. }
  128. }, 500);
  129. }
  130. }
  131.  
  132. // Initialize the audio player
  133. function initializeAudioPlayer() {
  134. const container = document.createElement('div');
  135. container.id = 'tts-audio-container';
  136. container.style.cssText = 'display: inline-block; margin: 0; padding: 0; vertical-align: middle; margin-top: 8px;';
  137.  
  138. const audioPlayer = document.createElement('audio');
  139. audioPlayer.id = 'tts-audio';
  140. audioPlayer.controls = true;
  141. audioPlayer.style.cssText = 'width: 50px; height: 20px; opacity: 0.3; margin: 0; padding: 0;';
  142. audioPlayer.innerHTML = 'Your browser does not support the audio element.';
  143.  
  144. let isInitialPlay = true;
  145.  
  146. audioPlayer.addEventListener('play', async (e) => {
  147. if (isInitialPlay) {
  148. e.preventDefault();
  149. audioPlayer.pause();
  150.  
  151. const messageElement = showMessage('Preparing audio...');
  152.  
  153. try {
  154. await fetchAudioSource();
  155. isInitialPlay = false;
  156. audioPlayer.play();
  157. } catch (error) {
  158. console.error('Error fetching audio:', error);
  159. messageElement.textContent = 'Error preparing audio';
  160. setTimeout(removeMessage, 2000);
  161. isInitialPlay = true;
  162. }
  163. }
  164. });
  165.  
  166. audioPlayer.addEventListener('playing', () => {
  167. removeMessage();
  168. });
  169.  
  170. audioPlayer.addEventListener('ended', () => {
  171. if (audioPlayer.dataset.blobUrl) {
  172. URL.revokeObjectURL(audioPlayer.dataset.blobUrl);
  173. delete audioPlayer.dataset.blobUrl;
  174. }
  175. audioPlayer.src = '';
  176. isInitialPlay = true;
  177. });
  178.  
  179. container.appendChild(audioPlayer);
  180.  
  181. // Find the last .group div and insert the audio player after it
  182. const lastGroupDiv = document.querySelector('.group:last-of-type');
  183. if (lastGroupDiv) {
  184. lastGroupDiv.parentNode.insertBefore(container, lastGroupDiv.nextSibling);
  185. } else {
  186. console.error('Could not find the last .group element.');
  187. }
  188. }
  189.  
  190. // Initialize the audio player
  191. initializeAudioPlayer();
  192. })();