YouTube Transcript Copier

Adds a styled button to copy the YouTube video transcript, with a timestamp toggle

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         YouTube Transcript Copier
// @match        https://www.youtube.com/watch*
// @license      MIT
// @grant        none
// @version      1.0
// @author       Amir Tehrani
// @description  Adds a styled button to copy the YouTube video transcript, with a timestamp toggle
// @namespace    https://greasyfork.org/
// @icon         https://www.google.com/s2/favicons?domain=youtube.com
// ==/UserScript==

(function() {
    'use strict';

    let observer = null;
    let currentURL = window.location.href;
    let insertionAttempts = 0;
    const maxAttempts = 20;
    let retryInterval = null;
    let includeTimestamps = false; // Default: no timestamps
    let copyButton = null;
    let buttonTextNode = null;
    let transcriptPanelTimeout = null; // Timeout for panel loading
    let transcriptButtonTimeout = null; // Timeout for the "Show transcript" button

    function createTranscriptButton() {
        if (document.getElementById('show-transcript-button')) {
            return true;
        }

        copyButton = document.createElement('button');
        copyButton.id = 'show-transcript-button';
        copyButton.classList.add('yt-transcript-button');
        copyButton.setAttribute('aria-label', 'Copy Transcript'); // Accessibility

        buttonTextNode = document.createTextNode('Copy Transcript');
        copyButton.appendChild(buttonTextNode);

        const timestampSpan = document.createElement('span');
        timestampSpan.id = 'timestamp-toggle';
        timestampSpan.textContent = ' (No Time)';
        timestampSpan.style.cssText = `
            font-size: 0.75em;
            margin-left: 6px;
            color: rgba(255, 255, 255, 0.7);
            cursor: pointer;
            user-select: none;
            border: 1px solid rgba(255, 255, 255, 0.3);
            border-radius: 12px;
            padding: 3px 6px;
            display: inline-block;
            vertical-align: middle;
            transition: color 0.2s ease, border-color 0.2s ease, background-color 0.2s ease;
            background-color: rgba(0, 0, 0, 0.1);
        `;

        timestampSpan.addEventListener('mouseover', function() {
            this.style.borderColor = 'rgba(255, 255, 255, 0.9)';
            this.style.backgroundColor = 'rgba(0, 0, 0, 0.2)';
        });
        timestampSpan.addEventListener('mouseout', function() {
            this.style.borderColor = includeTimestamps ? 'white' : 'rgba(255, 255, 255, 0.3)';
            this.style.backgroundColor = includeTimestamps ? 'rgba(0,0,0, 0.4)' : 'rgba(0, 0, 0, 0.1)';
        });

        copyButton.appendChild(timestampSpan);

        timestampSpan.addEventListener('click', function(event) {
            event.stopPropagation(); // Prevent main button click
            includeTimestamps = !includeTimestamps;
            this.textContent = includeTimestamps ? ' (Time)' : ' (No Time)';
            this.style.color = includeTimestamps ? 'white' : 'rgba(255, 255, 255, 0.7)';
            this.style.borderColor = includeTimestamps ? 'white' : 'rgba(255, 255, 255, 0.3)';
            this.style.backgroundColor = includeTimestamps ? 'rgba(0,0,0, 0.4)' : 'rgba(0, 0, 0, 0.1)';
        });

        copyButton.addEventListener('click', handleCopyClick);

        function insertButton() {
            const potentialTargets = [
                '#description #top-level-buttons-computed',
                '#top-row.ytd-watch-metadata',
                '#above-the-fold',
                '#meta-contents',
                '#primary-inner'
            ];

            for (const targetSelector of potentialTargets) {
                const targetElement = document.querySelector(targetSelector);
                if (targetElement) {
                    targetElement.parentNode.insertBefore(copyButton, targetElement.nextSibling);
                    injectStyles();
                    return true;
                }
            }
            return false;
        }

        return insertButton();
    }

    function handleCopyClick() {
        updateButtonText('Copy Transcript');

        const moreActionsButton = document.querySelector('button[aria-label="More actions"]');
        if (moreActionsButton) {
            moreActionsButton.click();
        }

        const buttonIntervalId = setInterval(() => {
            const transcriptButton = document.querySelector('[aria-label="Show transcript"]');
            if (transcriptButton) {
                transcriptButton.click();
                clearInterval(buttonIntervalId);
                clearTimeout(transcriptButtonTimeout);

                const panelIntervalId = setInterval(() => {
                    const transcriptPanel = document.querySelector('ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-searchable-transcript"] #content');
                    if (transcriptPanel && transcriptPanel.querySelector('ytd-transcript-segment-renderer')) {
                        clearInterval(panelIntervalId);
                        clearTimeout(transcriptPanelTimeout);
                        copyTranscriptText(transcriptPanel);
                    }
                }, 100);

                transcriptPanelTimeout = setTimeout(() => {
                    clearInterval(panelIntervalId);
                    console.error("Transcript panel or segments not found after timeout.");
                    if (copyButton) {
                        updateButtonText("Transcript Not Found");
                        copyButton.style.backgroundColor = "rgba(220, 53, 69, 0.8)";
                    }
                }, 15000);
            }
        }, 250);

        transcriptButtonTimeout = setTimeout(() => {
            clearInterval(buttonIntervalId);
            if (copyButton) {
                updateButtonText("Transcript Not Found")
                copyButton.style.backgroundColor = "rgba(220, 53, 69, 0.8)";
            }
            console.error("Transcript button not found after timeout.");
        }, 10000);
    }

    function copyTranscriptText(transcriptPanel) {
        if (!transcriptPanel) {
            console.error("Transcript container not found.");
            updateButtonText("Error");
            return;
        }

        let transcriptText = "";

        if (includeTimestamps) {
            transcriptPanel.querySelectorAll('ytd-transcript-segment-renderer').forEach(line => {
                const timestampElement = line.querySelector('.segment-timestamp');
                const textElement = line.querySelector('.segment-text');
                if (timestampElement && textElement) {
                    transcriptText += timestampElement.textContent.trim() + " " + textElement.textContent.trim() + "\n";
                }
            });
        } else {
            transcriptPanel.querySelectorAll('.segment-text').forEach(segment => {
                transcriptText += segment.textContent.trim() + " ";
            });
        }

        navigator.clipboard.writeText(transcriptText)
            .then(() => {
              updateButtonText("Copied!");
            })
            .catch(err => {
                console.error('Failed to copy transcript:', err);
                if (copyButton) {
                    updateButtonText("Copy Failed");
                    copyButton.style.backgroundColor = "rgba(220, 53, 69, 0.8)";
                }
            });
    }

    function updateButtonText(text) {
        if (copyButton && buttonTextNode) {
            buttonTextNode.textContent = text;
            if (text === "Copied!") {
                copyButton.style.backgroundColor = "rgba(40, 167, 69, 0.9)";
                  setTimeout(() => {
                        buttonTextNode.textContent = 'Copy Transcript';
                         copyButton.style.backgroundColor = 'rgba(0, 123, 255, 0.8)';

                         const timestampToggle = document.getElementById('timestamp-toggle');
                         timestampToggle.textContent = includeTimestamps ? ' (Time)' : ' (No Time)';
                         timestampToggle.style.color = includeTimestamps ? 'white' : 'rgba(255, 255, 255, 0.7)';
                         timestampToggle.style.borderColor = includeTimestamps ? 'white' : 'rgba(255, 255, 255, 0.3)';
                         timestampToggle.style.backgroundColor = includeTimestamps ? 'rgba(0,0,0, 0.4)' : 'rgba(0, 0, 0, 0.1)';

                    }, 1500);
            } else if (text.startsWith("Error") || text === "Copy Failed" || text === "Transcript Not Found") {
                 copyButton.style.backgroundColor = "rgba(220, 53, 69, 0.8)";
            }
        }
    }

    function injectStyles() {
        if (document.getElementById('yt-transcript-button-styles')) return;

        const style = document.createElement('style');
        style.id = 'yt-transcript-button-styles';
        style.textContent = `
            .yt-transcript-button {
                background-color: rgba(0, 123, 255, 0.8);
                border: none;
                color: white;
                padding: 10px 18px;
                text-align: center;
                text-decoration: none;
                display: inline-flex;
                align-items: center;
                font-size: 15px;
                margin: 4px 2px;
                cursor: pointer;
                border-radius: 24px;
                transition: all 0.2s ease;
                box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
                font-family: 'Roboto', sans-serif;
                font-weight: 500;
                position: relative;
                overflow: hidden;
                will-change: transform, box-shadow, background-color;
            }

            .yt-transcript-button:hover {
                background-color: rgba(0, 90, 180, 0.9);
                box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
                transform: translateY(-1px);
            }

            .yt-transcript-button:focus {
                outline: none;
                box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.3);
            }

            .yt-transcript-button:active {
                background-color: rgba(0, 60, 120, 0.9);
                box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
                transform: translateY(1px);
            }

            .yt-transcript-button::before {
                content: '';
                position: absolute;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background: radial-gradient(circle at 0 0, rgba(255, 255, 255, 0.4) 10%, transparent 10.01%);
                background-size: 0 0;
                opacity: 0;
                pointer-events: none;
                transition: background-size 0.4s ease, opacity 0.4s ease, background-position 0.4s ease;
            }

            .yt-transcript-button:active::before {
                background-size: 200% 200%;
                opacity: 1;
                transition: background-size 0.4s ease, opacity 0.4s ease;
            }
        `;
        document.head.appendChild(style);

        copyButton.addEventListener('mousedown', function(event) {
            const rect = copyButton.getBoundingClientRect();
            const x = event.clientX - rect.left;
            const y = event.clientY - rect.top;

            copyButton.style.setProperty('--ripple-x', x + 'px');
            copyButton.style.setProperty('--ripple-y', y + 'px');
            copyButton.style.background = `radial-gradient(circle at var(--ripple-x) var(--ripple-y), rgba(255, 255, 255, 0.4) 10%, transparent 10.01%)`;
        });

        copyButton.addEventListener('mouseup', function(event){
            copyButton.style.background = null;
        });

        copyButton.addEventListener("mouseleave", function(event) {
            copyButton.style.background = null;
        });
    }

    function attemptButtonCreation() {
        insertionAttempts++;
        if (createTranscriptButton() || insertionAttempts >= maxAttempts) {
            clearInterval(retryInterval);
            retryInterval = null;
            if (insertionAttempts >= maxAttempts) {
                console.error('Could not insert the button after multiple attempts.');
            }
        }
    }

    function setupObserver() {
        if (observer) observer.disconnect();
        observer = new MutationObserver(handleMutations);
        observer.observe(document.body, { childList: true, subtree: true });
    }

    function handleMutations(mutations) {
        if (window.location.href !== currentURL) {
            currentURL = window.location.href;
            resetState();
            startProcess();
        }
    }

    function resetState() {
        if (document.getElementById('show-transcript-button')) {
            document.getElementById('show-transcript-button').remove();
        }
        insertionAttempts = 0;
        if (retryInterval) {
            clearInterval(retryInterval);
            retryInterval = null;
        }
        clearTimeout(transcriptPanelTimeout);
        clearTimeout(transcriptButtonTimeout);
        transcriptPanelTimeout = null;
        transcriptButtonTimeout = null;

        if (observer) {
          observer.disconnect();
          observer = null;
        }

        copyButton = null;
        buttonTextNode = null;
    }

    function startProcess() {
        if (!createTranscriptButton()) {
            retryInterval = setInterval(attemptButtonCreation, 500);
        }
        setupObserver();
    }

    startProcess();
})();