Greasy Fork

来自缓存

Greasy Fork is available in English.

Zoom Smart Chapters Downloader

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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