YouTube Transcript

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

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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