YouTube Transcript Copier

Adds a 'Copy Transcript' button to the action bar (Like/Dislike/Share) and copies YouTube video transcripts with timestamps. Auto-expands description.

当前为 2025-04-30 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         YouTube Transcript Copier
// @namespace    http://tampermonkey.net/
// @version      1.3.0
// @description  Adds a 'Copy Transcript' button to the action bar (Like/Dislike/Share) and copies YouTube video transcripts with timestamps. Auto-expands description.
// @author       MrPickleMna
// @match        https://www.youtube.com/watch*
// @grant        GM_setClipboard
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const OUR_BUTTON_ID = 'pragmatic-copy-transcript-button';
    // --- Selectors ---
    // Container for action buttons (Like, Share, etc.)
    const ACTION_BUTTONS_CONTAINER_SELECTOR = '#top-level-buttons-computed'; // Inside ytd-menu-renderer usually
    // The Like/Dislike button group (used as insertion reference)
    const LIKE_DISLIKE_SELECTOR = 'segmented-like-dislike-button-view-model';
    // Selector for the button that opens the transcript panel (needed for clicking)
    const SHOW_TRANSCRIPT_SELECTOR = 'ytd-video-description-transcript-section-renderer button[aria-label="Show transcript"]';
    // Selector for the "...more" button in the description (needed for clicking)
    const EXPAND_DESC_SELECTOR = '#description-inline-expander #expand';
    // Selector for the transcript content panel
    const TRANSCRIPT_PANEL_SELECTOR = 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-searchable-transcript"]';
    // Observer target
    const OBSERVER_TARGET_SELECTOR = '#below'; // This still contains the action bar and description

    console.log('YouTube Transcript Copier: Script initiated (v1.3.0 - Action Bar Button).');

    /**
     * Handles the actual copying process after the button is clicked.
     * !! This function still needs to find and click the ORIGINAL "Show transcript" button !!
     */
    function copyTranscript() {
        console.log('Copy Transcript button clicked.');

        // *** We still need to find and click the *original* button to show the panel ***
        const showTranscriptButtonOriginal = document.querySelector(SHOW_TRANSCRIPT_SELECTOR);

        if (!showTranscriptButtonOriginal) {
            console.error("[Copy Transcript] Could not find the *original* 'Show transcript' button in the description area to open the panel.");
            alert("Error: Could not find the 'Show transcript' button in the description section. Ensure the description is expanded and the button exists.");
            return;
        }

        // Click the original button to ensure the transcript panel opens
        showTranscriptButtonOriginal.click();
        console.log("[Copy Transcript] 'Show transcript' button (original in description) clicked programmatically.");

        // --- Wait for the transcript panel to appear and have content ---
        // (Rest of the function remains the same as v1.2.0)
        const maxAttempts = 20;
        let attempts = 0;
        const intervalId = setInterval(() => {
            const transcriptPanel = document.querySelector(TRANSCRIPT_PANEL_SELECTOR);
            if (transcriptPanel && transcriptPanel.querySelector('ytd-transcript-segment-list-renderer')) {
                clearInterval(intervalId);
                console.log('[Copy Transcript] Transcript panel found and appears loaded:', transcriptPanel);
                let transcriptText = '';
                const segments = transcriptPanel.querySelectorAll('ytd-transcript-segment-renderer');
                if (segments && segments.length > 0) {
                    segments.forEach(segment => {
                        const timestampEl = segment.querySelector('.segment-timestamp');
                        const textEl = segment.querySelector('yt-formatted-string.segment-text');
                        if (timestampEl && textEl) {
                            const timestamp = timestampEl.innerText.trim();
                            const text = textEl.innerText.trim();
                            transcriptText += `${timestamp} ${text}\n`;
                        } else {
                            transcriptText += `${segment.innerText.trim()}\n`;
                        }
                    });
                    transcriptText = transcriptText.trim();
                    console.log(`[Copy Transcript] Extracted text from ${segments.length} segments.`);
                } else {
                    console.warn("[Copy Transcript] Could not find transcript segments, falling back to innerText of the panel.");
                    transcriptText = transcriptPanel.innerText.trim();
                }

                if (transcriptText) {
                    GM_setClipboard(transcriptText, 'text');
                    console.log('[Copy Transcript] Transcript copied to clipboard.');
                    alert('Transcript copied to clipboard!');
                } else {
                    console.error('[Copy Transcript] Transcript panel found, but no text content detected after processing.');
                    alert('Error: Transcript panel loaded but appears empty or could not extract text.');
                }
            } else {
                attempts++;
                if (attempts >= maxAttempts) {
                    clearInterval(intervalId);
                    console.error('[Copy Transcript] Timed out waiting for transcript panel to load content.');
                    alert('Error: Timed out waiting for transcript panel.');
                }
            }
        }, 500);
    }

    /**
     * Adds the "Copy Transcript" button to the action button row (Like/Dislike/Share).
     * Also handles clicking the "...more" button in the description if needed.
     **/
    function addCopyButtonIfMissing() {
        // 1. Check if our button already exists
        if (document.getElementById(OUR_BUTTON_ID)) {
            return; // Already added
        }

        // 2. Look for and click the "...more" button in the description if necessary.
        const expandButton = document.querySelector(EXPAND_DESC_SELECTOR);
        if (expandButton && expandButton.offsetParent !== null) {
            console.log('[Add Button] Found "...more" description button. Clicking it.');
            expandButton.click();
            return;
        }

        // 3. Find the target container and reference element for the *new* button location
        const actionButtonsContainer = document.querySelector(ACTION_BUTTONS_CONTAINER_SELECTOR);
        const likeDislikeGroup = actionButtonsContainer?.querySelector(LIKE_DISLIKE_SELECTOR);

        // 4. If container and reference point are found, add the button
        if (actionButtonsContainer && likeDislikeGroup) {
            if (document.getElementById(OUR_BUTTON_ID)) {
                return;
            }
            console.log('[Add Button] Found action buttons container and like/dislike group. Preparing to insert button.');

            const copyButton = document.createElement('button');
            copyButton.className = 'yt-spec-button-shape-next yt-spec-button-shape-next--tonal yt-spec-button-shape-next--mono yt-spec-button-shape-next--size-m yt-spec-button-shape-next--icon-leading';
            copyButton.id = OUR_BUTTON_ID;
            copyButton.title = 'Copy video transcript';
            copyButton.style.marginLeft = '8px'; // Space from Dislike button
            // *** ADDED MARGIN-RIGHT ***
            copyButton.style.marginRight = '8px'; // Space before Share button

            const textDiv = document.createElement('div');
            textDiv.className = 'yt-spec-button-shape-next__button-text-content';
            const textSpan = document.createElement('span');
            textSpan.className = 'yt-core-attributed-string yt-core-attributed-string--white-space-no-wrap';
            textSpan.setAttribute('role', 'text');
            textSpan.innerText = 'Copy Transcript';
            textDiv.appendChild(textSpan);
            copyButton.appendChild(textDiv);

            copyButton.addEventListener('click', copyTranscript);

            likeDislikeGroup.parentNode.insertBefore(copyButton, likeDislikeGroup.nextSibling);
            console.log('[Add Button] "Copy Transcript" button inserted into action bar.');

        } else {
            // console.log('[Add Button] Action button container or like/dislike group not found yet.');
        }
    }

    // --- MutationObserver Setup ---
    // (No changes needed in the observer itself or its setup logic)
    console.log('YouTube Transcript Copier: Setting up MutationObserver.');
    let observer = null;

    function startObserver() {
        if (observer) {
            observer.disconnect();
            // console.log('[Observer] Disconnected previous observer.'); // Less noisy log
        }

        const targetNode = document.querySelector(OBSERVER_TARGET_SELECTOR);
        if (targetNode) {
            // console.log(`[Observer] Target node '${OBSERVER_TARGET_SELECTOR}' found. Starting observer.`); // Less noisy log
            observer = new MutationObserver((mutationsList, obs) => {
                // Use requestAnimationFrame to debounce slightly and wait for layout changes
                window.requestAnimationFrame(addCopyButtonIfMissing);
            });
            observer.observe(targetNode, {
                childList: true,
                subtree: true
            });
            window.requestAnimationFrame(addCopyButtonIfMissing); // Initial check
        } else {
            console.log(`[Observer] Target node '${OBSERVER_TARGET_SELECTOR}' not found. Retrying in 1 second...`);
            setTimeout(startObserver, 1000);
        }
    }

    // --- Initial Execution & Navigation Handling ---
    // (Navigation handling remains the same as v1.2.0)
    setTimeout(startObserver, 1000); // Initial delay

    document.addEventListener('yt-navigate-finish', (event) => {
        console.log('[Navigation] Detected yt-navigate-finish event. Re-running setup.');
        setTimeout(startObserver, 500);
    });

    window.addEventListener('popstate', () => {
        console.log('[Navigation] Detected popstate event. Re-running setup.');
        setTimeout(startObserver, 500);
    });

})();