Lihuelworks' YouTube Subtitle Viewer (Manual Trigger) with TrustedHTML Bypass

Fetch subtitles as SRT with manual trigger, bypass TrustedHTML policy, and insert a button (with spinner) into a Polymer dropdown element

  1. // ==UserScript==
  2. // @name Lihuelworks' YouTube Subtitle Viewer (Manual Trigger) with TrustedHTML Bypass
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.5
  5. // @description Fetch subtitles as SRT with manual trigger, bypass TrustedHTML policy, and insert a button (with spinner) into a Polymer dropdown element
  6. // @match *://www.youtube.com/watch?v*
  7. // @grant GM_xmlhttpRequest
  8. // @grant GM_addStyle
  9. // @license MIT
  10. // @run-at document-end
  11. // @supportURL https://github.com/lihuelworks/youtube_translation_button_restorer/issues
  12. // @contributionURL https://github.com/lihuelworks/youtube_translation_button_restorer#donate
  13. // ==/UserScript==
  14.  
  15.  
  16. // Apply container-specific CSS styles outside of the function using GM_addStyle
  17. GM_addStyle(`
  18. .ytd-popup-container.style-scope {
  19. height: 250px;
  20. max-height: none;
  21. overflow: hidden;
  22. }
  23.  
  24. #lihuelworks-subtitle-container:hover {
  25. background-color: var(--yt-spec-10-percent-layer);
  26. }
  27. .spinner {
  28. border: 3px solid #ccc;
  29. border-top: 3px solid #333;
  30. border-radius: 50%;
  31. width: 15px;
  32. height: 15px;
  33. margin-left: 20px;
  34. animation: spin 0.6s linear infinite;
  35. }
  36. @keyframes spin {
  37. 0% { transform: rotate(0deg); }
  38. 100% { transform: rotate(360deg); }
  39. }
  40. ;
  41. `);
  42.  
  43. // Main function to handle the process
  44. (function() {
  45. 'use strict';
  46.  
  47. if (window.trustedTypes && trustedTypes.createPolicy) {
  48. if (!trustedTypes.defaultPolicy) {
  49. const passThroughFn = (x) => x;
  50. trustedTypes.createPolicy('default', {
  51. createHTML: passThroughFn,
  52. createScriptURL: passThroughFn,
  53. createScript: passThroughFn,
  54. });
  55. }
  56. }
  57.  
  58. function getVideoID() {
  59. return new URLSearchParams(window.location.search).get("v");
  60. }
  61.  
  62. function fetchCaptions(videoID) {
  63. showSpinner();
  64. GM_xmlhttpRequest({
  65. method: "GET",
  66. url: `https://www.youtube.com/watch?v=${videoID}`,
  67. onload: function(response) {
  68. const match = response.responseText.match(/"captionTracks":(\[.*?\])/);
  69. if (match) {
  70. const captions = JSON.parse(match[1]);
  71. const captionUrl = captions[0].baseUrl.replace(/\\u0026/g, "&");
  72. fetchSubtitle(captionUrl);
  73. } else {
  74. alert("No captions found.");
  75. hideSpinner();
  76. }
  77. }
  78. });
  79. }
  80.  
  81. function fetchSubtitle(url) {
  82. GM_xmlhttpRequest({
  83. method: "GET",
  84. url: url,
  85. onload: function(response) {
  86. const srtData = xmlToSrt(response.responseText);
  87. openInNewTab(srtData);
  88. hideSpinner();
  89. }
  90. });
  91. }
  92.  
  93. function xmlToSrt(xml) {
  94. const parser = new DOMParser();
  95. const xmlDoc = parser.parseFromString(xml, "text/xml");
  96. let srt = "";
  97. let counter = 1;
  98. xmlDoc.querySelectorAll("text").forEach((node) => {
  99. let start = parseFloat(node.getAttribute("start"));
  100. let duration = parseFloat(node.getAttribute("dur"));
  101. let end = start + duration;
  102. let startTime = formatTime(start);
  103. let endTime = formatTime(end);
  104. let text = decodeHtmlEntities(node.textContent);
  105. srt += `${counter}\n${startTime} --> ${endTime}\n${text}\n\n`;
  106. counter++;
  107. });
  108. return srt;
  109. }
  110.  
  111. function decodeHtmlEntities(text) {
  112. const element = document.createElement('div');
  113. if (text) {
  114. element.innerHTML = text;
  115. // Use innerText to extract correctly decoded characters
  116. return element.innerText || element.textContent;
  117. }
  118. return text;
  119. }
  120.  
  121. function formatTime(seconds) {
  122. let date = new Date(0);
  123. date.setSeconds(seconds);
  124. return date.toISOString().substr(11, 12).replace(".", ",");
  125. }
  126.  
  127. function openInNewTab(srtContent) {
  128. const blob = new Blob([srtContent], { type: "text/plain; charset=utf-8" });
  129. const url = URL.createObjectURL(blob);
  130. const newTab = window.open(url, "_blank");
  131. if (!newTab) {
  132. alert("Please allow popups for this action to work.");
  133. }
  134. }
  135.  
  136.  
  137. function showSpinner() {
  138. const button = document.getElementById("lihuelworks-subtitle-getter");
  139. if (button) {
  140. button.innerHTML = '';
  141. const spinner = document.createElement("div");
  142. spinner.className = 'spinner';
  143. button.appendChild(spinner);
  144. button.disabled = true;
  145. }
  146. }
  147.  
  148. function hideSpinner() {
  149. const button = document.getElementById("lihuelworks-subtitle-getter");
  150. if (button) {
  151. button.innerText = "Transcription";
  152. button.disabled = false;
  153. }
  154. }
  155.  
  156. function createButton() {
  157. const container = document.querySelector(".ytd-popup-container.style-scope > .ytd-menu-popup-renderer.style-scope");
  158. if (!container) {
  159. console.log("Menu container not found, retrying...");
  160. setTimeout(createButton, 1000);
  161. return;
  162. }
  163.  
  164. const divContainer = document.createElement("div");
  165. divContainer.id = "lihuelworks-subtitle-container";
  166. divContainer.style.display = "flex";
  167. divContainer.style.alignItems = "center";
  168.  
  169. const svgIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  170. svgIcon.setAttribute("width", "16");
  171. svgIcon.setAttribute("height", "16");
  172. svgIcon.setAttribute("fill", "currentColor");
  173. svgIcon.setAttribute("class", "bi bi-body-text");
  174. svgIcon.setAttribute("viewBox", "0 0 16 16");
  175. svgIcon.style.marginLeft = "20px";
  176. svgIcon.style.paddingTop = "-3px";
  177. svgIcon.style.textAlign = "baseline";
  178.  
  179. const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
  180. path.setAttribute("fill-rule", "evenodd");
  181. path.setAttribute("d", "M0 .5A.5.5 0 0 1 .5 0h4a.5.5 0 0 1 0 1h-4A.5.5 0 0 1 0 .5m0 2A.5.5 0 0 1 .5 2h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5m9 0a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m-9 2A.5.5 0 0 1 .5 4h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5m5 0a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m7 0a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5m-12 2A.5.5 0 0 1 .5 6h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5m8 0a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m-8 2A.5.5 0 0 1 .5 8h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m7 0a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5m-7 2a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 0 1h-8a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5");
  182. svgIcon.appendChild(path);
  183.  
  184. const button = document.createElement("button");
  185. button.id = "lihuelworks-subtitle-getter";
  186. button.classList.add("style-scope", "ytd-menu-service-item-renderer");
  187. button.innerText = "Transcription";
  188. button.style.flexBasis = "1e-09px";
  189. button.style.flexGrow = "1";
  190. button.style.flexShrink = "1";
  191. button.style.height = "36px";
  192. button.style.width = "auto";
  193. button.style.fontFamily = "Roboto, Arial, sans-serif";
  194. button.style.fontSize = "1.4rem";
  195. button.style.fontWeight = "400";
  196. button.style.lineHeight = "normal";
  197. button.style.textSizeAdjust = "100%";
  198. button.style.whiteSpace = "nowrap";
  199. button.style.whiteSpaceCollapse = "collapse";
  200. button.style.color = "rgb(241, 241, 241)";
  201. button.style.cursor = "pointer";
  202. button.style.border = "none";
  203. button.style.margin = "0";
  204. button.style.padding = "0";
  205. button.style.width = "auto";
  206. button.style.overflow = "visible";
  207. button.style.background = "transparent";
  208. button.style.color = "inherit";
  209. button.style.lineHeight = "normal";
  210. button.style.webkitFontSmoothing = "inherit";
  211. button.style.mozOsxFontSmoothing = "inherit";
  212. button.style.webkitAppearance = "none";
  213.  
  214. button.addEventListener("click", function() {
  215. const videoID = getVideoID();
  216. fetchCaptions(videoID);
  217. });
  218.  
  219. divContainer.appendChild(svgIcon);
  220. divContainer.appendChild(button);
  221. container.appendChild(divContainer);
  222. }
  223.  
  224. createButton();
  225. })();

QingJ © 2025

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