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 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);
    });

})();