YouTube Transcript for all videos with subs

Shows a transcript with clickable timestamps for any video with subtitles. Just enable subtitles and look in the description (or the dev console).

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==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);
		}
	});
})();