Zoom Smart Chapters Downloader

Download Zoom Smart Chapters in JSON and Markdown formats: https://gist.github.com/aculich/491ace4a581c8707fa6cd8304d89ea79

// ==UserScript==
// @name         Zoom Smart Chapters Downloader
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Download Zoom Smart Chapters in JSON and Markdown formats: https://gist.github.com/aculich/491ace4a581c8707fa6cd8304d89ea79
// @author       Your name
// @match        https://*.zoom.us/rec/play/*
// @match        https://*.zoom.us/rec/share/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // Utility function to format time in HH:MM:SS
    function formatTime(seconds) {
        return new Date(seconds * 1000).toISOString().substr(11, 8);
    }

    // Parse time string (e.g. "From 00:00" or "From 01:23:45") to seconds
    function parseTimeString(timeStr) {
        const match = timeStr.match(/From (\d{2}:)?(\d{2}):(\d{2})/);
        if (!match) return 0;
        
        const hours = match[1] ? parseInt(match[1]) : 0;
        const minutes = parseInt(match[2]);
        const seconds = parseInt(match[3]);
        
        return hours * 3600 + minutes * 60 + seconds;
    }

    // Get the Unix timestamp in milliseconds for a given offset in seconds
    function getUnixTimestamp(offsetSeconds) {
        // Get the recording start time from the URL if available
        const urlParams = new URLSearchParams(window.location.search);
        const startTimeParam = urlParams.get('startTime');
        
        if (startTimeParam) {
            // If we have a startTime parameter, use it as reference
            const baseTime = parseInt(startTimeParam);
            // Remove the offset that was added to the URL
            const currentOffset = urlParams.get('t') || 0;
            return baseTime - (currentOffset * 1000) + (offsetSeconds * 1000);
        } else {
            // Fallback: Use current time minus total duration as base
            const now = Date.now();
            const videoDuration = document.querySelector('video')?.duration || 0;
            const videoCurrentTime = document.querySelector('video')?.currentTime || 0;
            const startTime = now - ((videoDuration - videoCurrentTime) * 1000);
            return startTime + (offsetSeconds * 1000);
        }
    }

    // Monitor DOM changes for dynamic content
    function setupDynamicContentMonitor() {
        const observer = new MutationObserver((mutations) => {
            mutations.forEach(mutation => {
                if (mutation.type === 'childList' && mutation.addedNodes.length) {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            // Check if this is a summary or description element
                            if (node.classList?.contains('smart-chapter-summary') ||
                                node.classList?.contains('content') ||
                                node.querySelector?.('.smart-chapter-summary, .content')) {
                                console.group('Dynamic Content Added:');
                                console.log('Element:', node);
                                console.log('Class:', node.className);
                                console.log('Content:', node.textContent?.trim().substring(0, 100) + '...');
                                console.log('Full HTML:', node.outerHTML);
                                console.groupEnd();
                            }
                        }
                    });
                }
            });
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        return observer;
    }

    // Monitor network requests for API calls
    function setupNetworkMonitor() {
        const originalFetch = window.fetch;
        window.fetch = async function(...args) {
            const url = args[0];
            if (typeof url === 'string' && url.includes('zoom.us')) {
                console.group('Zoom API Request:');
                console.log('URL:', url);
                console.log('Args:', args[1]);
                console.groupEnd();
            }
            return originalFetch.apply(this, args);
        };

        const originalXHR = window.XMLHttpRequest.prototype.open;
        window.XMLHttpRequest.prototype.open = function(...args) {
            const url = args[1];
            if (typeof url === 'string' && url.includes('zoom.us')) {
                console.group('Zoom XHR Request:');
                console.log('URL:', url);
                console.log('Method:', args[0]);
                console.groupEnd();
            }
            return originalXHR.apply(this, args);
        };
    }

    // Helper function to wait for an element
    function waitForElement(selector, timeout = 2000) {
        return new Promise((resolve) => {
            if (document.querySelector(selector)) {
                return resolve(document.querySelector(selector));
            }

            const observer = new MutationObserver(() => {
                if (document.querySelector(selector)) {
                    observer.disconnect();
                    resolve(document.querySelector(selector));
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });

            setTimeout(() => {
                observer.disconnect();
                resolve(null);
            }, timeout);
        });
    }

    // Extract chapters from the DOM with enhanced dynamic content handling
    async function extractChapters() {
        const chapters = [];
        const chapterElements = document.querySelectorAll('.smart-chapter-card');
        
        // Get the base URL from og:url meta tag
        const ogUrlMeta = document.querySelector('meta[property="og:url"]');
        const baseUrl = ogUrlMeta ? ogUrlMeta.content : window.location.href.split('?')[0];
        
        // Get the original startTime from URL - this must remain constant across all chapter links
        // due to Zoom's URL handling limitations
        const urlParams = new URLSearchParams(window.location.search);
        const originalStartTime = urlParams.get('startTime') || '';
        
        // Note: Due to Zoom's URL handling limitations, we must:
        // 1. Keep the original startTime parameter the same across all chapter links
        // 2. Add our calculated chapter start times in a separate parameter (chapterStartTime)
        // This is because Zoom's player currently only respects the first chapter's startTime
        // and ignores subsequent chapter timings. We keep our calculated times in the URL
        // for potential future workarounds or third-party tools.
        
        console.group('Interactive Chapter Extraction:');
        
        for (let index = 0; index < chapterElements.length; index++) {
            const el = chapterElements[index];
            const timeEl = el.querySelector('.start-time');
            const titleEl = el.querySelector('.chapter-card-title');
            
            if (timeEl && titleEl) {
                console.group(`Processing Chapter ${index + 1}`);
                const timeStr = timeEl.textContent.trim();
                const title = titleEl.textContent.trim();
                console.log('Found title:', title);
                
                // Try to trigger content loading through various interactions
                console.group('Triggering Interactions:');
                
                // 1. Click the chapter card
                console.log('Clicking chapter card...');
                el.click();
                // Wait longer after clicking the card
                console.log('Waiting for UI update...');
                await new Promise(r => setTimeout(r, 1500));
                
                // 2. Try to find any clickable elements within the card
                const clickables = el.querySelectorAll('button, [role="button"], [tabindex="0"]');
                for (const clickable of clickables) {
                    console.log('Clicking element:', clickable.className);
                    clickable.click();
                    // Wait between clicking different elements
                    await new Promise(r => setTimeout(r, 800));
                }
                
                // 3. Look for Vue.js related elements
                const vueElements = el.querySelectorAll('[data-v-5eece099]');
                console.log(`Found ${vueElements.length} Vue elements`);
                vueElements.forEach(vueEl => {
                    if (vueEl.__vue__) {
                        console.log('Vue instance found:', vueEl.__vue__.$data);
                        try {
                            vueEl.__vue__.$emit('click');
                            vueEl.__vue__.$emit('select');
                        } catch (e) {
                            console.log('Vue event emission failed:', e);
                        }
                    }
                });
                
                // 4. Wait for potential dynamic content
                console.log('Waiting for description content...');
                const summaryEl = await waitForElement('.smart-chapter-summary');
                if (summaryEl) {
                    console.log('Found summary element after waiting');
                    // Add extra wait after finding summary element
                    await new Promise(r => setTimeout(r, 1000));
                }
                
                console.groupEnd();
                
                const offsetSeconds = parseTimeString(timeStr);
                const startTime = getUnixTimestamp(offsetSeconds);

                // Get description using multiple approaches
                let description = '';
                
                // Try different selectors and approaches
                const attempts = [
                    // Direct content div under summary
                    () => document.querySelector(`.smart-chapter-summary:nth-child(${index + 1}) .content > div`)?.textContent,
                    // Active/selected summary
                    () => document.querySelector('.smart-chapter-summary.active .content > div')?.textContent,
                    // Summary with matching title
                    () => Array.from(document.querySelectorAll('.smart-chapter-summary'))
                        .find(sum => sum.querySelector('.title')?.textContent.includes(title))
                        ?.querySelector('.content > div')?.textContent,
                    // Any visible summary content
                    () => document.querySelector('.smart-chapter-summary:not([style*="display: none"]) .content > div')?.textContent
                ];
                
                for (const attempt of attempts) {
                    const result = attempt();
                    if (result) {
                        description = result.trim();
                        console.log('Found description using attempt:', description.substring(0, 50) + '...');
                        break;
                    }
                    // Add small delay between attempts
                    await new Promise(r => setTimeout(r, 300));
                }

                console.log('Final description length:', description.length);
                console.groupEnd();

                chapters.push({
                    timestamp: timeStr,
                    startTime: startTime,
                    title: title,
                    description: description,
                    // Keep original startTime and add our calculated time as chapterStartTime
                    url: `${baseUrl}?${originalStartTime ? `startTime=${originalStartTime}&` : ''}chapterStartTime=${startTime}`
                });
                
                // Much longer delay (10 seconds) between processing chapters
                const nextChapter = index + 2;
                const totalChapters = chapterElements.length;
                console.log(`Waiting 1 seconds before processing chapter ${nextChapter}/${totalChapters}...`);
                await new Promise(r => setTimeout(r, 1000));
            }
        }
        
        console.groupEnd();
        return chapters;
    }

    // Convert chapters to markdown format
    function chaptersToMarkdown(chapters) {
        return chapters.map(chapter => {
            // Use the original timestamp from the HTML instead of converting Unix time
            const time = chapter.timestamp.replace('From ', '');
            return `## [${chapter.title} (${time})](${chapter.url})\n\n${chapter.description}\n`;
        }).join('\n');
    }

    // Convert chapters to JSON format
    function chaptersToJSON(chapters) {
        return JSON.stringify(chapters, null, 2);
    }

    // Download content as file
    function downloadFile(content, filename) {
        const blob = new Blob([content], { type: 'text/plain' });
        const url = window.URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        window.URL.revokeObjectURL(url);
        document.body.removeChild(a);
    }

    // Create banner with enhanced debug capabilities
    function createBanner() {
        const banner = document.createElement('div');
        banner.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            background: #2D8CFF;
            color: white;
            padding: 10px;
            z-index: 9999;
            display: flex;
            justify-content: center;
            align-items: center;
            box-shadow: 0 2px 4px rgba(0,0,0,0.2);
        `;

        const container = document.createElement('div');
        container.style.cssText = `
            display: flex;
            gap: 10px;
            align-items: center;
        `;

        const label = document.createElement('span');
        label.textContent = 'Smart Chapters:';
        label.style.fontWeight = 'bold';

        // Common button style
        const buttonStyle = `
            padding: 5px 15px;
            border-radius: 4px;
            border: none;
            background: white;
            color: #2D8CFF;
            cursor: pointer;
            font-weight: bold;
        `;

        // Common function to extract and process chapters
        async function getProcessedChapters() {
            console.group('Starting Chapter Extraction');
            const chapters = await extractChapters();
            console.log('Total chapters extracted:', chapters.length);
            return chapters;
        }

        const jsonButton = document.createElement('button');
        jsonButton.textContent = 'Download JSON';
        jsonButton.style.cssText = buttonStyle;
        jsonButton.onclick = async () => {
            const chapters = await getProcessedChapters();
            downloadFile(chaptersToJSON(chapters), `zoom-chapters-${Date.now()}.json`);
            console.groupEnd();
        };

        const mdButton = document.createElement('button');
        mdButton.textContent = 'Download Markdown';
        mdButton.style.cssText = buttonStyle;
        mdButton.onclick = async () => {
            const chapters = await getProcessedChapters();
            downloadFile(chaptersToMarkdown(chapters), `zoom-chapters-${Date.now()}.md`);
            console.groupEnd();
        };

        const debugButton = document.createElement('button');
        debugButton.textContent = '🔍 Debug Log';
        debugButton.style.cssText = buttonStyle;
        debugButton.onclick = async () => {
            const chapters = await getProcessedChapters();
            
            // Additional debug logging
            console.group('Smart Chapters Debug Info');
            
            // Check window for global variables
            console.group('Global Variables:');
            const globals = ['smartChapters', 'chapters', 'zoomChapters', 'recording'].filter(
                key => window[key] !== undefined
            );
            console.log('Found globals:', globals);
            globals.forEach(key => console.log(key + ':', window[key]));
            console.groupEnd();
            
            // Check for React/Vue devtools
            console.group('Framework Detection:');
            console.log('Vue detected:', !!window.__VUE_DEVTOOLS_GLOBAL_HOOK__);
            console.log('React detected:', !!window.__REACT_DEVTOOLS_GLOBAL_HOOK__);
            console.groupEnd();
            
            chapters.forEach((chapter, index) => {
                console.group(`Chapter ${index + 1}: ${chapter.title}`);
                console.log('Timestamp:', chapter.timestamp);
                console.log('Unix Time:', chapter.startTime);
                console.log('Title:', chapter.title);
                console.log('Description:', chapter.description || '(no description)');
                console.log('URL:', chapter.url);
                console.groupEnd();
            });
            console.groupEnd();
            console.groupEnd();
        };

        container.appendChild(label);
        container.appendChild(jsonButton);
        container.appendChild(mdButton);
        container.appendChild(debugButton);
        banner.appendChild(container);

        // Adjust page content to account for banner height
        const contentAdjuster = document.createElement('div');
        contentAdjuster.style.height = '50px';
        document.body.insertBefore(contentAdjuster, document.body.firstChild);

        // Start monitors
        setupDynamicContentMonitor();
        setupNetworkMonitor();

        return banner;
    }

    // Main function to initialize the script
    function init() {
        // Wait for the Smart Chapters container to be available
        const checkForChapters = setInterval(() => {
            const chaptersContainer = document.querySelector('.smart-chapter-container');
            if (!chaptersContainer) return;

            // Only add the banner if it doesn't exist yet
            if (!document.getElementById('smart-chapters-banner')) {
                const banner = createBanner();
                banner.id = 'smart-chapters-banner';
                document.body.insertBefore(banner, document.body.firstChild);
                clearInterval(checkForChapters);
            }
        }, 1000);

        // Clear interval after 30 seconds to prevent infinite checking
        setTimeout(() => clearInterval(checkForChapters), 30000);
    }

    // Start the script
    init();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址