YouTube Transcript

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

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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