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

  1. // ==UserScript==
  2. // @name YouTube Transcript Copier
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.3.0
  5. // @description Adds a 'Copy Transcript' button to the action bar (Like/Dislike/Share) and copies YouTube video transcripts with timestamps. Auto-expands description.
  6. // @author MrPickleMna
  7. // @match https://www.youtube.com/watch*
  8. // @grant GM_setClipboard
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. const OUR_BUTTON_ID = 'pragmatic-copy-transcript-button';
  16. // --- Selectors ---
  17. // Container for action buttons (Like, Share, etc.)
  18. const ACTION_BUTTONS_CONTAINER_SELECTOR = '#top-level-buttons-computed'; // Inside ytd-menu-renderer usually
  19. // The Like/Dislike button group (used as insertion reference)
  20. const LIKE_DISLIKE_SELECTOR = 'segmented-like-dislike-button-view-model';
  21. // Selector for the button that opens the transcript panel (needed for clicking)
  22. const SHOW_TRANSCRIPT_SELECTOR = 'ytd-video-description-transcript-section-renderer button[aria-label="Show transcript"]';
  23. // Selector for the "...more" button in the description (needed for clicking)
  24. const EXPAND_DESC_SELECTOR = '#description-inline-expander #expand';
  25. // Selector for the transcript content panel
  26. const TRANSCRIPT_PANEL_SELECTOR = 'ytd-engagement-panel-section-list-renderer[target-id="engagement-panel-searchable-transcript"]';
  27. // Observer target
  28. const OBSERVER_TARGET_SELECTOR = '#below'; // This still contains the action bar and description
  29.  
  30. console.log('YouTube Transcript Copier: Script initiated (v1.3.0 - Action Bar Button).');
  31.  
  32. /**
  33. * Handles the actual copying process after the button is clicked.
  34. * !! This function still needs to find and click the ORIGINAL "Show transcript" button !!
  35. */
  36. function copyTranscript() {
  37. console.log('Copy Transcript button clicked.');
  38.  
  39. // *** We still need to find and click the *original* button to show the panel ***
  40. const showTranscriptButtonOriginal = document.querySelector(SHOW_TRANSCRIPT_SELECTOR);
  41.  
  42. if (!showTranscriptButtonOriginal) {
  43. console.error("[Copy Transcript] Could not find the *original* 'Show transcript' button in the description area to open the panel.");
  44. alert("Error: Could not find the 'Show transcript' button in the description section. Ensure the description is expanded and the button exists.");
  45. return;
  46. }
  47.  
  48. // Click the original button to ensure the transcript panel opens
  49. showTranscriptButtonOriginal.click();
  50. console.log("[Copy Transcript] 'Show transcript' button (original in description) clicked programmatically.");
  51.  
  52. // --- Wait for the transcript panel to appear and have content ---
  53. // (Rest of the function remains the same as v1.2.0)
  54. const maxAttempts = 20;
  55. let attempts = 0;
  56. const intervalId = setInterval(() => {
  57. const transcriptPanel = document.querySelector(TRANSCRIPT_PANEL_SELECTOR);
  58. if (transcriptPanel && transcriptPanel.querySelector('ytd-transcript-segment-list-renderer')) {
  59. clearInterval(intervalId);
  60. console.log('[Copy Transcript] Transcript panel found and appears loaded:', transcriptPanel);
  61. let transcriptText = '';
  62. const segments = transcriptPanel.querySelectorAll('ytd-transcript-segment-renderer');
  63. if (segments && segments.length > 0) {
  64. segments.forEach(segment => {
  65. const timestampEl = segment.querySelector('.segment-timestamp');
  66. const textEl = segment.querySelector('yt-formatted-string.segment-text');
  67. if (timestampEl && textEl) {
  68. const timestamp = timestampEl.innerText.trim();
  69. const text = textEl.innerText.trim();
  70. transcriptText += `${timestamp} ${text}\n`;
  71. } else {
  72. transcriptText += `${segment.innerText.trim()}\n`;
  73. }
  74. });
  75. transcriptText = transcriptText.trim();
  76. console.log(`[Copy Transcript] Extracted text from ${segments.length} segments.`);
  77. } else {
  78. console.warn("[Copy Transcript] Could not find transcript segments, falling back to innerText of the panel.");
  79. transcriptText = transcriptPanel.innerText.trim();
  80. }
  81.  
  82. if (transcriptText) {
  83. GM_setClipboard(transcriptText, 'text');
  84. console.log('[Copy Transcript] Transcript copied to clipboard.');
  85. alert('Transcript copied to clipboard!');
  86. } else {
  87. console.error('[Copy Transcript] Transcript panel found, but no text content detected after processing.');
  88. alert('Error: Transcript panel loaded but appears empty or could not extract text.');
  89. }
  90. } else {
  91. attempts++;
  92. if (attempts >= maxAttempts) {
  93. clearInterval(intervalId);
  94. console.error('[Copy Transcript] Timed out waiting for transcript panel to load content.');
  95. alert('Error: Timed out waiting for transcript panel.');
  96. }
  97. }
  98. }, 500);
  99. }
  100.  
  101. /**
  102. * Adds the "Copy Transcript" button to the action button row (Like/Dislike/Share).
  103. * Also handles clicking the "...more" button in the description if needed.
  104. **/
  105. function addCopyButtonIfMissing() {
  106. // 1. Check if our button already exists
  107. if (document.getElementById(OUR_BUTTON_ID)) {
  108. return; // Already added
  109. }
  110.  
  111. // 2. Look for and click the "...more" button in the description if necessary.
  112. const expandButton = document.querySelector(EXPAND_DESC_SELECTOR);
  113. if (expandButton && expandButton.offsetParent !== null) {
  114. console.log('[Add Button] Found "...more" description button. Clicking it.');
  115. expandButton.click();
  116. return;
  117. }
  118.  
  119. // 3. Find the target container and reference element for the *new* button location
  120. const actionButtonsContainer = document.querySelector(ACTION_BUTTONS_CONTAINER_SELECTOR);
  121. const likeDislikeGroup = actionButtonsContainer?.querySelector(LIKE_DISLIKE_SELECTOR);
  122.  
  123. // 4. If container and reference point are found, add the button
  124. if (actionButtonsContainer && likeDislikeGroup) {
  125. if (document.getElementById(OUR_BUTTON_ID)) {
  126. return;
  127. }
  128. console.log('[Add Button] Found action buttons container and like/dislike group. Preparing to insert button.');
  129.  
  130. const copyButton = document.createElement('button');
  131. 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';
  132. copyButton.id = OUR_BUTTON_ID;
  133. copyButton.title = 'Copy video transcript';
  134. copyButton.style.marginLeft = '8px'; // Space from Dislike button
  135. // *** ADDED MARGIN-RIGHT ***
  136. copyButton.style.marginRight = '8px'; // Space before Share button
  137.  
  138. const textDiv = document.createElement('div');
  139. textDiv.className = 'yt-spec-button-shape-next__button-text-content';
  140. const textSpan = document.createElement('span');
  141. textSpan.className = 'yt-core-attributed-string yt-core-attributed-string--white-space-no-wrap';
  142. textSpan.setAttribute('role', 'text');
  143. textSpan.innerText = 'Copy Transcript';
  144. textDiv.appendChild(textSpan);
  145. copyButton.appendChild(textDiv);
  146.  
  147. copyButton.addEventListener('click', copyTranscript);
  148.  
  149. likeDislikeGroup.parentNode.insertBefore(copyButton, likeDislikeGroup.nextSibling);
  150. console.log('[Add Button] "Copy Transcript" button inserted into action bar.');
  151.  
  152. } else {
  153. // console.log('[Add Button] Action button container or like/dislike group not found yet.');
  154. }
  155. }
  156.  
  157. // --- MutationObserver Setup ---
  158. // (No changes needed in the observer itself or its setup logic)
  159. console.log('YouTube Transcript Copier: Setting up MutationObserver.');
  160. let observer = null;
  161.  
  162. function startObserver() {
  163. if (observer) {
  164. observer.disconnect();
  165. // console.log('[Observer] Disconnected previous observer.'); // Less noisy log
  166. }
  167.  
  168. const targetNode = document.querySelector(OBSERVER_TARGET_SELECTOR);
  169. if (targetNode) {
  170. // console.log(`[Observer] Target node '${OBSERVER_TARGET_SELECTOR}' found. Starting observer.`); // Less noisy log
  171. observer = new MutationObserver((mutationsList, obs) => {
  172. // Use requestAnimationFrame to debounce slightly and wait for layout changes
  173. window.requestAnimationFrame(addCopyButtonIfMissing);
  174. });
  175. observer.observe(targetNode, {
  176. childList: true,
  177. subtree: true
  178. });
  179. window.requestAnimationFrame(addCopyButtonIfMissing); // Initial check
  180. } else {
  181. console.log(`[Observer] Target node '${OBSERVER_TARGET_SELECTOR}' not found. Retrying in 1 second...`);
  182. setTimeout(startObserver, 1000);
  183. }
  184. }
  185.  
  186. // --- Initial Execution & Navigation Handling ---
  187. // (Navigation handling remains the same as v1.2.0)
  188. setTimeout(startObserver, 1000); // Initial delay
  189.  
  190. document.addEventListener('yt-navigate-finish', (event) => {
  191. console.log('[Navigation] Detected yt-navigate-finish event. Re-running setup.');
  192. setTimeout(startObserver, 500);
  193. });
  194.  
  195. window.addEventListener('popstate', () => {
  196. console.log('[Navigation] Detected popstate event. Re-running setup.');
  197. setTimeout(startObserver, 500);
  198. });
  199.  
  200. })();