YouTube Embed Video Description

Adds a button to the YouTube player to display the video description without conflicts, with a reliable download, Plain Text formatting, Close the button

// ==UserScript==
// @name         YouTube Embed Video Description
// @namespace    GPT
// @version      1.0.8
// @description  Adds a button to the YouTube player to display the video description without conflicts, with a reliable download, Plain Text formatting, Close the button
// @description:ru  Добавляет кнопку в плеер YouTube для отображения описания видео без конфликтов, с надёжной загрузкой, plain text форматирование, с кнопкой закрыть
// @author       Wizzergod
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @match        *://www.youtube.com/embed/*
// @match        *://www.youtube.com/watch?v=*
// @grant        GM_addStyle
// @grant        GM_getResourceURL
// @grant        GM_xmlhttpRequest
// @resource     icon https://images.icon-icons.com/3361/PNG/512/communication_information_aid_disclaimer_customer_service_about_guide_help_info_icon_210836.png
// @run-at       document-body
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  const lang = navigator.language.startsWith('ru') ? 'ru' : 'en';
  const i18n = {
    ru: {
      title: 'Показать описание видео',
      loading: 'Загрузка описания...',
      notFound: 'Видео не найдено.',
      error: 'Ошибка загрузки описания.',
      close: '✖',
    },
    en: {
      title: 'Show video description',
      loading: 'Loading description...',
      notFound: 'Video not found.',
      error: 'Error loading description.',
      close: '✖',
    }
  };

  const t = i18n[lang];

  const iconURL = GM_getResourceURL('icon');

  GM_addStyle(`
    #wizzergod-yt-desc-btn {
      background-image: url(${iconURL});
      background-size: 26px 26px;
      background-repeat: no-repeat;
      background-position: center;
      width: 36px;
      height: 36px;
      cursor: pointer;
      border: none;
      background-color: parent;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      ition: form 0.3s ease;
      z-index: 99999;
      margin: 0 8px;
    }
    #wizzergod-yt-desc-btn:hover {
      form: scale(1.15);
    }
    #wizzergod-yt-desc-btn:active {
      form: scale(0.9);
      background-color: rgba(255, 255, 255, 0.1);
    }

    #wizzergod-yt-desc-modal {
      position: fixed;
      top: 15vh;
      left: 10vw;
      width: 80vw;
      height: 50vh;
      background: rgba(17,17,17,0.95);
      color: white;
      padding: 20px;
      border-radius: 12px;
      z-index: 2147483647;
      display: none;
      flex-direction: column;
      resize: both;
      overflow: auto;
      box-shadow: 0 0 20px rgba(0,0,0,0.8);
      font-family: Arial, sans-serif;
      backdrop-filter: blur(10px);
      border: 1px solid rgba(255, 255, 255, 0.1);
    }

    #wizzergod-yt-desc-modal.show {
      display: flex;
    }
    #wizzergod-yt-desc-modal .close-btn {
      align-self: flex-end;
      font-size: 24px;
      cursor: pointer;
      padding: 5px 10px;
      user-select: none;
    }
    #wizzergod-yt-desc-modal .content {
      white-space: pre-wrap;
      word-break: break-word;
      font-size: 14px;
      line-height: 1.4;
      flex-grow: 1;
      overflow-y: auto;
      text-align: center;
      margin-top: 10px;
    }
  `);

  function createButton() {
    const btn = document.createElement('button');
    btn.id = 'wizzergod-yt-desc-btn';
    btn.className = 'ytp-button';
    btn.title = t.title;
    btn.setAttribute('aria-label', t.title);
    btn.onclick = () => {
      toggleModal();
    };
    return btn;
  }

  function createModal() {
    const modal = document.createElement('div');
    modal.id = 'wizzergod-yt-desc-modal';

    const close = document.createElement('div');
    close.className = 'close-btn';
    close.textContent = t.close;
    close.onclick = () => modal.classList.remove('show');

    const content = document.createElement('div');
    content.className = 'content';
    content.textContent = t.loading;

    modal.appendChild(close);
    modal.appendChild(content);
    document.body.appendChild(modal);

    return modal;
  }

  async function fetchDescription(videoId, modal) {
    try {
      const res = await fetch(`https://www.youtube.com/watch?v=${videoId}`, { cache: 'no-store' });
      const text = await res.text();
      const match = text.match(/"shortDescription":"(.*?)"/);
      const desc = match ? JSON.parse('"' + match[1] + '"') : t.notFound;

      const content = modal.querySelector('.content');
      content.textContent = desc;
    } catch (e) {
      const content = modal.querySelector('.content');
      content.textContent = t.error;
    }
  }

  function toggleModal() {
    let modal = document.getElementById('wizzergod-yt-desc-modal');
    if (!modal) modal = createModal();
    modal.classList.add('show');

    const content = modal.querySelector('.content');
    content.textContent = t.loading;

    const videoId = location.href.match(/embed\/([^?&]+)/)?.[1] || location.href.match(/[?&]v=([^?&]+)/)?.[1];
    if (videoId) {
      fetchDescription(videoId, modal);
    } else {
      content.textContent = t.notFound;
    }
  }

  function addButtonWhenReady() {
    const controlBarSelector = '.ytp-right-controls';
    const controls = document.querySelector(controlBarSelector);

    if (!controls) {
      setTimeout(addButtonWhenReady, 300);
      return;
    }

    if (document.getElementById('wizzergod-yt-desc-btn')) return;

    const btn = createButton();
    controls.insertBefore(btn, controls.firstChild);
  }

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

  observer.observe(document.body, {
    childList: true,
    subtree: true,
  });

  addButtonWhenReady();
})();