Skip to content

Instantly share code, notes, and snippets.

@kiranwayne
Created March 25, 2026 10:51
Show Gist options
  • Select an option

  • Save kiranwayne/22cce83a2b64d25126f71812b33ed45f to your computer and use it in GitHub Desktop.

Select an option

Save kiranwayne/22cce83a2b64d25126f71812b33ed45f to your computer and use it in GitHub Desktop.
Adds a button to download current lesson transcript from O'Reilly
// ==UserScript==
// @name O'Reilly Current Transcript Tools
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Download or copy transcript from the current O'Reilly lesson page
// @match https://learning.oreilly.com/*
// @updateURL https://gist.github.com/kiranwayne/22cce83a2b64d25126f71812b33ed45f/raw/oreilly_transcript_tools.js
// @downloadURL https://gist.github.com/kiranwayne/22cce83a2b64d25126f71812b33ed45f/raw/oreilly_transcript_tools.js
// @grant none
// ==/UserScript==
(function () {
'use strict';
const PANEL_ID = 'oreilly-transcript-panel';
const TOGGLE_ID = 'oreilly-transcript-toggle';
const STYLE_ID = 'oreilly-transcript-style';
const STORAGE_KEY = 'oreilly-transcript-include-timestamps';
function textOf(el) {
return el?.textContent?.replace(/\s+/g, ' ').trim() || '';
}
function sanitizeFilename(name) {
return (name || 'OReilly Transcript')
.replace(/[<>:"/\\|?*\x00-\x1F]/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function getLessonTitle() {
const selectors = [
'#videoTitle',
'[data-testid="title"]',
'h1',
'h2.MuiTypography-h2'
];
for (const selector of selectors) {
const value = textOf(document.querySelector(selector));
if (value) return value;
}
return document.title.replace(/\s*\|\s*O['’]Reilly.*$/i, '').trim() || 'OReilly Transcript';
}
function getTranscriptEntries() {
const container = document.querySelector('div[data-testid="transcript-body"]');
if (!container) return [];
return [...container.querySelectorAll('button')]
.map((button) => {
const timestamp =
textOf(button.querySelector('p.MuiTypography-uiBodySmall')) ||
textOf(button.querySelector('p[class*="uiBodySmall"]'));
const text =
textOf(button.querySelector('p.MuiTypography-uiBody')) ||
textOf(button.querySelector('p[class*="uiBody"]'));
if (!text) return null;
return { timestamp, text };
})
.filter(Boolean);
}
function buildTranscript(includeTimestamps) {
const entries = getTranscriptEntries();
if (!entries.length) return '';
return entries
.map(({ timestamp, text }) =>
includeTimestamps && timestamp ? `[${timestamp}] ${text}` : text
)
.join('\n\n');
}
function buildFilename() {
return `${sanitizeFilename(getLessonTitle())}.txt`;
}
function setStatus(message, isError = false) {
const el = document.getElementById('oreilly-transcript-status');
if (!el) return;
el.textContent = message;
el.style.color = isError ? '#ffb4b4' : '#d7f7d7';
}
function downloadText(filename, content) {
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
async function copyText(content) {
await navigator.clipboard.writeText(content);
}
function getIncludeTimestamps() {
const checkbox = document.getElementById('oreilly-transcript-timestamps');
return checkbox ? checkbox.checked : true;
}
function saveIncludeTimestamps(value) {
localStorage.setItem(STORAGE_KEY, value ? 'true' : 'false');
}
function loadIncludeTimestamps() {
return localStorage.getItem(STORAGE_KEY) !== 'false';
}
function ensureStyles() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = `
#${PANEL_ID} {
position: fixed;
right: 20px;
bottom: 78px;
width: 320px;
background: #1f1f1f;
color: #fff;
border: 1px solid #555;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
z-index: 999999;
font-family: Arial, sans-serif;
overflow: hidden;
}
#${PANEL_ID}.hidden {
display: none;
}
#${PANEL_ID} .ot-header {
padding: 10px 12px;
background: #cf3d2e;
font-size: 14px;
font-weight: 700;
}
#${PANEL_ID} .ot-body {
padding: 12px;
}
#${PANEL_ID} .ot-btn {
width: 100%;
margin-bottom: 8px;
padding: 10px 12px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 13px;
}
#${PANEL_ID} .ot-download {
background: #ffffff;
color: #111;
}
#${PANEL_ID} .ot-copy {
background: #2f6fed;
color: #fff;
}
#${PANEL_ID} .ot-label {
display: flex;
align-items: center;
gap: 8px;
margin: 8px 0 10px;
font-size: 13px;
}
#oreilly-transcript-status {
font-size: 12px;
min-height: 1.2em;
color: #d7f7d7;
}
#${TOGGLE_ID} {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 999999;
background: #cf3d2e;
color: #fff;
border: none;
border-radius: 999px;
padding: 10px 14px;
font-weight: 700;
cursor: pointer;
box-shadow: 0 6px 18px rgba(0,0,0,0.35);
}
`;
document.head.appendChild(style);
}
async function handleDownload() {
const transcript = buildTranscript(getIncludeTimestamps());
if (!transcript) {
setStatus('Transcript not found. Open the transcript panel first.', true);
return;
}
const filename = buildFilename();
downloadText(filename, transcript);
setStatus(`Downloaded: ${filename}`);
}
async function handleCopy() {
const transcript = buildTranscript(getIncludeTimestamps());
if (!transcript) {
setStatus('Transcript not found. Open the transcript panel first.', true);
return;
}
try {
await copyText(transcript);
setStatus('Transcript copied to clipboard');
} catch {
setStatus('Clipboard copy failed', true);
}
}
function ensureUI() {
if (!document.getElementById(PANEL_ID)) {
const panel = document.createElement('div');
panel.id = PANEL_ID;
panel.className = 'hidden';
panel.innerHTML = `
<div class="ot-header">O'Reilly Transcript Tools</div>
<div class="ot-body">
<button class="ot-btn ot-download" id="oreilly-transcript-download">Download Transcript</button>
<button class="ot-btn ot-copy" id="oreilly-transcript-copy">Copy Transcript</button>
<label class="ot-label">
<input type="checkbox" id="oreilly-transcript-timestamps">
Include timestamps
</label>
<div id="oreilly-transcript-status">Ready</div>
</div>
`;
document.body.appendChild(panel);
document
.getElementById('oreilly-transcript-download')
.addEventListener('click', handleDownload);
document
.getElementById('oreilly-transcript-copy')
.addEventListener('click', handleCopy);
const checkbox = document.getElementById('oreilly-transcript-timestamps');
checkbox.checked = loadIncludeTimestamps();
checkbox.addEventListener('change', () => {
saveIncludeTimestamps(checkbox.checked);
setStatus(checkbox.checked ? 'Timestamps enabled' : 'Timestamps disabled');
});
}
if (!document.getElementById(TOGGLE_ID)) {
const toggle = document.createElement('button');
toggle.id = TOGGLE_ID;
toggle.textContent = 'Transcript';
toggle.addEventListener('click', () => {
const panel = document.getElementById(PANEL_ID);
if (panel) panel.classList.toggle('hidden');
});
document.body.appendChild(toggle);
}
}
function init() {
ensureStyles();
ensureUI();
}
init();
const observer = new MutationObserver(() => {
if (!document.getElementById(PANEL_ID) || !document.getElementById(TOGGLE_ID)) {
init();
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment