Created
March 25, 2026 10:51
-
-
Save kiranwayne/22cce83a2b64d25126f71812b33ed45f to your computer and use it in GitHub Desktop.
Adds a button to download current lesson transcript from O'Reilly
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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