YouTube Transcript

Kopiert das offene YouTube-Transcript zuverlässig in die Zwischenablage

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         YouTube Transcript
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Kopiert das offene YouTube-Transcript zuverlässig in die Zwischenablage
// @match        https://www.youtube.com/watch*
// @grant        GM_setClipboard
// @run-at       document-idle
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

  function log(...args) {
    console.log('[YT Transcript]', ...args);
  }

  function showToast(message, type = 'success') {
    const existing = document.getElementById('yt-transcript-toast');
    if (existing) existing.remove();

    const toast = document.createElement('div');
    toast.id = 'yt-transcript-toast';
    toast.textContent = message;

    toast.style.position = 'fixed';
    toast.style.top = '20px';
    toast.style.right = '20px';
    toast.style.zIndex = '999999';
    toast.style.padding = '10px 16px';
    toast.style.borderRadius = '12px';
    toast.style.background = type === 'error' ? 'rgba(180, 40, 40, 0.95)' : 'rgba(32, 32, 32, 0.95)';
    toast.style.color = '#fff';
    toast.style.fontSize = '14px';
    toast.style.fontWeight = '500';
    toast.style.fontFamily = '"Roboto","Arial",sans-serif';
    toast.style.boxShadow = '0 6px 18px rgba(0,0,0,0.25)';
    toast.style.opacity = '0';
    toast.style.transform = 'translateY(-8px)';
    toast.style.transition = 'opacity 0.2s ease, transform 0.2s ease';
    toast.style.pointerEvents = 'none';

    document.body.appendChild(toast);

    requestAnimationFrame(() => {
      toast.style.opacity = '1';
      toast.style.transform = 'translateY(0)';
    });

    setTimeout(() => {
      toast.style.opacity = '0';
      toast.style.transform = 'translateY(-8px)';
      setTimeout(() => toast.remove(), 220);
    }, 2200);
  }

  function getTranscriptPanels() {
    return [
      ...document.querySelectorAll('ytd-engagement-panel-section-list-renderer')
    ].filter(panel => {
      const targetId =
        panel.getAttribute('target-id') ||
        panel.dataset.targetId ||
        panel.querySelector('[data-target-id]')?.getAttribute('data-target-id') ||
        '';

      return /transcript|PAmodern_transcript_view/i.test(targetId);
    });
  }

  function getVisibleTranscriptPanel() {
    const panels = getTranscriptPanels();

    for (const panel of panels) {
      const style = window.getComputedStyle(panel);
      const hidden =
        panel.hasAttribute('hidden') ||
        style.display === 'none' ||
        style.visibility === 'hidden';

      if (!hidden) return panel;
    }

    return panels[0] || null;
  }

  function getTranscriptRoot() {
    const panel = getVisibleTranscriptPanel();
    if (!panel) return null;

    return (
      panel.querySelector('.ytSectionListRendererContents') ||
      panel.querySelector('#contents') ||
      panel.querySelector('yt-section-list-renderer') ||
      panel.querySelector('#content') ||
      panel
    );
  }

  function getTranscriptSegmentsFromRoot(root) {
    if (!root) return [];

    const modernSegments = [...root.querySelectorAll('transcript-segment-view-model')];
    if (modernSegments.length) {
      return modernSegments.map(seg => {
        const textNode =
          seg.querySelector('.yt-core-attributed-string[role="text"]') ||
          seg.querySelector('span[role="text"]') ||
          seg.querySelector('.yt-core-attributed-string') ||
          seg.querySelector('span');

        const text = textNode ? textNode.textContent.trim() : '';
        return { text };
      }).filter(item => item.text);
    }

    const legacySegments = [...root.querySelectorAll('ytd-transcript-segment-renderer, .cue-group')];
    if (legacySegments.length) {
      return legacySegments.map(seg => {
        const text =
          seg.querySelector('.segment-text')?.textContent?.trim() ||
          seg.querySelector('.cue')?.textContent?.trim() ||
          '';

        return { text };
      }).filter(item => item.text);
    }

    return [];
  }

  function getTranscriptSegments() {
    const root = getTranscriptRoot();
    return getTranscriptSegmentsFromRoot(root);
  }

  async function clickShowTranscriptButton() {
    const buttons = [...document.querySelectorAll('button, yt-button-shape button, tp-yt-paper-button')];

    const btn = buttons.find(el => {
      const text = (el.innerText || el.textContent || '').trim().toLowerCase();
      const aria = (el.getAttribute('aria-label') || '').trim().toLowerCase();
      return (
        text.includes('show transcript') ||
        text.includes('transkript anzeigen') ||
        aria.includes('show transcript') ||
        aria.includes('transkript anzeigen')
      );
    });

    if (btn) {
      btn.click();
      log('Show Transcript geklickt');
      await sleep(2000);
      return true;
    }

    return false;
  }

  async function ensureTranscriptOpen() {
    let segments = getTranscriptSegments();
    if (segments.length) return true;

    await clickShowTranscriptButton();
    await sleep(1500);

    segments = getTranscriptSegments();
    return segments.length > 0;
  }

  async function scrollTranscriptToLoadAll() {
    const root = getTranscriptRoot();
    if (!root) return;

    const scrollBox =
      root.closest('.ytSectionListRendererContents') ||
      root.querySelector('.ytSectionListRendererContents') ||
      root;

    let lastCount = -1;
    let stableRounds = 0;

    for (let i = 0; i < 80; i++) {
      scrollBox.scrollTop = scrollBox.scrollHeight;
      await sleep(300);

      const count = getTranscriptSegments().length;
      log('Segmente nach Scroll:', count);

      if (count === lastCount) {
        stableRounds++;
      } else {
        stableRounds = 0;
      }

      lastCount = count;

      if (stableRounds >= 4) break;
    }

    scrollBox.scrollTop = 0;
    await sleep(200);
  }

  function buildTranscriptText(segments) {
    return segments
      .map(({ text }) => text)
      .filter(Boolean)
      .join('\n');
  }

  async function copyText(text) {
    try {
      if (typeof GM_setClipboard === 'function') {
        GM_setClipboard(text, 'text');
        return true;
      }

      await navigator.clipboard.writeText(text);
      return true;
    } catch (err) {
      console.error(err);

      const ta = document.createElement('textarea');
      ta.value = text;
      ta.style.position = 'fixed';
      ta.style.left = '-9999px';
      document.body.appendChild(ta);
      ta.focus();
      ta.select();
      const ok = document.execCommand('copy');
      ta.remove();
      return ok;
    }
  }

  async function extractTranscript() {
    const ok = await ensureTranscriptOpen();

    if (!ok) {
      showToast('Transcript konnte nicht gefunden oder geöffnet werden.', 'error');
      return;
    }

    const btn = document.getElementById('yt-transcript-copy-btn-fixed');
    if (btn) {
      btn.disabled = true;
      btn.textContent = 'Kopiere...';
      btn.style.opacity = '0.7';
    }

    await scrollTranscriptToLoadAll();

    const segments = getTranscriptSegments();

    if (!segments.length) {
      console.log('Root:', getTranscriptRoot());
      console.log('Panels:', getTranscriptPanels());

      if (btn) {
        btn.disabled = false;
        btn.textContent = 'Transcript kopieren';
        btn.style.opacity = '1';
      }

      showToast('Keine Transcript-Segmente gefunden.', 'error');
      return;
    }

    const text = buildTranscriptText(segments);
    const copied = await copyText(text);

    console.log(text);

    if (btn) {
      btn.disabled = false;
      btn.textContent = 'Transcript kopieren';
      btn.style.opacity = '1';
    }

    if (copied) {
      showToast(`Transcript kopiert: ${segments.length} Segmente.`);
    } else {
      showToast(`Transcript gefunden (${segments.length} Segmente), aber Kopieren fehlgeschlagen.`, 'error');
    }
  }

  function styleButton(btn) {
    btn.style.display = 'inline-flex';
    btn.style.alignItems = 'center';
    btn.style.justifyContent = 'center';
    btn.style.height = '36px';
    btn.style.padding = '0 16px';
    btn.style.marginLeft = '10px';
    btn.style.border = 'none';
    btn.style.borderRadius = '18px';
    btn.style.background = '#f2f2f2';
    btn.style.color = '#0f0f0f';
    btn.style.fontSize = '14px';
    btn.style.fontWeight = '500';
    btn.style.lineHeight = '36px';
    btn.style.cursor = 'pointer';
    btn.style.whiteSpace = 'nowrap';
    btn.style.boxShadow = 'none';
    btn.style.fontFamily = '"Roboto","Arial",sans-serif';
    btn.style.transition = 'background 0.2s ease, opacity 0.2s ease';
    btn.style.flex = '0 0 auto';
    btn.style.verticalAlign = 'middle';
  }

  function addHoverEvents(btn) {
    btn.addEventListener('mouseenter', () => {
      if (!btn.disabled) btn.style.background = '#e5e5e5';
    });

    btn.addEventListener('mouseleave', () => {
      btn.style.background = '#f2f2f2';
    });
  }

  function findCreateButton() {
    const candidates = [...document.querySelectorAll('button, yt-button-shape button, tp-yt-paper-button')];

    return candidates.find(el => {
      const text = (el.innerText || el.textContent || '').trim().toLowerCase();
      const aria = (el.getAttribute('aria-label') || '').trim().toLowerCase();
      return text === 'create' || aria === 'create' || text.includes('create');
    }) || null;
  }

  function placeButtonNextToCreate(btn, createBtn) {
    const reference =
      createBtn.closest('yt-button-view-model') ||
      createBtn.closest('yt-button-shape') ||
      createBtn.parentElement;

    if (!reference || !reference.parentElement) return false;

    const parent = reference.parentElement;

    if (window.getComputedStyle(parent).display.includes('flex')) {
      btn.style.position = 'relative';
      btn.style.top = '0';
      btn.style.right = '0';
      btn.style.marginLeft = '10px';
      btn.style.marginTop = '0';
      btn.style.zIndex = '1';

      if (reference.nextSibling !== btn) {
        reference.insertAdjacentElement('afterend', btn);
      }
      return true;
    }

    return false;
  }

  function createButton() {
    let btn = document.getElementById('yt-transcript-copy-btn-fixed');

    if (!btn) {
      btn = document.createElement('button');
      btn.id = 'yt-transcript-copy-btn-fixed';
      btn.type = 'button';
      btn.textContent = 'Transcript kopieren';
      styleButton(btn);
      addHoverEvents(btn);
      btn.addEventListener('click', extractTranscript);
    }

    const createBtn = findCreateButton();

    if (createBtn && placeButtonNextToCreate(btn, createBtn)) {
      return;
    }

    if (!document.body.contains(btn)) {
      btn.style.position = 'fixed';
      btn.style.top = '20px';
      btn.style.right = '20px';
      btn.style.marginLeft = '0';
      btn.style.zIndex = '999999';
      document.body.appendChild(btn);
    }
  }

  function init() {
    createButton();
  }

  const observer = new MutationObserver(() => {
    createButton();
  });

  window.addEventListener('load', () => {
    init();
    observer.observe(document.body, { childList: true, subtree: true });
  });
})();