Greasy Fork is available in English.
Adds a button to copy the transcript from Udemy video lectures
// ==UserScript==
// @name Udemy Transcript Copier
// @namespace https://greasyfork.org/users/KosherKale
// @version 1.0.0
// @description Adds a button to copy the transcript from Udemy video lectures
// @author kosherkale
// @license GPL-3.0
// @match https://*.udemy.com/course/*
// @grant GM_setClipboard
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
const BUTTON_ID = 'ute-export-btn';
const PANEL_SELECTOR = '[data-purpose="transcript-panel"]';
const CUE_SELECTOR = '[data-purpose="cue-text"]';
function getTranscriptText() {
const cues = document.querySelectorAll(CUE_SELECTOR);
return Array.from(cues)
.map(el => el.textContent.trim())
.filter(Boolean)
.join(' ');
}
function getVideoTitle() {
// Try the lesson heading first, then fall back to page title
const heading = document.querySelector('[data-purpose="lead-title"], h1[class*="curriculum-item-link--title"]');
if (heading) return heading.textContent.trim();
return document.title.replace(' | Udemy', '').trim();
}
function downloadText(text, filename) {
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
function createButton() {
const btn = document.createElement('button');
btn.id = BUTTON_ID;
btn.textContent = 'Copy Transcript';
btn.title = 'Copy transcript to clipboard or download as .txt';
Object.assign(btn.style, {
display: 'inline-flex',
alignItems: 'center',
gap: '6px',
margin: '8px 0 4px 0',
padding: '6px 14px',
background: '#a435f0',
color: '#fff',
border: 'none',
borderRadius: '4px',
fontSize: '13px',
fontWeight: '700',
cursor: 'pointer',
width: '100%',
justifyContent: 'center',
boxSizing: 'border-box',
});
// Right-click or Ctrl+click → download; left-click → clipboard
btn.addEventListener('click', (e) => {
const text = getTranscriptText();
if (!text) {
btn.textContent = 'No transcript found';
setTimeout(() => { btn.textContent = 'Copy Transcript'; }, 2000);
return;
}
if (e.ctrlKey || e.metaKey) {
// Download as file
const title = getVideoTitle().replace(/[\\/:*?"<>|]/g, '_').slice(0, 80);
downloadText(text, `${title}_transcript.txt`);
btn.textContent = 'Downloaded!';
} else {
// Copy to clipboard
if (typeof GM_setClipboard === 'function') {
GM_setClipboard(text);
} else {
navigator.clipboard.writeText(text).catch(() => {
// Fallback for older browsers
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
});
}
btn.textContent = 'Copied!';
}
setTimeout(() => { btn.textContent = 'Copy Transcript'; }, 2000);
});
btn.addEventListener('mouseenter', () => {
btn.style.background = '#8710d8';
});
btn.addEventListener('mouseleave', () => {
btn.style.background = '#a435f0';
});
return btn;
}
function injectButton(panel) {
if (document.getElementById(BUTTON_ID)) return;
const btn = createButton();
panel.insertAdjacentElement('beforebegin', btn);
}
function tryInject() {
const panel = document.querySelector(PANEL_SELECTOR);
if (panel) injectButton(panel);
}
// Watch for the transcript panel being added to the DOM (SPA navigation)
const observer = new MutationObserver(() => tryInject());
observer.observe(document.body, { childList: true, subtree: true });
// Also try immediately in case panel is already present
tryInject();
})();