您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Shows a transcript with clickable timestamps for any video with subtitles. Just enable subtitles and look in the description (or the dev console).
// ==UserScript== // @name YouTube Transcript for all videos with subs // @namespace https://baegus.cz // @version 0.1 // @description Shows a transcript with clickable timestamps for any video with subtitles. Just enable subtitles and look in the description (or the dev console). // @author Jaroslav Petrnoušek // @match https://www.youtube.com/* // @match https://youtube.com/* // @license MIT // @grant none // ==/UserScript== (function() { 'use strict'; function seekToTime (milliseconds) { window.scrollTo(0,0); const player = document.querySelector("video"); if (player) { player.currentTime = milliseconds / 1000; } } const pad = (num) => num.toString().padStart(2, "0"); function formatYouTubeTime(ms) { const totalSeconds = Math.floor(ms / 1000); const days = Math.floor(totalSeconds / (24 * 3600)); const remainingSeconds = totalSeconds % (24 * 3600); const hours = Math.floor(remainingSeconds / 3600); const minutes = Math.floor((remainingSeconds % 3600) / 60); const seconds = remainingSeconds % 60; // Construct the time string based on duration if (days > 0) { return `${days}:${pad(hours)}:${pad(minutes)}:${pad(seconds)}`; } else if (hours > 0) { return `${hours}:${pad(minutes)}:${pad(seconds)}`; } else { return `${minutes}:${pad(seconds)}`; } } function dispatchSubtitleData(subtitleData) { const event = new CustomEvent("youtubeSubtitleData", { detail: subtitleData }); window.dispatchEvent(event); } // Intercept Fetch requests const originalFetch = window.fetch; window.fetch = async function(...args) { try { const response = await originalFetch(...args); if (args[0] && typeof args[0] === "string" && args[0].includes("/api/timedtext")) { const clonedResponse = response.clone(); try { const data = await clonedResponse.json(); dispatchSubtitleData({ type: "fetch", url: args[0], data: data }); } catch (jsonError) { console.log("Error parsing subtitle JSON (fetch):", jsonError); } } return response; } catch (fetchError) { throw fetchError; } }; // Intercept XMLHttpRequest const originalXHROpen = XMLHttpRequest.prototype.open; const originalXHRSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function(...args) { this._url = args[1]; return originalXHROpen.apply(this, args); }; XMLHttpRequest.prototype.send = function(...args) { const xhr = this; if (xhr._url && xhr._url.includes("/api/timedtext")) { const originalOnLoad = xhr.onload; xhr.onload = function() { try { const data = JSON.parse(xhr.responseText); dispatchSubtitleData({ type: "xhr", url: xhr._url, data: data }); } catch (parseError) { console.log("Error parsing subtitle JSON (XHR):", parseError); } if (originalOnLoad) { originalOnLoad.apply(xhr, arguments); } }; const originalAddEventListener = xhr.addEventListener; xhr.addEventListener = function(event, callback, ...rest) { if (event === "load") { const wrappedCallback = function() { try { const data = JSON.parse(xhr.responseText); dispatchSubtitleData({ type: "xhr", url: xhr._url, data: data }); } catch (parseError) { console.log("Error parsing subtitle JSON (XHR addEventListener):", parseError); } return callback.apply(this, arguments); }; return originalAddEventListener.call(xhr, event, wrappedCallback, ...rest); } return originalAddEventListener.call(xhr, event, callback, ...rest); }; } return originalXHRSend.apply(this, args); }; console.log("'YouTube Transcript for all videos' is active. Enable subtitles to show transcript here in the console!"); window.addEventListener("youtubeSubtitleData", async function(event) { try { //console.log("Subtitle Data Intercepted:", event.detail); const subtitleData = event.detail.data; if (!event.detail.data) { throw new Error("No subtitle data"); } const timedTextData = []; const timeTextLines = []; subtitleData.events.forEach(event => { if (!event.segs) return; const segText = event.segs.map(segData => segData.utf8).join(" "); if (segText == "\n") return; const timeFormatted = formatYouTubeTime(event.tStartMs); timedTextData.push({ time: event.tStartMs, timeFormatted, text: segText, }); timeTextLines.push(`${timeFormatted} ${segText}`); }); console.clear(); console.log("Transcript created. You can find it in the video description (with clickable timestamps) or right here:") console.log(timeTextLines.join("\n")); const bottomRowItems = document.querySelector("#bottom-row #items"); const existingTranscriptCont = bottomRowItems.querySelector("#customTranscriptCont"); if (existingTranscriptCont) { existingTranscriptCont.parentNode.removeChild(existingTranscriptCont); } const transcriptCont = document.createElement("div"); transcriptCont.id = "customTranscriptCont"; const transcriptTitle = document.createElement("h2"); transcriptTitle.className = "style-scope ytd-rich-list-header-renderer"; Object.assign(transcriptTitle.style, { "margin": "1em 0", }); transcriptTitle.innerText = "Custom transcript"; transcriptCont.appendChild(transcriptTitle); for (const line of timedTextData) { const lineCont = document.createElement("p"); const timestampLink = document.createElement("span"); timestampLink.className = "yt-core-attributed-string__link yt-core-attributed-string__link--call-to-action-color"; timestampLink.style.cursor = "pointer"; timestampLink.innerText = `${line.timeFormatted} `; lineCont.appendChild(timestampLink); timestampLink.addEventListener("click",(e) => { seekToTime(line.time); }); const subtitleText = document.createElement("span"); subtitleText.innerText = line.text; lineCont.appendChild(subtitleText); transcriptCont.appendChild(lineCont); } bottomRowItems.prepend(transcriptCont); } catch (error) { console.error("Error processing subtitle data:", error); } }); })();