Maxwindt / Zoom Audio Extractor

// ==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();
  }

})();