Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save Programazing/d29915fb01335f599f22045809fe7906 to your computer and use it in GitHub Desktop.
Save Programazing/d29915fb01335f599f22045809fe7906 to your computer and use it in GitHub Desktop.
Tampermonkey YouTube Transcript Extractor - Extracts YouTube transcripts to the clipboard
// ==UserScript==
// @name YouTube Transcript Extractor
// @namespace http://tampermonkey.net/
// @version 1.9
// @description Extracts YouTube transcripts to the clipboard
// @author Programazing
// @match https://www.youtube.com/watch?v=*
// @grant none
// ==/UserScript==
(function() {
'use strict';
function waitForElement(selector, timeout = 10000) {
return new Promise((resolve, reject) => {
const interval = 100;
let elapsed = 0;
const timer = setInterval(() => {
const el = document.querySelector(selector);
if (el) {
clearInterval(timer);
resolve(el);
} else if ((elapsed += interval) >= timeout) {
clearInterval(timer);
reject(new Error(`Element ${selector} not found within ${timeout}ms`));
}
}, interval);
});
}
async function addCopyButton() {
// Wait for the masthead's right section
let mastheadEnd;
try {
mastheadEnd = await waitForElement('ytd-masthead #end');
} catch {
console.error("Could not find masthead end section");
return;
}
if (document.getElementById('copy-transcript-btn')) return;
// Create button with beautiful styling
const btn = document.createElement('button');
btn.id = 'copy-transcript-btn';
btn.textContent = 'Copy Transcript';
btn.style.marginLeft = '12px';
btn.style.padding = '10px 16px';
btn.style.background = '#1da1f2';
btn.style.color = '#fff';
btn.style.border = 'none';
btn.style.borderRadius = '6px';
btn.style.fontSize = '16px';
btn.style.cursor = 'pointer';
btn.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)';
btn.style.transition = 'background 0.2s';
btn.style.height = '40px';
btn.style.alignSelf = 'center';
btn.style.display = 'inline-flex';
btn.style.alignItems = 'center';
btn.onmouseover = () => { btn.style.background = '#0d8ddb'; };
btn.onmouseout = () => { btn.style.background = '#1da1f2'; };
btn.onclick = async () => {
btn.textContent = 'Opening transcript...';
btn.disabled = true;
try {
// Try to find and click the transcript button
let transcriptBtn = Array.from(document.querySelectorAll('button'))
.find(b => (b.textContent || "").toLowerCase().includes('transcript') &&
(b.textContent || "").toLowerCase().includes('show'));
if (transcriptBtn) {
transcriptBtn.click();
// Wait for transcript panel to appear and load
await new Promise(resolve => setTimeout(resolve, 2000));
}
// Look for transcript panel directly
const transcriptPanel = document.querySelector('ytd-transcript-search-panel-renderer') ||
document.querySelector('[role="dialog"] ytd-transcript-segment-list-renderer') ||
document.querySelector('ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-transcript"]');
if (!transcriptPanel) {
alert('Could not find transcript panel. This video may not have a transcript.');
btn.textContent = 'Copy Transcript';
btn.disabled = false;
return;
}
// Wait an extra moment for content to load
btn.textContent = 'Extracting transcript...';
await new Promise(resolve => setTimeout(resolve, 1000));
// Try multiple different selectors to find transcript segments
const segments = transcriptPanel.querySelectorAll('ytd-transcript-segment-renderer') ||
transcriptPanel.querySelectorAll('.ytd-transcript-segment-renderer') ||
transcriptPanel.querySelectorAll('[class*="segment"]');
let lines = [];
if (segments && segments.length > 0) {
segments.forEach(seg => {
const time = seg.querySelector('#segment-timestamp') ?
seg.querySelector('#segment-timestamp').textContent.trim() :
seg.querySelector('[id*="timestamp"]')?.textContent.trim() || '';
const text = seg.querySelector('#segment-text') ?
seg.querySelector('#segment-text').textContent.trim() :
seg.querySelector('[id*="text"]')?.textContent.trim() || '';
if (time && text) lines.push(`${time} ${text}`);
});
}
let clipboardText = '';
if (lines.length === 0) {
// Fallback: split raw text at timestamps, robustly
const text = transcriptPanel.textContent.trim();
const parts = text.split(/(\d{1,2}:\d{2})/g);
let formattedLines = [];
for (let i = 1; i < parts.length; i += 2) {
const timestamp = parts[i];
const lineText = (parts[i + 1] || '').replace(/\s+/g, ' ').trim();
if (timestamp && lineText) {
formattedLines.push(`${timestamp} ${lineText}`);
}
}
if (formattedLines.length > 0) {
clipboardText = formattedLines.join('\n');
} else {
clipboardText = text;
}
} else {
clipboardText = lines.join('\n');
}
try {
await navigator.clipboard.writeText(clipboardText);
btn.textContent = 'Copied!';
} catch (err) {
alert("Failed to copy transcript to clipboard.");
btn.textContent = 'Copy Transcript';
}
setTimeout(() => {
btn.textContent = 'Copy Transcript';
btn.disabled = false;
}, 2000);
} catch (error) {
console.error('Error in transcript extraction:', error);
alert('An error occurred while extracting the transcript.');
btn.textContent = 'Copy Transcript';
btn.disabled = false;
}
};
mastheadEnd.appendChild(btn);
}
// Observe URL changes (for SPA navigation)
let lastUrl = '';
setInterval(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
setTimeout(addCopyButton, 1500);
}
}, 500);
// Initial run
setTimeout(addCopyButton, 2000);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment