Lihuelworks' YouTube Subtitle Downloader (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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Lihuelworks' YouTube Subtitle Downloader (Manual Trigger) with TrustedHTML Bypass
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Fetch subtitles as SRT with manual trigger, bypass TrustedHTML policy, and insert a button (with spinner) into a Polymer dropdown element
// @match        *://www.youtube.com/watch?v*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @license MIT 
// @run-at       document-end
// @supportURL   https://github.com/lihuelworks/youtube_translation_button_restorer/issues
// @contributionURL https://github.com/lihuelworks/youtube_translation_button_restorer#donate
// ==/UserScript==


// Apply container-specific CSS styles outside of the function using GM_addStyle
GM_addStyle(`
    .ytd-popup-container.style-scope {
       height: 250px;
       max-height: none;
       overflow: hidden;
   }

   #lihuelworks-subtitle-container:hover {
           background-color: var(--yt-spec-10-percent-layer);
   }
.spinner {
           border: 3px solid #ccc;
           border-top: 3px solid #333;
           border-radius: 50%;
           width: 15px;
           height: 15px;
           margin-left: 20px;
           animation: spin 0.6s linear infinite;
       }
       @keyframes spin {
           0% { transform: rotate(0deg); }
           100% { transform: rotate(360deg); }
       }
   ;
`);

// Main function to handle the process
(function() {
   'use strict';

   if (window.trustedTypes && trustedTypes.createPolicy) {
       if (!trustedTypes.defaultPolicy) {
           const passThroughFn = (x) => x;
           trustedTypes.createPolicy('default', {
               createHTML: passThroughFn,
               createScriptURL: passThroughFn,
               createScript: passThroughFn,
           });
       }
   }

   function getVideoID() {
       return new URLSearchParams(window.location.search).get("v");
   }

   function fetchCaptions(videoID) {
       showSpinner();
       GM_xmlhttpRequest({
           method: "GET",
           url: `https://www.youtube.com/watch?v=${videoID}`,
           onload: function(response) {
               const match = response.responseText.match(/"captionTracks":(\[.*?\])/);
               if (match) {
                   const captions = JSON.parse(match[1]);
                   const captionUrl = captions[0].baseUrl.replace(/\\u0026/g, "&");
                   fetchSubtitle(captionUrl);
               } else {
                   alert("No captions found.");
                   hideSpinner();
               }
           }
       });
   }

   function fetchSubtitle(url) {
       GM_xmlhttpRequest({
           method: "GET",
           url: url,
           onload: function(response) {
               const srtData = xmlToSrt(response.responseText);
               downloadSrtFile(srtData);
               hideSpinner();
           }
       });
   }

   function xmlToSrt(xml) {
       const parser = new DOMParser();
       const xmlDoc = parser.parseFromString(xml, "text/xml");
       let srt = "";
       let counter = 1;
       xmlDoc.querySelectorAll("body > p").forEach((node) => {
           let start = parseFloat(node.getAttribute("d"));
           let duration = parseFloat(node.getAttribute("d"));
           let end = start + duration;
           let startTime = formatTime(start);
           let endTime = formatTime(end);
           let text = decodeHtmlEntities(node.textContent);
           srt += `${counter}\n${startTime} --> ${endTime}\n${text}\n\n`;
           counter++;
       });
       return srt;
   }

   function decodeHtmlEntities(text) {
       const element = document.createElement('div');
       if (text) {
           element.innerHTML = text;
           // Use innerText to extract correctly decoded characters
           return element.innerText || element.textContent;
       }
       return text;
   }

   function formatTime(seconds) {
       let date = new Date(0);
       date.setSeconds(seconds);
       return date.toISOString().substr(11, 12).replace(".", ",");
   }

   // Function to download the SRT file
   function downloadSrtFile(srtContent) {
       const blob = new Blob([srtContent], { type: "text/plain; charset=utf-8" });
       const url = URL.createObjectURL(blob);
       const a = document.createElement("a");
       a.href = url;
       a.download = "captions.srt";
       document.body.appendChild(a);
       a.click();
       document.body.removeChild(a);
   }

   function showSpinner() {
       const button = document.getElementById("lihuelworks-subtitle-getter");
       if (button) {
           button.innerHTML = '';
           const spinner = document.createElement("div");
           spinner.className = 'spinner';
           button.appendChild(spinner);
           button.disabled = true;
       }
   }

   function hideSpinner() {
       const button = document.getElementById("lihuelworks-subtitle-getter");
       if (button) {
           button.innerText = "Transcription";
           button.disabled = false;
       }
   }

   function createButton() {
       const container = document.querySelector(".ytd-popup-container.style-scope > .ytd-menu-popup-renderer.style-scope");
       if (!container) {
           console.log("Menu container not found, retrying...");
           setTimeout(createButton, 1000);
           return;
       }

       const divContainer = document.createElement("div");
       divContainer.id = "lihuelworks-subtitle-container";
       divContainer.style.display = "flex";
       divContainer.style.alignItems = "center";

       const svgIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
       svgIcon.setAttribute("width", "16");
       svgIcon.setAttribute("height", "16");
       svgIcon.setAttribute("fill", "currentColor");
       svgIcon.setAttribute("class", "bi bi-body-text");
       svgIcon.setAttribute("viewBox", "0 0 16 16");
       svgIcon.style.marginLeft = "20px";
       svgIcon.style.paddingTop = "-3px";
       svgIcon.style.textAlign = "baseline";

       const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
       path.setAttribute("fill-rule", "evenodd");
       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");
       svgIcon.appendChild(path);

       const button = document.createElement("button");
       button.id = "lihuelworks-subtitle-getter";
       button.classList.add("style-scope", "ytd-menu-service-item-renderer");
       button.innerText = "Transcription";
       button.style.flexBasis = "1e-09px";
       button.style.flexGrow = "1";
       button.style.flexShrink = "1";
       button.style.height = "36px";
       button.style.width = "auto";
       button.style.fontFamily = "Roboto, Arial, sans-serif";
       button.style.fontSize = "1.4rem";
       button.style.fontWeight = "400";
       button.style.lineHeight = "normal";
       button.style.textSizeAdjust = "100%";
       button.style.whiteSpace = "nowrap";
       button.style.whiteSpaceCollapse = "collapse";
       button.style.color = "rgb(241, 241, 241)";
       button.style.cursor = "pointer";
       button.style.border = "none";
       button.style.margin = "0";
       button.style.padding = "0";
       button.style.width = "auto";
       button.style.overflow = "visible";
       button.style.background = "transparent";
       button.style.color = "inherit";
       button.style.lineHeight = "normal";
       button.style.webkitFontSmoothing = "inherit";
       button.style.mozOsxFontSmoothing = "inherit";
       button.style.webkitAppearance = "none";

       button.addEventListener("click", function() {
           const videoID = getVideoID();
           fetchCaptions(videoID);
       });

       divContainer.appendChild(svgIcon);
       divContainer.appendChild(button);
       container.appendChild(divContainer);
   }

   createButton();
})();