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 });
  });
})();