Nebula.tv Auto Translate

Extract subtitles and replace them inline with DeepL translations (500k chars/month free tier)!

  1. // ==UserScript==
  2. // @name Nebula.tv Auto Translate
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.0
  5. // @description Extract subtitles and replace them inline with DeepL translations (500k chars/month free tier)!
  6. // @author samu126
  7. // @match https://nebula.tv/*
  8. // @grant GM_addStyle
  9. // @grant GM_registerMenuCommand
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15. console.log('Nebula.tv Auto Translate loaded!');
  16.  
  17. // Load config from localStorage or use default values
  18. const defaultConfig = {
  19. targetLang: 'HU',
  20. deepLApiKey: '',
  21. subSize: 24,
  22. };
  23.  
  24. const config = JSON.parse(localStorage.getItem('subtitleTranslatorConfig')) || defaultConfig;
  25.  
  26. // Apply saved configuration
  27. const { targetLang, deepLApiKey, subSize } = config;
  28. const apiURL = 'https://api-free.deepl.com/v2/translate'; // Free tier api
  29. const seenSubtitles = new Set();
  30.  
  31. // Configuration page HTML
  32. function createConfigPage() {
  33. const configPage = document.createElement('div');
  34. configPage.id = 'configPage';
  35. configPage.innerHTML = `
  36. <div style="position: absolute; top: 10%; right: 10%; background: white; color: black; border: 1px solid black; padding: 20px; z-index: 9999; width: 300px;">
  37. <h3>DeepL Subtitle Translator Config</h3>
  38. <label for="targetLang">Target Language (e.g., HU for Hungarian):</label>
  39. <input type="text" id="targetLang" value="${targetLang}" style="width: 100%; margin-bottom: 10px;"/>
  40. <br/>
  41. <label for="deepLApiKey">DeepL API Key:</label>
  42. <input type="text" id="deepLApiKey" value="${deepLApiKey}" style="width: 100%; margin-bottom: 10px;"/>
  43. <br/>
  44. <label for="subSize">Subtitle size:</label>
  45. <input type="number" id="subSize" value="${subSize}" style="width: 100%; margin-bottom: 10px;"/>
  46. <br/>
  47. <button id="saveConfig" style="width: 100%; background-color: #4CAF50; color: white; padding: 10px;">Save</button>
  48. <button id="closeConfig" style="width: 100%; margin-top: 10px; background-color: red; color: white; padding: 10px;">Close</button>
  49. </div>
  50. `;
  51.  
  52. document.body.appendChild(configPage);
  53.  
  54. // Close the configuration page
  55. document.getElementById('closeConfig').addEventListener('click', () => {
  56. document.body.removeChild(configPage);
  57. });
  58.  
  59. // Save the configuration
  60. document.getElementById('saveConfig').addEventListener('click', () => {
  61. const newTargetLang = document.getElementById('targetLang').value.trim().toUpperCase();
  62. const newDeepLApiKey = document.getElementById('deepLApiKey').value.trim();
  63. const newSubSize = document.getElementById('subSize').value;
  64.  
  65. if (newTargetLang && newDeepLApiKey && newSubSize) {
  66. const newConfig = { targetLang: newTargetLang, deepLApiKey: newDeepLApiKey, subSize: newSubSize };
  67. localStorage.setItem('subtitleTranslatorConfig', JSON.stringify(newConfig));
  68.  
  69. // Reload the page to apply the new config
  70. location.reload();
  71. } else {
  72. alert('Please fill in all fields.');
  73. }
  74. });
  75. }
  76.  
  77. // Register the command in Tampermonkey's menu
  78. GM_registerMenuCommand('Open Subtitle Translator Config', createConfigPage);
  79.  
  80. function waitForElement(selector, callback) {
  81. const observer = new MutationObserver(() => {
  82. const element = document.querySelector(selector);
  83. if (element) {
  84. console.log('Found element:', element);
  85. observer.disconnect();
  86. callback(element);
  87. }
  88. });
  89. observer.observe(document.body, { childList: true, subtree: true });
  90. }
  91.  
  92. async function translateTextDeepL(text) {
  93. const params = new URLSearchParams();
  94. params.append('auth_key', deepLApiKey);
  95. params.append('text', text);
  96. params.append('target_lang', targetLang);
  97.  
  98. try {
  99. const response = await fetch(apiURL, {
  100. method: 'POST',
  101. body: params
  102. });
  103.  
  104. if (!response.ok) {
  105. throw new Error(`HTTP error! Status: ${response.status}`);
  106. }
  107.  
  108. const data = await response.json();
  109.  
  110. if (data.translations && data.translations.length > 0) {
  111. return data.translations[0].text;
  112. } else {
  113. return '[No translation returned]';
  114. }
  115. } catch (error) {
  116. console.error('DeepL translation error:', error);
  117. return '[Translation failed] ' + text;
  118. }
  119. }
  120.  
  121. function displayTranslatedSubtitle(text, subtitleDiv) {
  122. const translatedDiv = document.createElement('div');
  123. translatedDiv.classList.add('translated-subtitle'); // Add an identifier class
  124. translatedDiv.textContent = text;
  125.  
  126. Object.assign(translatedDiv.style, {
  127. position: 'absolute',
  128. bottom: '50px',
  129. left: '50%',
  130. transform: 'translateX(-50%)',
  131. textAlign: 'center',
  132. fontSize: subSize + 'px',
  133. color: 'white',
  134. backgroundColor: 'rgba(0,0,0,0.7)',
  135. padding: '10px',
  136. borderRadius: '5px',
  137. textShadow: '2px 2px 4px black',
  138. zIndex: 9999,
  139. fontFamily: 'Arial, sans-serif',
  140. width: 'auto'
  141. });
  142.  
  143. // Replace the original subtitle with the translated subtitle
  144. subtitleDiv.parentNode.replaceChild(translatedDiv, subtitleDiv);
  145. }
  146.  
  147. waitForElement('div[data-subtitles-container="true"]', (container) => {
  148. console.log('Subtitle container found!', container);
  149.  
  150. // Observe subtitle container for new subtitle text
  151. const observer = new MutationObserver((mutationsList) => {
  152. for (const mutation of mutationsList) {
  153. if (mutation.type === 'childList') {
  154. const subtitleDivs = container.querySelectorAll('div > div > div');
  155. subtitleDivs.forEach(async subDiv => {
  156. // Check if this subtitle has already been translated (based on an added class)
  157. if (subDiv.classList.contains('translated-subtitle')) {
  158. return; // Skip if already translated
  159. }
  160.  
  161. const subtitleText = subDiv.innerText.trim();
  162. if (subtitleText && !seenSubtitles.has(subtitleText)) {
  163. seenSubtitles.add(subtitleText);
  164. console.log('Original subtitle:', subtitleText);
  165.  
  166. // Hide the original subtitle immediately
  167. subDiv.style.visibility = 'hidden';
  168.  
  169. const translated = await translateTextDeepL(subtitleText);
  170. console.log('Translated subtitle:', translated);
  171.  
  172. // Display the translated subtitle in place of the original
  173. displayTranslatedSubtitle(translated, subDiv);
  174. }
  175. });
  176. }
  177. }
  178. });
  179.  
  180. observer.observe(container, { childList: true, subtree: true });
  181. console.log('Now observing and translating subtitles via DeepL...');
  182. });
  183. })();

QingJ © 2025

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