Last active
March 9, 2026 03:53
-
-
Save gotexis/746cad1c40292c8f9386af7826321856 to your computer and use it in GitHub Desktop.
MS Teams Transcript Downloader
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 MS Teams Transcript Downloader | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.2 | |
| // @description Intercepts expanded API calls to download MS Teams transcripts | |
| // @author Exis | |
| // @match *://*.teams.microsoft.com/* | |
| // @match *://*.sharepoint.com/* | |
| // @grant none | |
| // @run-at document-start | |
| // ==/UserScript== | |
| (function() { | |
| 'use strict'; | |
| console.log('[MS Teams Downloader] ๐ Script V1.2 Active.'); | |
| let latestTranscriptJsonUrl = null; | |
| // ========================================== | |
| // 1. Intercept `fetch` | |
| // ========================================== | |
| const originalFetch = window.fetch; | |
| window.fetch = async function(...args) { | |
| const url = args[0] instanceof Request ? args[0].url : args[0]; | |
| const response = await originalFetch.apply(this, args); | |
| // Match URLs containing "media" and "transcripts" anywhere | |
| if (typeof url === 'string' && url.includes('media') && url.includes('transcripts')) { | |
| console.log('[MS Teams Downloader] ๐ Found potential transcript data in request...'); | |
| try { | |
| const clone = response.clone(); | |
| const data = await clone.json(); | |
| // Navigate the nested JSON structure for "expanded" calls | |
| // Path: data.media.transcripts[0].temporaryDownloadUrl | |
| let downloadUrlRaw = null; | |
| if (data?.media?.transcripts && data.media.transcripts.length > 0) { | |
| downloadUrlRaw = data.media.transcripts[0].temporaryDownloadUrl; | |
| } else if (data?.temporaryDownloadUrl) { | |
| downloadUrlRaw = data.temporaryDownloadUrl; | |
| } | |
| if (downloadUrlRaw) { | |
| console.log('[MS Teams Downloader] ๐ฏ Successfully extracted Transcript URL!'); | |
| const downloadUrlObj = new URL(downloadUrlRaw); | |
| downloadUrlObj.searchParams.set('format', 'json'); | |
| latestTranscriptJsonUrl = downloadUrlObj.toString(); | |
| injectDownloadButton(); | |
| } else { | |
| console.log('[MS Teams Downloader] โ ๏ธ Request matched but no temporaryDownloadUrl found in JSON payload.'); | |
| } | |
| } catch(e) { | |
| // Ignore errors if the JSON isn't what we expect | |
| } | |
| } | |
| return response; | |
| }; | |
| // ========================================== | |
| // 2. Inject Button | |
| // ========================================== | |
| function injectDownloadButton() { | |
| if (document.getElementById('tm-download-transcript-btn')) return; | |
| const btn = document.createElement('button'); | |
| btn.id = 'tm-download-transcript-btn'; | |
| btn.innerText = 'Download Transcript'; | |
| btn.style.cssText = ` | |
| position: fixed; | |
| bottom: 24px; | |
| right: 24px; | |
| z-index: 999999; | |
| padding: 12px 20px; | |
| background-color: #5B5FC7; | |
| color: white; | |
| border: 2px solid #ffffff; | |
| border-radius: 6px; | |
| font-family: 'Segoe UI', system-ui, sans-serif; | |
| font-size: 14px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.3); | |
| `; | |
| btn.onclick = async () => { | |
| btn.innerText = 'Processing...'; | |
| try { | |
| const resp = await originalFetch(latestTranscriptJsonUrl); | |
| const transcriptData = await resp.json(); | |
| const choice = prompt("Choose Format:\n1. RAW JSON\n2. VTT (Captions)\n3. Text (Grouped by Speaker)", "3"); | |
| let content, filename, type; | |
| let title = document.title.split(' - ')[0].replace(/[^a-z0-9]/gi, '_'); | |
| if (choice === "1") { | |
| content = JSON.stringify(transcriptData, null, 2); | |
| filename = `${title}.json`; | |
| type = 'application/json'; | |
| } else if (choice === "2") { | |
| content = convertToVTT(transcriptData); | |
| filename = `${title}.vtt`; | |
| type = 'text/vtt'; | |
| } else { | |
| content = convertToGroupedText(transcriptData); | |
| filename = `${title}.txt`; | |
| type = 'text/plain'; | |
| } | |
| downloadFile(content, filename, type); | |
| } catch (err) { | |
| alert("Download failed. See console."); | |
| console.error(err); | |
| } | |
| btn.innerText = 'Download Transcript'; | |
| }; | |
| document.body.appendChild(btn); | |
| } | |
| // ========================================== | |
| // 3. Formatting Helpers | |
| // ========================================== | |
| function convertToVTT(data) { | |
| let vtt = 'WEBVTT\n\n'; | |
| data.entries.forEach(entry => { | |
| vtt += `${formatTime(entry.startOffset)} --> ${formatTime(entry.endOffset)}\n`; | |
| vtt += entry.speakerDisplayName ? `<v ${entry.speakerDisplayName}>${entry.text}\n\n` : `${entry.text}\n\n`; | |
| }); | |
| return vtt; | |
| } | |
| function convertToGroupedText(data) { | |
| let txt = '', currentSpeaker = null; | |
| data.entries.forEach(entry => { | |
| const speaker = entry.speakerDisplayName || 'Unknown'; | |
| if (speaker !== currentSpeaker) { | |
| txt += `\n${speaker}:\n`; | |
| currentSpeaker = speaker; | |
| } | |
| txt += `${entry.text}\n`; | |
| }); | |
| return txt.trim(); | |
| } | |
| function formatTime(t) { | |
| let ms = 0; | |
| const m = t.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:([\d.]+)S)?/); | |
| if (m) ms = ((parseFloat(m[1]||0)*3600) + (parseFloat(m[2]||0)*60) + parseFloat(m[3]||0)) * 1000; | |
| const d = new Date(ms); | |
| return `${String(d.getUTCHours()).padStart(2,'0')}:${String(d.getUTCMinutes()).padStart(2,'0')}:${String(d.getUTCSeconds()).padStart(2,'0')}.${String(d.getUTCMilliseconds()).padStart(3,'0')}`; | |
| } | |
| function downloadFile(content, filename, mimeType) { | |
| const a = document.createElement('a'); | |
| a.href = URL.createObjectURL(new Blob([content], { type: mimeType })); | |
| a.download = filename; | |
| a.click(); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment