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).

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

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