// ==UserScript== // @name Zoom Recording CC Extractor // @name:en Zoom Recording CC Extractor // @name:ja Zoom録画 字幕抽出ツール // @name:zh-CN Zoom 录像字幕提取器 // @name:zh-TW Zoom 錄影字幕提取器 // @name:ko Zoom 녹화 자막 추출기 // @name:ru Zoom Запись — Извлечение субтитров // @name:es Zoom Grabación — Extractor de subtítulos // @name:pt-BR Zoom Gravação — Extrator de legendas // @name:fr Zoom Enregistrement — Extracteur de sous-titres // @name:de Zoom Aufnahme — Untertitel-Extraktor // @namespace https://github.com/CheerChen // @version 1.0 // @description Extract closed captions (VTT) from Zoom cloud recording playback pages. Intercepts CC network requests and provides download/copy UI. // @description:en Extract closed captions (VTT) from Zoom cloud recording playback pages. Intercepts CC network requests and provides download/copy UI. // @description:ja Zoomクラウド録画再生ページから字幕(VTT)を抽出。CCネットワークリクエストをインターセプトし、ダウンロード/コピーUIを提供します。 // @description:zh-CN 从 Zoom 云录像回放页面提取字幕(VTT)。拦截 CC 网络请求并提供下载/复制界面。 // @description:zh-TW 從 Zoom 雲端錄影回放頁面提取字幕(VTT)。攔截 CC 網路請求並提供下載/複製介面。 // @description:ko Zoom 클라우드 녹화 재생 페이지에서 자막(VTT)을 추출합니다. CC 네트워크 요청을 가로채고 다운로드/복사 UI를 제공합니다. // @description:ru Извлечение субтитров (VTT) со страниц воспроизведения облачных записей Zoom. Перехватывает сетевые запросы CC и предоставляет интерфейс скачивания/копирования. // @description:es Extraer subtítulos (VTT) de las páginas de reproducción de grabaciones en la nube de Zoom. Intercepta solicitudes de red CC y proporciona una interfaz de descarga/copia. // @description:pt-BR Extrair legendas (VTT) das páginas de reprodução de gravações na nuvem do Zoom. Intercepta solicitações de rede CC e fornece interface de download/cópia. // @description:fr Extraire les sous-titres (VTT) des pages de lecture des enregistrements cloud Zoom. Intercepte les requêtes réseau CC et fournit une interface de téléchargement/copie. // @description:de Untertitel (VTT) von Zoom-Cloud-Aufnahme-Wiedergabeseiten extrahieren. Fängt CC-Netzwerkanfragen ab und bietet eine Download-/Kopier-Oberfläche. // @author cheerchen37 // @match https://*.zoom.us/rec/play/* // @match https://*.zoom.us/rec/share/* // @grant none // @icon https://www.google.com/s2/favicons?domain=zoom.us // @license MIT // @homepage https://github.com/CheerChen/userscripts // @supportURL https://github.com/CheerChen/userscripts/issues // ==/UserScript== (function () { 'use strict'; // Intercept XMLHttpRequest to capture VTT responses const origOpen = XMLHttpRequest.prototype.open; const origSend = XMLHttpRequest.prototype.send; XMLHttpRequest.prototype.open = function (method, url) { this._zmUrl = url; return origOpen.apply(this, arguments); }; XMLHttpRequest.prototype.send = function () { if (this._zmUrl && this._zmUrl.includes('type=cc')) { this.addEventListener('load', function () { handleCCResponse(this.response, this._zmUrl); }); } return origSend.apply(this, arguments); }; // Also intercept fetch const origFetch = window.fetch; window.fetch = function (input, init) { const url = typeof input === 'string' ? input : input.url; if (url && url.includes('type=cc')) { return origFetch.apply(this, arguments).then(response => { const cloned = response.clone(); cloned.text().then(text => handleCCResponse(text, url)); return response; }); } return origFetch.apply(this, arguments); }; let captured = false; function handleCCResponse(data, url) { if (captured) return; captured = true; let text = typeof data === 'string' ? data : ''; if (data instanceof ArrayBuffer) { text = new TextDecoder('utf-8').decode(data); } // If still garbled, try decoding as gzip via DecompressionStream if (text && !text.startsWith('WEBVTT') && !text.includes('-->')) { tryDecompress(data, url); return; } showResult(text, url); } async function tryDecompress(data, url) { try { // Re-fetch with explicit handling const resp = await fetch(url, { credentials: 'include' }); const text = await resp.text(); showResult(text, url); } catch (e) { console.error('[Zoom CC Extractor] Decompress failed:', e); // Fall back to showing raw showResult(typeof data === 'string' ? data : 'Failed to decode', url); } } function showResult(text, url) { console.log('[Zoom CC Extractor] Captured CC text:'); console.log(text); // Create floating UI const panel = document.createElement('div'); panel.style.cssText = 'position:fixed;top:10px;right:10px;width:500px;max-height:80vh;' + 'background:#1a1a2e;color:#e0e0e0;border:2px solid #0f3460;border-radius:8px;' + 'z-index:999999;font-family:monospace;font-size:12px;box-shadow:0 4px 20px rgba(0,0,0,0.5);'; // Header const header = document.createElement('div'); header.style.cssText = 'padding:10px 15px;background:#0f3460;border-radius:6px 6px 0 0;' + 'display:flex;justify-content:space-between;align-items:center;cursor:move;'; header.innerHTML = 'Zoom CC Extractor'; // Buttons const btnGroup = document.createElement('div'); const downloadVttBtn = createButton('Download VTT', () => downloadFile(text, 'zoom_cc.vtt', 'text/vtt')); const downloadTxtBtn = createButton('Download TXT', () => downloadFile(vttToPlainText(text), 'zoom_cc.txt', 'text/plain')); const copyBtn = createButton('Copy Text', () => { navigator.clipboard.writeText(vttToPlainText(text)); copyBtn.textContent = 'Copied!'; setTimeout(() => (copyBtn.textContent = 'Copy Text'), 1500); }); const closeBtn = createButton('X', () => panel.remove()); closeBtn.style.marginLeft = '8px'; closeBtn.style.background = '#e94560'; btnGroup.append(downloadVttBtn, downloadTxtBtn, copyBtn, closeBtn); header.append(btnGroup); // Content const content = document.createElement('pre'); content.style.cssText = 'padding:15px;overflow:auto;max-height:calc(80vh - 50px);margin:0;white-space:pre-wrap;word-break:break-word;'; content.textContent = text; panel.append(header, content); document.body.append(panel); // Drag let isDragging = false, offsetX, offsetY; header.addEventListener('mousedown', e => { isDragging = true; offsetX = e.clientX - panel.offsetLeft; offsetY = e.clientY - panel.offsetTop; }); document.addEventListener('mousemove', e => { if (!isDragging) return; panel.style.left = (e.clientX - offsetX) + 'px'; panel.style.top = (e.clientY - offsetY) + 'px'; panel.style.right = 'auto'; }); document.addEventListener('mouseup', () => (isDragging = false)); } function createButton(label, onClick) { const btn = document.createElement('button'); btn.textContent = label; btn.style.cssText = 'margin-left:5px;padding:3px 8px;background:#16213e;color:#e0e0e0;' + 'border:1px solid #0f3460;border-radius:4px;cursor:pointer;font-size:11px;'; btn.addEventListener('click', onClick); return btn; } function downloadFile(content, filename, mimeType) { const blob = new Blob([content], { type: mimeType }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; a.click(); URL.revokeObjectURL(a.href); } function vttToPlainText(vtt) { return vtt .split('\n') .filter(line => { const trimmed = line.trim(); // Skip WEBVTT header, timestamps, sequence numbers, NOTE, STYLE, empty lines if (!trimmed) return false; if (trimmed === 'WEBVTT') return false; if (/^\d+$/.test(trimmed)) return false; if (/-->/.test(trimmed)) return false; if (/^NOTE\b/.test(trimmed)) return false; if (/^STYLE\b/.test(trimmed)) return false; return true; }) .map(line => line.replace(/<[^>]+>/g, '').trim()) // strip VTT tags like .filter(Boolean) .join('\n'); } // Also add a manual trigger button in case auto-intercept misses it function addManualButton() { const btn = document.createElement('button'); btn.textContent = 'Extract CC'; btn.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:999998;padding:10px 20px;' + 'background:#e94560;color:white;border:none;border-radius:8px;cursor:pointer;' + 'font-size:14px;font-weight:bold;box-shadow:0 2px 10px rgba(233,69,96,0.4);'; btn.addEventListener('click', async () => { btn.textContent = 'Fetching...'; btn.disabled = true; try { // Find CC URL from page or use known pattern const ccUrl = findCCUrl(); if (!ccUrl) { alert('Could not find CC URL. Check console for details.'); btn.textContent = 'Extract CC'; btn.disabled = false; return; } const resp = await fetch(ccUrl, { credentials: 'include' }); const text = await resp.text(); captured = false; showResult(text, ccUrl); } catch (e) { console.error('[Zoom CC Extractor]', e); alert('Failed: ' + e.message); } btn.textContent = 'Extract CC'; btn.disabled = false; }); document.body.append(btn); } function findCCUrl() { // Try to find the CC URL from network entries via PerformanceObserver const entries = performance.getEntriesByType('resource'); for (const entry of entries) { if (entry.name.includes('type=cc') || entry.name.includes('/vtt')) { return entry.name; } } // Try to extract from current page URL pattern const params = new URLSearchParams(window.location.search); // Fallback: prompt user const url = prompt('[Zoom CC Extractor] Paste the CC/VTT URL:'); return url || null; } // Wait for page to load, then add button if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', addManualButton); } else { addManualButton(); } })();