YouTube Transcript

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

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

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