Skip to content

Instantly share code, notes, and snippets.

@gotexis
Last active March 9, 2026 03:53
Show Gist options
  • Select an option

  • Save gotexis/746cad1c40292c8f9386af7826321856 to your computer and use it in GitHub Desktop.

Select an option

Save gotexis/746cad1c40292c8f9386af7826321856 to your computer and use it in GitHub Desktop.
MS Teams Transcript Downloader
// ==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