Skip to content

Instantly share code, notes, and snippets.

@kiranwayne
Last active November 26, 2025 05:03
Show Gist options
  • Select an option

  • Save kiranwayne/445dda6abed129da85b5594a6e9e0f32 to your computer and use it in GitHub Desktop.

Select an option

Save kiranwayne/445dda6abed129da85b5594a6e9e0f32 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Udemy Transcript Downloader (Business & Consumer)
// @namespace http://tampermonkey.net/
// @version 2.9
// @description Download Udemy transcripts. Draggable UI. Robust Timestamp cleaning.
// @author You
// @match https://*.udemy.com/course/*
// @updateURL https://gist.githubusercontent.com/kiranwayne/445dda6abed129da85b5594a6e9e0f32/raw/udemy_transcript_downloader.js
// @downloadURL https://gist.githubusercontent.com/kiranwayne/445dda6abed129da85b5594a6e9e0f32/raw/udemy_transcript_downloader.js
// @icon https://www.google.com/s2/favicons?sz=64&domain=udemy.com
// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// @grant none
// ==/UserScript==
(function() {
'use strict';
// --- CONFIGURATION ---
const REMOVE_TIMESTAMPS = true; // Set to false if you WANT timestamps
// ---------------------
let courseDataCache = null;
let isFetching = false;
// --- UI STYLES ---
const style = document.createElement('style');
style.innerHTML = `
#udemy-transcript-panel {
position: fixed;
bottom: 20px;
right: 20px;
width: 300px;
background: #1c1d1f;
color: #fff;
border: 1px solid #3e4143;
box-shadow: 0 4px 15px rgba(0,0,0,0.7);
z-index: 9999;
font-family: Arial, sans-serif;
border-radius: 6px;
display: flex;
flex-direction: column;
user-select: none;
cursor: default;
}
#ut-header {
padding: 12px 15px;
background: #2d2f31;
border-bottom: 1px solid #3e4143;
border-radius: 6px 6px 0 0;
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
}
#ut-header h3 {
margin: 0;
font-size: 15px;
color: #a435f0;
font-weight: bold;
pointer-events: none;
}
#ut-body {
padding: 15px;
}
.ut-btn {
display: block;
width: 100%;
padding: 10px;
margin-bottom: 10px;
background: #2d2f31;
border: 1px solid #fff;
color: #fff;
cursor: pointer;
text-align: center;
font-weight: bold;
font-size: 13px;
border-radius: 4px;
transition: background 0.2s;
user-select: none;
}
.ut-btn:hover {
background: #3e4143;
}
.ut-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
#ut-status {
margin-top: 5px;
font-size: 12px;
color: #d1d7dc;
min-height: 1.2em;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: default;
}
#ut-close {
cursor: pointer;
font-weight: bold;
color: #888;
padding: 0 5px;
}
#ut-close:hover {
color: #fff;
}
#ut-toggle-btn {
position: fixed;
bottom: 20px;
right: 20px;
width: 50px;
height: 50px;
background: #a435f0;
border-radius: 50%;
box-shadow: 0 2px 10px rgba(0,0,0,0.5);
z-index: 9999;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 24px;
font-weight: bold;
border: 2px solid white;
user-select: none;
}
#ut-toggle-btn:hover {
background: #8710d8;
}
`;
document.head.appendChild(style);
// --- DRAG LOGIC ---
function makeDraggable(el) {
const header = el.querySelector('#ut-header');
let isDragging = false;
let startX, startY, initialLeft, initialTop;
header.addEventListener('mousedown', (e) => {
if (e.target.id === 'ut-close') return;
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = el.getBoundingClientRect();
initialLeft = rect.left;
initialTop = rect.top;
el.style.bottom = 'auto';
el.style.right = 'auto';
el.style.left = initialLeft + 'px';
el.style.top = initialTop + 'px';
});
window.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
el.style.top = `${initialTop + dy}px`;
el.style.left = `${initialLeft + dx}px`;
});
window.addEventListener('mouseup', () => { isDragging = false; });
}
// --- UI CREATION ---
function createUI() {
if (document.getElementById('udemy-transcript-panel')) return;
const panel = document.createElement('div');
panel.id = 'udemy-transcript-panel';
panel.innerHTML = `
<div id="ut-header">
<h3>Transcript Downloader</h3>
<div id="ut-close" title="Minimize">✕</div>
</div>
<div id="ut-body">
<button id="btn-single" class="ut-btn">Download Full (Single .txt)</button>
<button id="btn-chapter" class="ut-btn">Download by Chapter (.zip)</button>
<button id="btn-lecture" class="ut-btn">Download by Lecture (.zip)</button>
<div id="ut-status">Ready</div>
</div>
`;
document.body.appendChild(panel);
const toggleBtn = document.createElement('div');
toggleBtn.id = 'ut-toggle-btn';
toggleBtn.innerText = 'T';
toggleBtn.title = 'Open Transcript Downloader';
toggleBtn.style.display = 'none';
toggleBtn.onclick = () => {
toggleBtn.style.display = 'none';
panel.style.display = 'flex';
};
document.body.appendChild(toggleBtn);
document.getElementById('ut-close').onclick = () => {
panel.style.display = 'none';
toggleBtn.style.display = 'flex';
};
document.getElementById('btn-single').onclick = () => processDownload('single');
document.getElementById('btn-chapter').onclick = () => processDownload('chapter');
document.getElementById('btn-lecture').onclick = () => processDownload('lecture');
makeDraggable(panel);
}
// --- HELPER FUNCTIONS ---
function updateStatus(msg) {
const el = document.getElementById('ut-status');
if (el) el.textContent = msg;
}
function sanitizeFilename(name) {
return name.replace(/[\/\\?%*:|"<>]/g, '-').replace(/\s+/g, ' ').trim();
}
function getCourseId() {
let id = null;
const appLoader = document.querySelector('.ud-app-loader');
if (appLoader && appLoader.dataset.moduleArgs) {
try { id = JSON.parse(appLoader.dataset.moduleArgs).courseId; } catch (e) {}
}
if (!id && window.UD && window.UD.course) id = window.UD.course.id;
if (!id) id = document.body.getAttribute("data-clp-course-id");
return id;
}
// --- TEXT CLEANING ---
function cleanTranscript(vttText) {
if (!REMOVE_TIMESTAMPS) return vttText;
return vttText
// Remove WebVTT header
.replace(/^WEBVTT/g, '')
// Remove Note lines
.replace(/^NOTE .*/gm, '')
// Remove timestamps (Matches 00:00.000 --> 00:00.000 OR 00:00:00.000 --> ...)
.replace(/^[\d:.]+\s-->\s[\d:.]+.*$/gm, '')
// Remove single index numbers often found in VTT
.replace(/^\d+$/gm, '')
// Remove empty lines
.replace(/^\s*[\r\n]/gm, '')
// Join lines to make it a paragraph (Optional: remove this if you want line breaks preserved)
.replace(/([^\n])\n([^\n])/g, '$1 $2')
.trim();
}
// --- CORE FETCH LOGIC ---
async function fetchCourseContent(courseId) {
if (courseDataCache) return courseDataCache;
isFetching = true;
updateStatus("Fetching curriculum list...");
const curriculumUrl = `/api-2.0/courses/${courseId}/subscriber-curriculum-items/?page_size=1000&fields[lecture]=title,object_index,asset&fields[chapter]=title,object_index&caching_intent=True`;
const response = await fetch(curriculumUrl);
const data = await response.json();
const results = data.results || [];
results.sort((a, b) => a.sort_order - b.sort_order);
const structuredData = [];
let currentChapter = { title: "Introduction", index: 0, lectures: [] };
let totalLectures = 0;
for (const item of results) {
if (item._class === "chapter") {
if (currentChapter.lectures.length > 0) structuredData.push(currentChapter);
currentChapter = {
title: item.title,
index: item.object_index,
lectures: []
};
}
if (item._class === "lecture" && item.asset && item.asset.asset_type === "Video") {
currentChapter.lectures.push({
id: item.id,
title: item.title,
index: item.object_index,
asset: item.asset
});
totalLectures++;
}
}
if (currentChapter.lectures.length > 0) structuredData.push(currentChapter);
let processed = 0;
for (const chap of structuredData) {
for (const lec of chap.lectures) {
processed++;
updateStatus(`Fetching captions: ${processed}/${totalLectures}`);
await new Promise(r => setTimeout(r, 200));
try {
const lectureUrl = `/api-2.0/users/me/subscribed-courses/${courseId}/lectures/${lec.id}/?fields[lecture]=asset,supplementary_assets&fields[asset]=captions`;
const lecResp = await fetch(lectureUrl);
const lecJson = await lecResp.json();
const tracks = (lecJson.asset && lecJson.asset.captions) ? lecJson.asset.captions : [];
if (tracks.length > 0) {
const track = tracks.find(t => t.locale_id === 'en_US' || t.locale_id === 'en_GB')
|| tracks.find(t => t.label && t.label.toLowerCase().includes('english'))
|| tracks[0];
const vttResp = await fetch(track.url);
const vttText = await vttResp.text();
lec.transcript = cleanTranscript(vttText);
lec.lang = track.locale_id || track.label;
} else {
lec.transcript = "[No transcript available]";
}
} catch (e) {
lec.transcript = "[Error fetching transcript]";
}
}
}
isFetching = false;
courseDataCache = structuredData;
return structuredData;
}
// --- DOWNLOAD GENERATORS ---
async function processDownload(mode) {
if (isFetching) return;
const courseId = getCourseId();
if (!courseId) {
updateStatus("Error: Course ID not found.");
return;
}
try {
const data = await fetchCourseContent(courseId);
updateStatus("Generating files...");
const zip = new JSZip();
const folderName = `Udemy_Course_${courseId}`;
if (mode === 'single') {
let content = `COURSE TRANSCRIPT\n\n`;
data.forEach(chap => {
content += `\n\n=== CHAPTER ${chap.index}: ${chap.title} ===\n`;
chap.lectures.forEach(lec => {
content += `\n--- Lecture ${lec.index}: ${lec.title} ---\n${lec.transcript}\n`;
});
});
const blob = new Blob([content], {type: "text/plain;charset=utf-8"});
saveAs(blob, `${folderName}_Full.txt`);
} else if (mode === 'chapter') {
data.forEach(chap => {
let content = `CHAPTER ${chap.index}: ${chap.title}\n\n`;
chap.lectures.forEach(lec => {
content += `\n--- Lecture ${lec.index}: ${lec.title} ---\n${lec.transcript}\n`;
});
const safeTitle = sanitizeFilename(`${String(chap.index).padStart(2, '0')}_${chap.title}`);
zip.file(`${safeTitle}.txt`, content);
});
const content = await zip.generateAsync({type:"blob"});
saveAs(content, `${folderName}_Chapters.zip`);
} else if (mode === 'lecture') {
data.forEach(chap => {
const chapFolder = zip.folder(sanitizeFilename(`${String(chap.index).padStart(2, '0')}_${chap.title}`));
chap.lectures.forEach(lec => {
const safeTitle = sanitizeFilename(`${String(lec.index).padStart(3, '0')}_${lec.title}`);
chapFolder.file(`${safeTitle}.txt`, lec.transcript);
});
});
const content = await zip.generateAsync({type:"blob"});
saveAs(content, `${folderName}_Lectures.zip`);
}
updateStatus("Download Complete!");
setTimeout(() => updateStatus("Ready"), 3000);
} catch (e) {
updateStatus("Error during download.");
console.error(e);
isFetching = false;
}
}
// --- INITIALIZATION ---
function init() {
const id = getCourseId();
if (id) {
console.log(`[Transcript] Course ID found: ${id}`);
createUI();
} else {
console.log("[Transcript] No course ID found on this page.");
}
}
window.addEventListener('load', () => setTimeout(init, 2000));
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment