NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Zoom Audio Extractor
// @namespace https://github.com/MaxWindt/zoom-audio-extractor
// @version 1.0.2
// @description Extract Zoom recording audio files (Original, English, Deutsch) and export to Google Sheets
// @author MaxWindt
// @match https://zoom.us/recording*
// @match https://zoom.us/rec*
// @grant GM_setClipboard
// @grant GM_notification
// @icon https://zoom.us/favicon.ico
// @homepage https://github.com/MaxWindt/zoom-audio-extractor
// @supportURL https://github.com/MaxWindt/zoom-audio-extractor/issues
// @license MIT
// ==/UserScript==
(function() {
'use strict';
console.log("=== Zoom Audio Extractor Loaded ===");
// Create button to trigger extraction
function createExtractionButton() {
const button = document.createElement('button');
button.id = 'zoom-extract-audio-btn';
button.innerHTML = '🎵 Extract Audio Files';
button.style.cssText = `
position: fixed;
bottom: 20px;
right: 20px;
padding: 12px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
z-index: 9999;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
transition: all 0.3s ease;
`;
button.addEventListener('mouseenter', () => {
button.style.transform = 'translateY(-2px)';
button.style.boxShadow = '0 6px 16px rgba(102, 126, 234, 0.6)';
});
button.addEventListener('mouseleave', () => {
button.style.transform = 'translateY(0)';
button.style.boxShadow = '0 4px 12px rgba(102, 126, 234, 0.4)';
});
// Only start extraction when button is clicked
button.addEventListener('click', () => {
console.log("Button clicked - starting extraction");
button.disabled = true;
button.innerHTML = '⏳ Processing...';
startExtraction(button);
});
document.body.appendChild(button);
console.log("Extraction button created and ready");
}
// Main extraction function - only runs when button is clicked
function startExtraction(button) {
console.log("=== Starting Complete Zoom Audio Extraction ===");
// Step 1: Find and expand all collapsed sections
console.log("\n--- Step 1: Expanding ALL collapsed interpretation sections ---");
const audioListWarps = document.querySelectorAll('[class*="audio_list_warp"]');
console.log(`Found ${audioListWarps.length} total audio list sections\n`);
let collapsedCount = 0;
const headingsToClick = [];
audioListWarps.forEach((warp, warpIndex) => {
const heading = warp.querySelector('[class*="audio_list_info"]');
if (heading) {
const ariaLabel = heading.getAttribute('aria-label') || '';
const sectionName = heading.querySelector('[class*="audio_list_info_title"]')?.textContent?.trim() || `Section ${warpIndex + 1}`;
console.log(`Section ${warpIndex + 1}: ${sectionName}`);
if (ariaLabel.toLowerCase().includes('collapsed')) {
console.log(` → Status: COLLAPSED (will expand)`);
collapsedCount++;
headingsToClick.push({ heading, sectionName, index: warpIndex });
} else {
console.log(` → Status: Already expanded`);
}
}
});
console.log(`\nTotal collapsed sections to expand: ${collapsedCount}\n`);
// Click all sections instantly
console.log("Clicking all sections...");
headingsToClick.forEach(({ heading, sectionName }, index) => {
console.log(`Clicking section ${index + 1}: ${sectionName}`);
heading.click();
});
console.log("\n--- All sections clicked, waiting for content to load ---\n");
// Wait for translations to load before extracting
waitForTranslationsAndExtract(button);
}
// Wait for translations to become visible, then extract
function waitForTranslationsAndExtract(button) {
console.log("Waiting for translation links to load...");
let attempts = 0;
const maxAttempts = 30; // Wait up to 3 seconds (30 * 100ms)
function checkForTranslations() {
attempts++;
// Look for interpretation links
const interpretationLinks = Array.from(document.querySelectorAll('a')).filter(el =>
el.textContent.includes('Audio only - Interpretation')
);
console.log(`Attempt ${attempts}: Found ${interpretationLinks.length} interpretation links`);
// If we found interpretations or hit max attempts, proceed with extraction
if (interpretationLinks.length > 0 || attempts >= maxAttempts) {
if (interpretationLinks.length === 0) {
console.log("Max attempts reached, proceeding with extraction");
} else {
console.log(`Translation links loaded! Found ${interpretationLinks.length} interpretations`);
}
console.log("\n--- Extracting audio files ---\n");
extractAndFormatAudioFiles(button);
return;
}
// Keep checking
setTimeout(checkForTranslations, 100);
}
checkForTranslations();
}
// Extract and format audio files
function extractAndFormatAudioFiles(button) {
const recordings = [];
// Find all clips/recording containers
console.log("Finding all recording containers...");
const clipsContainers = document.querySelectorAll('.clips_container');
console.log(`Found ${clipsContainers.length} recording containers\n`);
clipsContainers.forEach((container, containerIndex) => {
console.log(`Processing Recording Container ${containerIndex + 1}:`);
const itemLists = container.querySelectorAll('.item_list');
let mainAudioLink = '';
let recordingName = '';
const titleEl = container.querySelector('.clip_title');
if (titleEl) {
recordingName = titleEl.textContent.trim();
console.log(` Recording Name: ${recordingName}`);
}
const mainAudioElement = Array.from(itemLists).find(itemList => {
const audioLink = itemList.querySelector('a');
return audioLink && audioLink.textContent.trim() === 'Audio only';
});
if (mainAudioElement) {
const audioLink = mainAudioElement.querySelector('a');
mainAudioLink = audioLink ? audioLink.getAttribute('href') : '';
console.log(` Main Audio Link: ${mainAudioLink}`);
}
const interpretations = {
english: '',
deutsch: ''
};
const interpretationElements = Array.from(itemLists).filter(itemList => {
const audioLink = itemList.querySelector('a');
return audioLink && audioLink.textContent.includes('Audio only - Interpretation');
});
console.log(` Found ${interpretationElements.length} interpretations`);
interpretationElements.forEach(itemList => {
const audioLink = itemList.querySelector('a');
const title = audioLink.textContent.trim();
const link = audioLink.getAttribute('href') || '';
const languageMatch = title.match(/\(([^)]+)\)/);
const language = languageMatch ? languageMatch[1] : '';
if (language === 'English') {
interpretations.english = link;
console.log(` English: ${link}`);
} else if (language === 'Deutsch') {
interpretations.deutsch = link;
console.log(` Deutsch: ${link}`);
}
});
if (mainAudioLink) {
recordings.push({
original: mainAudioLink,
english: interpretations.english,
deutsch: interpretations.deutsch,
name: recordingName
});
}
console.log();
});
// Check for standalone recordings
console.log("Checking for standalone 'Audio only' recordings...");
const allPlainAudioElements = Array.from(document.querySelectorAll('a')).filter(el =>
el.textContent.trim() === 'Audio only' &&
!Array.from(clipsContainers).some(container => container.contains(el))
);
console.log(`Found ${allPlainAudioElements.length} standalone recordings\n`);
allPlainAudioElements.forEach((audioEl) => {
const link = audioEl.getAttribute('href') || '';
if (link && !recordings.some(rec => rec.original === link)) {
recordings.push({
original: link,
english: '',
deutsch: '',
name: 'Standalone Audio'
});
console.log(`Added standalone: ${link}`);
}
});
// Show dialog with table
showDialogWithTable(recordings, button);
}
// Create and show dialog
function showDialogWithTable(recordings, button) {
// Create TSV data
let tsvData = "Original\tEnglish\tDeutsch\n";
recordings.forEach(rec => {
tsvData += `${rec.original || ''}\t${rec.english || ''}\t${rec.deutsch || ''}\n`;
});
// Create styles
const styles = `
<style>
#zoom-audio-dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
overflow: hidden;
}
#zoom-audio-dialog {
background: white;
border-radius: 8px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 900px;
max-height: 80vh;
width: 90%;
display: flex;
flex-direction: column;
overflow: hidden;
}
#zoom-audio-dialog-header {
padding: 24px;
border-bottom: 1px solid #e0e0e0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
flex-shrink: 0;
}
#zoom-audio-dialog-header h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
}
#zoom-audio-dialog-content {
padding: 24px;
overflow-y: auto;
overflow-x: hidden;
flex: 1;
}
#zoom-audio-dialog-content::-webkit-scrollbar {
width: 8px;
}
#zoom-audio-dialog-content::-webkit-scrollbar-track {
background: #f1f1f1;
}
#zoom-audio-dialog-content::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
#zoom-audio-dialog-content::-webkit-scrollbar-thumb:hover {
background: #555;
}
#zoom-audio-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
#zoom-audio-table thead {
background: #f5f5f5;
position: sticky;
top: 0;
z-index: 10;
}
#zoom-audio-table th {
padding: 12px;
text-align: left;
font-weight: 600;
color: #333;
border-bottom: 2px solid #ddd;
}
#zoom-audio-table td {
padding: 12px;
border-bottom: 1px solid #eee;
word-break: break-all;
font-size: 12px;
}
#zoom-audio-table tr:hover {
background: #f9f9f9;
}
#zoom-audio-table td a {
color: #667eea;
text-decoration: none;
cursor: pointer;
}
#zoom-audio-table td a:hover {
text-decoration: underline;
}
#zoom-audio-dialog-footer {
padding: 16px 24px;
border-top: 1px solid #e0e0e0;
display: flex;
justify-content: flex-end;
gap: 12px;
background: #fafafa;
flex-shrink: 0;
}
.zoom-audio-btn {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
#zoom-audio-copy-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
#zoom-audio-copy-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
#zoom-audio-close-btn {
background: #e0e0e0;
color: #333;
}
#zoom-audio-close-btn:hover {
background: #d0d0d0;
}
#zoom-audio-copy-feedback {
color: #4caf50;
font-weight: 600;
margin-right: 16px;
opacity: 0;
transition: opacity 0.3s;
}
#zoom-audio-copy-feedback.show {
opacity: 1;
}
.zoom-audio-stats {
color: #f0f0f0;
font-size: 14px;
margin-top: 8px;
}
</style>
`;
// Create HTML
const html = `
${styles}
<div id="zoom-audio-dialog-overlay">
<div id="zoom-audio-dialog">
<div id="zoom-audio-dialog-header">
<h2>Zoom Audio Files - Ready for Google Sheets</h2>
<div class="zoom-audio-stats">Total Recordings: ${recordings.length}</div>
</div>
<div id="zoom-audio-dialog-content">
<table id="zoom-audio-table">
<thead>
<tr>
<th>Original</th>
<th>English</th>
<th>Deutsch</th>
</tr>
</thead>
<tbody>
${recordings.map(rec => `
<tr>
<td>${rec.original ? `<a href="${rec.original}" target="_blank" title="Open in new tab">Play</a>` : '-'}</td>
<td>${rec.english ? `<a href="${rec.english}" target="_blank" title="Open in new tab">Play</a>` : '-'}</td>
<td>${rec.deutsch ? `<a href="${rec.deutsch}" target="_blank" title="Open in new tab">Play</a>` : '-'}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
<div id="zoom-audio-dialog-footer">
<span id="zoom-audio-copy-feedback">✓ Copied to clipboard!</span>
<button id="zoom-audio-close-btn" class="zoom-audio-btn">Close</button>
<button id="zoom-audio-copy-btn" class="zoom-audio-btn">📋 Copy for Google Sheets</button>
</div>
</div>
</div>
`;
// Insert into page
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
document.body.appendChild(tempDiv);
// Prevent background scroll when dialog is open
const originalOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
// Get elements
const overlay = document.getElementById('zoom-audio-dialog-overlay');
const closeBtn = document.getElementById('zoom-audio-close-btn');
const copyBtn = document.getElementById('zoom-audio-copy-btn');
const feedback = document.getElementById('zoom-audio-copy-feedback');
// Close dialog
function closeDialog() {
overlay.remove();
document.body.style.overflow = originalOverflow;
// Re-enable button
button.disabled = false;
button.innerHTML = '🎵 Extract Audio Files';
}
closeBtn.addEventListener('click', closeDialog);
// Close on overlay click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
closeDialog();
}
});
// Prevent scrolling on overlay
overlay.addEventListener('wheel', (e) => {
e.preventDefault();
}, { passive: false });
// Copy to clipboard
copyBtn.addEventListener('click', () => {
navigator.clipboard.writeText(tsvData).then(() => {
feedback.classList.add('show');
copyBtn.textContent = '✓ Copied!';
setTimeout(() => {
feedback.classList.remove('show');
copyBtn.textContent = '📋 Copy for Google Sheets';
}, 2000);
}).catch(err => {
alert('Failed to copy to clipboard. Please try again.');
});
});
console.log("Dialog opened successfully!");
}
// Initialize - ONLY create the button on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', createExtractionButton);
} else {
createExtractionButton();
}
})();