您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a 'Copy Transcript' button to the action bar (Like/Dislike/Share) and copies YouTube video transcripts with timestamps. Auto-expands description.
当前为
- // ==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);
- });
- })();