YouTube Downloader - Local Server Interface - PRO

The Best YouTube Downloader! Download Video (Full HD/4K/8K), Audio (MP3), Images, Record Livestreams & Screenshots via Local Server. Features: Universal Support, Batch Download, Shortcuts, Quality Selection, Resizable UI.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         YouTube Downloader - Local Server Interface - PRO
// @name:pt-BR   YouTube Downloader - Local Server Interface - PRO
// @name:es      YouTube Downloader - Local Server Interface - PRO
// @name:fr      YouTube Downloader - Local Server Interface - PRO
// @name:de      YouTube Downloader - Local Server Interface - PRO
// @name:it      YouTube Downloader - Local Server Interface - PRO
// @name:ru      YouTube Downloader - Local Server Interface - PRO
// @name:zh-CN   YouTube Downloader - Local Server Interface - PRO
// @name:ja      YouTube Downloader - Local Server Interface - PRO
// @name:ko      YouTube Downloader - Local Server Interface - PRO
// @name:hi      YouTube Downloader - Local Server Interface - PRO
// @name:id      YouTube Downloader - Local Server Interface - PRO
// @namespace    http://tampermonkey.net/
// @version      3.12.6
// @description  The Best YouTube Downloader! Download Video (Full HD/4K/8K), Audio (MP3), Images, Record Livestreams & Screenshots via Local Server. Features: Universal Support, Batch Download, Shortcuts, Quality Selection, Resizable UI.
// @description:pt-BR A melhor ferramenta para baixar YouTube! Baixe Vídeos (Full HD/4K/8K), Áudio (MP3), Imagens, Grave Lives e Captura de Tela via Servidor Local. Recursos: Suporte Universal, Download em Lote, Atalhos, Seleção de Qualidade, UI Redimensionável.
// @description:es   ¡El mejor descargador de YouTube! Descarga Video (Full HD/4K/8K), Audio (MP3), Imágenes, Graba Transmisiones y Capturas de Pantalla a través del Servidor Local. Características: Soporte Universal, Descarga por Lotes, Atajos, Selección de Calidad, UI Redimensionable.
// @description:zh-CN 最好的YouTube下载器!通过本地服务器下载视频(全高清/4K/8K)、音频(MP3)、图片、录制直播和截图。功能:通用支持、批量下载、快捷键、质量选择、可调整大小的UI。
// @description:ru   Лучший загрузчик YouTube! Скачивайте Видео (Full HD/4K/8K), Аудио (MP3), Картинки, Запись Стримов и Скриншоты через локальный сервер. Функции: Универсальная поддержка, Выбор качества, Горячие клавиши, Изменяемый размер UI.
// @description:fr   Le meilleur téléchargeur YouTube ! Téléchargez Vidéo (Full HD/4K/8K), Audio (MP3), Images, Enregistrez Lives et Captures d'Écran via serveur local. Fonctionnalités : Support Universel, Raccourcis clavier, Sélection de Qualité, UI redimensionnable.
// @description:de   Der beste YouTube-Downloader! Video (Full HD/4K/8K), Audio (MP3), Bilder, Live-Streams aufnehmen & Screenshots über lokalen Server herunterladen. Features: Universelle Unterstützung, Tastenkürzel, Qualitätsauswahl, Anpassbare UI.
// @description:ja   最高のYouTubeダウンローダー!ローカルサーバー経由でビデオ(フルHD/4K/8K)、オーディオ(MP3)、画像、ライブ録画、スクリーンショットをダウンロード。機能:ユニバーサルサポート、ショートカット、品質選択、サイズ変更可能なUI。
// @description:it   Il miglior downloader di YouTube! Scarica Video (Full HD/4K/8K), Audio (MP3), Immagini, Registra Live e Screenshot tramite server locale. Funzioni: Supporto Universale, Scorciatoie da tastiera, Selezione Qualità, UI ridimensionabile.
// @description:hi   सर्वश्रेष्ठ यूट्यूब डाउनलोडर! स्थानीय सर्वर के माध्यम से वीडियो (पूर्ण एचडी/4K/8K), ऑडियो (MP3), चित्र, लाइव रिकॉर्डिंग और स्क्रीनशॉट डाउनलोड करें। विशेषताएं: यूनिवर्सल समर्थन, कीबोर्ड शॉर्टकट, गुणवत्ता चयन, आकार बदलने योग्य यूआई।
// @description:id   Pengunduh YouTube Terbaik! Unduh Video (Full HD/4K/8K), Audio (MP3), Gambar, Rekam Langsung & Tangkapan Layar melalui Server Lokal. Fitur: Dukungan Universal, Pintasan Keyboard, Pilihan Kualitas, UI yang Dapat Diubah Ukurannya.
// @description:ko   최고의 YouTube 다운로더! 로컬 서버를 통해 비디오(Full HD/4K/8K), 오디오(MP3), 이미지, 라이브 녹화 및 스크린샷을 다운로드하십시오. 기능: 범용 지원, 단축키, 품질 선택, 크기 조정 가능한 UI.
// @description:ar   أفضل تنزيل يوتيوب! قم بتنزيل الفيديو (Full HD/4K/8K) والصوت (MP3) والصور وتسجيل البث المباشر ولقطات الشاشة عبر الخادم المحلي. الميزات: الدعم العالمي ، اختصارات لوحة المفاتيح ، تحديد الجودة ، واجهة المستخدم القابلة لتغيير الحجم.
// @copyright    2025, Tauã B. Kloch Leite - All Rights Reserved.
// @author       Tauã B. Kloch Leite
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @match        https://www.youtube.com/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_openInTab
// @grant        GM_setClipboard
// @grant        GM_xmlhttpRequest
// @connect      127.0.0.1
// @connect      *
// ==/UserScript==

(function () {
  'use strict';

  if (window.self !== window.top) return;

  let policy = null;
  if (window.trustedTypes && window.trustedTypes.createPolicy) {
      try { policy = window.trustedTypes.createPolicy('yt-dl-policy', { createHTML: (s) => s }); } catch (e) { }
  }
  const safeHTML = (html) => policy ? policy.createHTML(html) : html;

  const SERVER_URL = "http://127.0.0.1:5000";
  const DRIVE_LINK = "https://drive.google.com/file/d/1RFw0VR_cnLpBjJbIajNz0XHESLVfIYG1/view?usp=sharing";
  const UPDATE_URL = "https://greasyfork.org/en/scripts/557579-youtube-downloader-local-server-interface-pro";
  const POLLING_INTERVAL = 1500;

  const ICONS = {
        pix: "https://upload.wikimedia.org/wikipedia/commons/a/a2/Logo%E2%80%94pix_powered_by_Banco_Central_%28Brazil%2C_2020%29.svg",
        paypal: "https://www.paypalobjects.com/webstatic/icon/pp258.png",
        btc: "https://cryptologos.cc/logos/bitcoin-btc-logo.svg?v=025",
        eth: "https://cryptologos.cc/logos/ethereum-eth-logo.svg?v=025",
        sol: "https://cryptologos.cc/logos/solana-sol-logo.svg?v=025",
        usdt: "https://cryptologos.cc/logos/tether-usdt-logo.svg?v=025",
        bubble: "https://www.google.com/s2/favicons?sz=64&domain=youtube.com",
        warn: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Antu_dialog-warning.svg/200px-Antu_dialog-warning.svg.png"
  };

  const EN_BASE = {
        title: "Local Downloader PRO", tab_dl: "Downloads", tab_batch: "Batch List", tab_sup: "Donate", tab_help: "Help",
        vid: "🎬 Video", aud: "🎵 Audio", img: "🖼️ Image", queue: "Queue", done: "Done", err: "Error", refresh: "🔄 Refresh", clear: "🗑️ Clear",
        conn_err: "Server Offline? Start the App!", open: "Open", folder: "Folder", sup_title: "SUPPORT THE CODE", sup_desc: "Help keep updates coming!",
        lbl_pix: "PIX KEY (BR)", btn_copy: "COPY", auto_dl: "⬇️ Saved: ", wallet_title: "CRYPTO WALLETS", login_err: "⚠️ LOGIN NEEDED",
        retry: "Retry", cancel: "Cancel", open_panel: "🚀 Open Server Panel", toggle: "👁️ Show/Hide UI",
        help_btn: "❓ Help / Install", back: "Back to Panel",
        batch_ph: "Paste links here (one per line)...", batch_btn: "PROCESS LIST", batch_sent: "Links sent: ",
        sc_vid: "SHIFT + Right Click", sc_aud: "ALT + Right Click", sc_img: "CTRL + Right Click",
        pro_tip: "💡 PRO TIP: No need to open the video! Hold the shortcut key and use Right Click directly on the thumbnail.",
        err_old_ver: "⚠️ Requires New Universal Server!",
        help_login_err: "Login Error? Click the yellow warning.",
        footer_msg: "Tauã B. Kloch Leite - All Rights Reserved 2025",
        help_title: "INSTALLATION REQUIRED",
        help_s1: "1. Download Universal_Downloader.exe", help_s2: "2. Open the App", help_s3: "3. Click 'Start Server'",
        help_btn_dl: "DOWNLOAD SERVER", help_warn: "The script needs this app!",
        univ_note: "NOTE: The new server is UNIVERSAL. Update now!",
        menu_toggle: "👁️ Show/Hide UI (Alt+Shift+Y)", menu_help: "❓ Help / Shortcuts", menu_panel: "⚙️ Open Panel", menu_dl: "📥 Download Server",
        menu_update: "🔄 Check Update", btn_panel: "Panel",
        lbl_qual: "Quality:", q_best: "💎 Best (4K/8K)", q_1080: "📺 Full HD (1080p)", q_720: "HD (720p)", q_480: "SD (480p)",
        rec: "🔴 Rec Clip", rec_stop: "⏹️ Stop Rec", rec_err: "Rec Error",
        ss_btn: "📸 Frame", ss_ok: "✅ Frame Captured!",
        sc_rec: "ALT + R", sc_ss: "ALT + S",
        server_update_warn: "⚠️ ATTENTION: Update to Server v6.9.1 to fix Asian/Symbol filenames!",
        banner_msg: "NEW SERVER v6.9.1! Fixed file names (JP/CN/Symbols). Update now!",
        banner_btn: "UPDATE"
  };

  const STRINGS = {
    en: EN_BASE,
    pt: {
        ...EN_BASE,
        title: "Downloader Local PRO", tab_dl: "Downloads", tab_batch: "Lista Batch", tab_sup: "Doação", tab_help: "Ajuda",
        vid: "🎬 Vídeo", aud: "🎵 Áudio", img: "🖼️ Imagem", queue: "Fila", done: "Prontos", err: "Erros", refresh: "🔄 Atualizar", clear: "🗑️ Limpar",
        conn_err: "Servidor Offline? Inicie o App!", open: "Abrir", folder: "Pasta", sup_title: "APOIE O PROJETO", sup_desc: "Mantenha as atualizações vivas!",
        lbl_pix: "CHAVE PIX", btn_copy: "COPIAR", auto_dl: "⬇️ Salvo: ", wallet_title: "CARTEIRAS CRIPTO", login_err: "⚠️ LOGIN NECESSÁRIO",
        retry: "🔄 Reiniciar", cancel: "❌ Cancelar", open_panel: "🚀 Abrir Painel Server", toggle: "👁️ Mostrar/Ocultar UI",
        help_btn: "❓ Ajuda / Instalação", back: "Voltar para o Painel",
        batch_ph: "Cole os links aqui (um por linha)...", batch_btn: "PROCESSAR LISTA", batch_sent: "Links enviados: ",
        sc_vid: "SHIFT + Clique Direito", sc_aud: "ALT + Clique Direito", sc_img: "CTRL + Clique Direito",
        pro_tip: "💡 DICA PRO: Não precisa abrir o vídeo! Segure a tecla de atalho e use o Clique Direito na miniatura.",
        err_old_ver: "⚠️ Requer Novo Servidor Universal!",
        help_login_err: "Erro de Login? Clique no aviso amarelo.",
        footer_msg: "Tauã B. Kloch Leite - All Rights Reserved 2025",
        help_title: "INSTALAÇÃO NECESSÁRIA",
        help_s1: "1. Baixe o Universal_Downloader.exe", help_s2: "2. Abra o Aplicativo", help_s3: "3. Clique em 'Start Server'",
        help_btn_dl: "BAIXAR SERVIDOR", help_warn: "O script precisa disso!",
        univ_note: "NOTA: O novo servidor é UNIVERSAL. Atualize!",
        menu_toggle: "👁️ Mostrar/Ocultar UI (Alt+Shift+Y)", menu_help: "❓ Ajuda / Atalhos", menu_panel: "⚙️ Abrir Painel", menu_dl: "📥 Baixar Servidor",
        menu_update: "🔄 Verificar Atualização", btn_panel: "Painel",
        lbl_qual: "Qualidade:", q_best: "💎 Melhor (4K/8K)", q_1080: "📺 Full HD (1080p)", q_720: "HD (720p)", q_480: "SD (480p)",
        rec: "🔴 Gravar Clip", rec_stop: "⏹️ Parar Gravação", rec_err: "Erro ao Gravar",
        ss_btn: "📸 Capturar Frame", ss_ok: "✅ Frame Salvo!",
        sc_rec: "ALT + R", sc_ss: "ALT + S",
        server_update_warn: "⚠️ ATENÇÃO: Atualize para o Servidor v6.9.1 para corrigir nomes (JP/CN/Símbolos)!",
        banner_msg: "NOVO SERVIDOR v6.9.1! Correção de nomes de arquivos (JP/CN). Atualize!",
        banner_btn: "ATUALIZAR"
    }
  };

  const getLang = () => {
      const l = navigator.language || "en";
      const code = l.split('-')[0];
      if (STRINGS[l]) return { ...EN_BASE, ...STRINGS[l] };
      if (STRINGS[code]) return { ...EN_BASE, ...STRINGS[code] };
      return EN_BASE;
  };
  const T = getLang();

  const state = { uiMode: GM_getValue("yt_dl_uiMode", 1), stats: {}, items: [], activeTab: 'dl' };
  const imgCache = {};
  let lastHtml = '';
  let isServerOnline = false;
  let isProcessingClick = false;

  let mediaRecorder = null;
  let recordedChunks = [];
  let isRecording = false;

  let bubblePos = { left: '20px', bottom: '20px', top: 'auto', right: 'auto' };
  let panelPos = null;

  const setUIMode = (m) => {
      if (container) {
          if (state.uiMode === 1) {
              bubblePos = { left: container.style.left, top: container.style.top, bottom: container.style.bottom, right: container.style.right };
          } else if (state.uiMode === 2) {
              panelPos = { left: container.style.left, top: container.style.top, width: container.style.width, height: container.style.height };
          }
      }

      state.uiMode = m;
      GM_setValue("yt_dl_uiMode", m);
      renderUI();

      if (!container) return;

      if (m === 1) {
          container.style.width = '';
          container.style.height = '';
          container.style.resize = 'none';
          applyStyles(container, bubblePos);
      } else if (m === 2) {
          container.style.resize = 'both';
          if (panelPos) {
              applyStyles(container, { ...panelPos, bottom: 'auto', right: 'auto' });
              if(panelPos.width) container.style.width = panelPos.width;
              if(panelPos.height) container.style.height = panelPos.height;
          } else {
              const defW = 360;
              const defH = 500;
              const left = Math.max(0, (window.innerWidth / 2) - (defW / 2));
              const top = Math.max(0, (window.innerHeight / 2) - (defH / 2));

              container.style.position = 'fixed';
              container.style.left = left + 'px';
              container.style.top = top + 'px';
              container.style.width = defW + 'px';
              container.style.height = defH + 'px';
              container.style.bottom = 'auto';
              container.style.right = 'auto';
          }
      }
  };

  const applyStyles = (el, styles) => {
      if(styles.left) el.style.left = styles.left;
      if(styles.top) el.style.top = styles.top;
      if(styles.bottom) el.style.bottom = styles.bottom;
      if(styles.right) el.style.right = styles.right;
  };

  const getHistory = () => GM_getValue('yt_dl_history_local', []);
  const addToHistory = (f) => { let h=getHistory(); if(!h.includes(f)){ h.push(f); if(h.length>50)h.shift(); GM_setValue('yt_dl_history_local', h); }};

  const cleanFileName = (name) => {
      if (!name) return "download_unknown";

      return name
          .replace(/[<>:"/\\|?*]/g, "_")
          .replace(/[\x00-\x1F]/g, "")
          .replace(/^\.+|\.+$/g, "")
          .trim();
  };

  const generateRandomId = () => Math.floor(Math.random() * 900000) + 100000;

  const getYoutubeVideoID = (url) => {
      try {
          const u = new URL(url);
          if (u.hostname.includes('youtube.com')) {
              if (u.pathname.startsWith('/shorts/')) return u.pathname.split('/')[2];
              return u.searchParams.get('v');
          }
          if (u.hostname.includes('youtu.be')) return u.pathname.slice(1);
      } catch(e){}
      return null;
  };

  const gmFetch = (url, options = {}) => {
      return new Promise((resolve, reject) => {
          GM_xmlhttpRequest({
              method: options.method || "GET",
              url: url,
              headers: options.headers || {},
              data: options.body,
              timeout: options.customTimeout || 2000,
              responseType: options.responseType || null,
              onload: (res) => {
                  if (!res.status || res.status === 0) return reject("OFFLINE");
                  try {
                      if(options.responseType === 'arraybuffer' || options.responseType === 'blob') {
                            resolve(res.response);
                      } else {
                          resolve({ json: () => JSON.parse(res.responseText), ok: true, status: res.status });
                      }
                  } catch (e) { reject(e); }
              },
              onerror: () => reject("OFFLINE"),
              ontimeout: () => reject("OFFLINE")
          });
      });
  };

  const bufferToBase64 = (buffer) => {
      let binary = '';
      const bytes = new Uint8Array(buffer);
      const len = bytes.byteLength;
      for (let i = 0; i < len; i++) binary += String.fromCharCode(bytes[i]);
      return window.btoa(binary);
  };

  const tunnelUniversalImage = (imgElement, path, id) => {
      if (imgCache[id]) { imgElement.src = imgCache[id]; return; }
      let url = path.startsWith('/') ? `${SERVER_URL}${path}` : path;
      gmFetch(url, { responseType: 'arraybuffer', customTimeout: 5000 }).then(buffer => {
          const base64 = bufferToBase64(buffer);
          let mime = 'image/jpeg';
          if(path.toLowerCase().endsWith('.png')) mime = 'image/png';
          if(path.toLowerCase().endsWith('.webp')) mime = 'image/webp';
          const dataUri = `data:${mime};base64,${base64}`;
          imgCache[id] = dataUri;
          imgElement.src = dataUri;
      }).catch(() => { imgElement.src = ""; });
  };

  const startRecording = () => {
      const video = document.querySelector('video');
      if (!video) { toast(T.rec_err, false); return; }
      try {
          const stream = video.captureStream();
          mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
          recordedChunks = [];
          mediaRecorder.ondataavailable = (event) => { if (event.data.size > 0) recordedChunks.push(event.data); };
          mediaRecorder.onstop = () => {
              const blob = new Blob(recordedChunks, { type: 'video/webm' });
              const url = URL.createObjectURL(blob);
              const a = document.createElement('a');
              a.href = url;
              a.download = `yt_rec_${new Date().getTime()}.webm`;
              a.click();
              window.URL.revokeObjectURL(url);
              updateRecButton(false);
          };
          mediaRecorder.start();
          isRecording = true;
          updateRecButton(true);
      } catch (e) {
          toast("Error: " + e.message, false);
      }
  };

  const stopRecording = () => { if (mediaRecorder && isRecording) { mediaRecorder.stop(); isRecording = false; } };
  const toggleRecording = () => { if (isRecording) stopRecording(); else startRecording(); };

  const updateRecButton = (active) => {
      const btn = document.getElementById('btn-rec');
      if (btn) {
          if (active) {
              btn.textContent = T.rec_stop;
              btn.classList.add('rec-active');
              btn.classList.remove('btn-gray');
          } else {
              btn.textContent = T.rec;
              btn.classList.remove('rec-active');
              btn.classList.add('btn-gray');
          }
      }
  };

  const captureFrame = () => {
      const video = document.querySelector('video');
      if (!video) { toast(T.err, false); return; }
      const canvas = document.createElement('canvas');
      canvas.width = video.videoWidth;
      canvas.height = video.videoHeight;
      const ctx = canvas.getContext('2d');
      ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
      try {
          const dataUrl = canvas.toDataURL('image/png');
          const a = document.createElement('a');
          a.href = dataUrl;
          const title = document.title.replace(' - YouTube', '') || 'snapshot';
          a.download = `${cleanFileName(title)}_${generateRandomId()}.png`;
          document.body.appendChild(a);
          a.click();
          document.body.removeChild(a);
          toast(T.ss_ok);
      } catch (e) {
          toast("Error: " + e.message, false);
      }
  };

  const findMediaUrl = (target, mode) => {
      let foundUrl = null, foundThumb = null, foundTitle = null;
      if (mode === 'image') {
          const container = target.closest('ytd-compact-video-renderer, ytd-grid-video-renderer, ytd-rich-item-renderer, ytd-playlist-panel-video-renderer, ytd-reel-item-renderer');
          if (container) {
              const link = container.querySelector('a#thumbnail, a[href*="/watch"]');
              const imgEl = container.querySelector('ytd-thumbnail img') || container.querySelector('img');
              const titleEl = container.querySelector('#video-title');
              if (imgEl && imgEl.src) { foundUrl = imgEl.src.split('?')[0]; foundThumb = foundUrl; }
              else if (link) { const deepImg = link.querySelector('img'); if(deepImg) { foundUrl = deepImg.src.split('?')[0]; foundThumb = foundUrl; } }
              if (titleEl) { foundTitle = titleEl.textContent.trim() || titleEl.title; }
              if (foundUrl) {
                  if (!foundTitle) foundTitle = "Image_Sidebar";
                  const uniqueTitle = `${cleanFileName(foundTitle)}_${generateRandomId()}`;
                  return { url: foundUrl, thumb: foundThumb, title: uniqueTitle };
              }
          }
      }
      if (!foundUrl) {
          const link = target.closest('a[href*="/watch"], a[href*="/shorts/"]');
          if (link) {
              foundUrl = link.href;
              const vidId = getYoutubeVideoID(foundUrl);
              if (vidId) foundThumb = `https://i.ytimg.com/vi/${vidId}/hqdefault.jpg`;
              const container = target.closest('ytd-compact-video-renderer') || target.closest('ytd-video-renderer') || target.closest('ytd-rich-item-renderer');
              if (container) { const titleEl = container.querySelector('#video-title'); if (titleEl) foundTitle = titleEl.textContent.trim(); }
          }
      }
      if (!foundUrl) { foundUrl = window.location.href; foundTitle = document.title.replace(" - YouTube", ""); }
      if (foundUrl && !foundThumb && (window.location.pathname.startsWith('/watch') || window.location.pathname.startsWith('/shorts/'))) {
            const vidId = getYoutubeVideoID(foundUrl);
            if(vidId) foundThumb = `https://i.ytimg.com/vi/${vidId}/maxresdefault.jpg`;
      }
      if (!foundTitle) foundTitle = "Media";
      return { url: foundUrl, thumb: foundThumb, title: `${cleanFileName(foundTitle)}_${generateRandomId()}` };
  };

  const handleShortcut = (e, type) => {
      e.preventDefault();
      const media = findMediaUrl(e.target, type);
      if(media.url) send(type, media); else toast("Media Not Found", false);
  };

  document.addEventListener('contextmenu', (e) => {
      if (e.shiftKey) handleShortcut(e, 'video');
      if (e.altKey) handleShortcut(e, 'audio');
      if (e.ctrlKey) handleShortcut(e, 'image');
  });

  let isDraggingUI = false;
  const makeDraggable = (el) => {
      let startX, startY, initialLeft, initialTop;
      const onMouseDown = (e) => {
          if (state.uiMode === 2 && !e.target.closest('.yt-dl-head') && !e.target.closest('.yt-dl-footer')) return;
          if (state.uiMode === 1 && !e.target.closest('.yt-dl-bubble')) return;
          if (state.uiMode === 2) { const rect = el.getBoundingClientRect(); if (e.clientX > rect.right - 20 && e.clientY > rect.bottom - 20) return; }
          isDraggingUI = true; el.dataset.moved = "false"; startX = e.clientX; startY = e.clientY;
          const rect = el.getBoundingClientRect(); initialLeft = rect.left; initialTop = rect.top;
          el.style.bottom = 'auto'; el.style.right = 'auto'; el.style.left = initialLeft + 'px'; el.style.top = initialTop + 'px';
          e.preventDefault();
      };
      const onMouseMove = (e) => {
          if (!isDraggingUI) return;
          const dx = e.clientX - startX; const dy = e.clientY - startY;
          if (Math.abs(dx) > 3 || Math.abs(dy) > 3) {
              el.dataset.moved = "true"; el.style.left = (initialLeft + dx) + 'px'; el.style.top = (initialTop + dy) + 'px';
          }
      };
      const onMouseUp = () => {
          if (isDraggingUI) {
              isDraggingUI = false;
              if (state.uiMode === 1) bubblePos = { left: el.style.left, top: el.style.top, bottom: 'auto', right: 'auto' };
              else panelPos = { left: el.style.left, top: el.style.top, width: el.style.width, height: el.style.height };
          }
      };
      el.addEventListener('mousedown', onMouseDown); window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp);
  };

  const clearList = async () => { try { await gmFetch(`${SERVER_URL}/clear`, { method: 'POST', customTimeout: 1000 }); } catch(e){ } GM_setValue('yt_dl_history_local', []); state.items = []; state.stats = { total:0, in_progress:0, finished:0, errors:0 }; lastHtml = ''; updateListContent(); };
  const openLocalFile = async (filename) => { try { await gmFetch(`${SERVER_URL}/open_file`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({filename: filename}), customTimeout: 1000 }); } catch(e) { if(e === "OFFLINE") toast(T.conn_err, false); } };
  const openFolder = async (type) => { try { await gmFetch(`${SERVER_URL}/open_folder`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({type: type}), customTimeout: 1000 }); } catch(e) { if(e === "OFFLINE") toast(T.conn_err, false); } };
  const copyToClipboard = (text) => { GM_setClipboard(text); toast(T.btn_copy + " OK!"); };
  const cancelDownload = async (id) => { try { await gmFetch(`${SERVER_URL}/cancel`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({id: id}), customTimeout: 1000 }); toast(T.cancel + " OK"); refreshData(); } catch(e) { } };

  const updateButtonState = () => {
      if(!container || state.uiMode !== 2) return;
      const path = window.location.pathname;
      const isVideoPage = path.startsWith('/watch') || path.startsWith('/shorts/');
      const hasVideoElement = !!document.querySelector('video');

      ['btn-vid', 'btn-aud', 'btn-img'].forEach(id => {
          const btn = document.getElementById(id);
          if(btn) {
              btn.disabled = !isVideoPage;
              if (btn.disabled) {
                  btn.style.opacity = "0.4";
                  btn.style.filter = "grayscale(100%)";
                  btn.style.cursor = "not-allowed";
              } else {
                  btn.style.opacity = "1";
                  btn.style.filter = "none";
                  btn.style.cursor = "pointer";
              }
          }
      });

      const btnRec = document.getElementById('btn-rec');
      if (btnRec && !isRecording) {
           btnRec.disabled = !(isVideoPage && hasVideoElement);
           if (btnRec.disabled) {
               btnRec.style.opacity = "0.4";
               btnRec.style.filter = "grayscale(100%)";
               btnRec.style.cursor = "not-allowed";
           } else {
               btnRec.style.opacity = "1";
               btnRec.style.filter = "none";
               btnRec.style.cursor = "pointer";
           }
      }

      const btnSs = document.getElementById('btn-ss');
      if (btnSs) {
           btnSs.disabled = !(isVideoPage && hasVideoElement);
           if (btnSs.disabled) {
               btnSs.style.opacity = "0.4";
               btnSs.style.filter = "grayscale(100%)";
               btnSs.style.cursor = "not-allowed";
           } else {
               btnSs.style.opacity = "1";
               btnSs.style.filter = "none";
               btnSs.style.cursor = "pointer";
           }
      }
  };

  const processBatch = () => {
      const area = document.getElementById('yt-dl-batch-area'); if(!area) return;
      const lines = area.value.split('\n'); let count = 0;
      lines.forEach(line => { const url = line.trim(); if(url.startsWith('http')) { send('video', { url: url, thumb: null, title: `Batch_${generateRandomId()}` }); count++; } });
      area.value = ''; lastHtml = ''; toast(`${T.batch_sent}${count}`); state.activeTab = 'dl'; renderUI();
  };

  const refreshData = async () => {
      updateButtonState();
      try {
          const [sRes, fRes] = await Promise.all([ gmFetch(`${SERVER_URL}/stats`, { customTimeout: 1000 }), gmFetch(`${SERVER_URL}/files`, { customTimeout: 1000 }) ]);
          isServerOnline = true;
          state.stats = await sRes.json();
          const files = await fRes.json();
          state.items = files.items || [];
          state.items.forEach(i => { if(i.status === 'finished' && i.filename && !getHistory().includes(i.filename)) { addToHistory(i.filename); toast(T.auto_dl + i.title.substring(0,20)+"..."); } });
          if(state.uiMode === 2) updateListContent();
      } catch (e) { isServerOnline = false; }
  };

  const send = async (type, mediaData) => {
      if (!isServerOnline) { toast(T.conn_err, false); gmFetch(`${SERVER_URL}/stats`, { customTimeout: 500 }).then(()=> isServerOnline=true).catch(()=>{}); return; }
      if (isProcessingClick) return;
      isProcessingClick = true; setTimeout(() => isProcessingClick = false, 500);

      try {
          let finalUrl, thumbUrl, title;
          if (typeof mediaData === 'object' && mediaData.url) { finalUrl = mediaData.url; thumbUrl = mediaData.thumb; title = mediaData.title; }
          else { finalUrl = location.href; const extracted = findMediaUrl(document.body, type); thumbUrl = extracted.thumb; title = extracted.title; }

          let endpoint = 'download';
          if (type === 'audio') endpoint = 'download_audio';
          if (type === 'image') endpoint = 'download_image';

          const quality = document.getElementById('yt-dl-quality') ? document.getElementById('yt-dl-quality').value : 'best';

          const cleanTitle = cleanFileName(title);
          console.log("[YT-DL] Sending Title:", cleanTitle);

          const response = await gmFetch(`${SERVER_URL}/${endpoint}`, {
              method: 'POST', headers: {'Content-Type': 'application/json'},
              body: JSON.stringify({ videoUrl: finalUrl, thumb: thumbUrl, type: type, title: cleanTitle, quality: quality }),
              customTimeout: 2500
          });

          if (!response.ok) { if (type === 'image') throw new Error("OLD_SERVER"); throw new Error("Generic Error"); }
          lastHtml = ''; refreshData(); toast(`${type.toUpperCase()} OK 🚀`);
          if(state.uiMode === 1) setUIMode(2);
      } catch(e) {
          if (e === "OFFLINE") { toast(T.conn_err, false); isServerOnline = false; }
          else if (e.message === "OLD_SERVER") { toast(T.err_old_ver, false); }
          else { toast(T.conn_err, false); }
      }
  };

  const css = `
    .yt-dl-container { font-family: 'Roboto', sans-serif; z-index: 2147483647; position: fixed; bottom: 20px; left: 20px; }
    @media (max-width: 768px) { .yt-dl-panel { width: 90% !important; left: 5% !important; bottom: 10px !important; } }
    .yt-dl-bubble { width: 60px; height: 60px; background: transparent; border: none; cursor: move; display: flex; align-items: center; justify-content: center; transition: transform 0.2s; }
    .yt-dl-bubble img { width: 100%; height: 100%; filter: drop-shadow(0 4px 8px rgba(0,0,0,0.6)); }
    .yt-dl-bubble:hover { transform: scale(1.1); }

    .yt-dl-panel { width: 360px; height: 500px; min-width: 320px; min-height: 300px; max-width: 95vw; max-height: 80vh; resize: both; overflow: hidden; display: flex; flex-direction: column; background: #0f0f0f; color: #fff; border-radius: 12px; border: 1px solid #333; font-size: 12px; box-shadow: 0 10px 40px rgba(0,0,0,0.9); animation: slideUp 0.3s ease-out; }
    @keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
    .yt-dl-head { background: #1a1a1a; padding: 10px 15px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #333; cursor: move; flex-shrink: 0; }
    .yt-dl-min-btn { cursor: pointer; font-size: 18px; color: #aaa; padding: 0 5px; } .yt-dl-min-btn:hover { color: #fff; }
    .progress-bg { width: 100%; height: 4px; background: #333; margin-top: 4px; border-radius: 2px; overflow: hidden; }
    .progress-fill { height: 100%; background: #4caf50; width: 0%; transition: width 0.3s ease; }
    .prog-text { font-size: 9px; color: #888; text-align: right; margin-top: 2px; }
    .yt-dl-tabs { display: flex; background: #111; flex-shrink: 0; }
    .yt-dl-tab { flex: 1; text-align: center; padding: 10px 0; cursor: pointer; color: #aaa; border-bottom: 2px solid transparent; font-weight: 700; text-transform: uppercase; font-size: 10px; }
    .yt-dl-tab.active { color: #fff; border-bottom: 2px solid #d63384; background: #222; }
    .yt-dl-body { flex: 1; overflow-y: auto; padding: 15px; }
    .yt-dl-footer { text-align: center; font-size: 9px; color: #555; border-top: 1px solid #222; padding: 8px 0; flex-shrink: 0; background: #0f0f0f; cursor: move; }
    .yt-dl-btn-group { display: flex; gap: 8px; margin-bottom: 5px; }
    .yt-dl-btn { flex: 1; border: none; padding: 10px; border-radius: 6px; cursor: pointer; color: #fff; font-weight: 700; font-size: 13px; display: flex; align-items: center; justify-content: center; gap: 5px; transition: 0.2s; }
    .yt-dl-btn:hover { filter: brightness(1.1); }
    .btn-blue { background: #3ea6ff; color: #000; } .btn-purple { background: #d63384; } .btn-gray { background: #333; border: 1px solid #444; } .btn-red { background: #d32f2f; } .btn-orange { background: #ff9800; color:#000; }
    .yt-dl-item { display: flex; align-items: center; gap: 10px; padding: 10px 0; border-bottom: 1px solid #222; }
    .yt-dl-thumb { width: 50px; height: 50px; background: #000; border-radius: 6px; object-fit: cover; }
    .yt-dl-info { flex: 1; overflow: hidden; }
    .yt-dl-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: 500; font-size: 12px; margin-bottom: 4px; }
    .yt-dl-status { font-size: 10px; display: flex; align-items: center; gap: 6px; }
    .tag-type { padding: 2px 6px; border-radius: 4px; font-weight: bold; font-size: 9px; text-transform: uppercase; }
    .tag-vid { background: #0f3d5c; color: #3ea6ff; border: 1px solid #1e5985; } .tag-aud { background: #3c1f30; color: #ff66b2; border: 1px solid #7d2a58; } .tag-img { background: #3d2b0f; color: #ff9800; border: 1px solid #855a15; }
    .ctrl-btn { background: #333; border: 1px solid #444; color: #ccc; cursor: pointer; font-size: 10px; border-radius: 4px; padding: 3px 8px; margin-left: 5px; }
    .ctrl-btn:hover { background: #555; color: #fff; }
    .btn-retry { color: #4caf50; border-color: #2e7d32; } .btn-cancel { color: #f44336; border-color: #c62828; }
    .sup-row { display: flex; align-items: center; gap: 8px; background: #1a1a1a; padding: 8px; border-radius: 6px; border: 1px solid #333; margin-bottom: 8px; }
    .sup-icon { width: 20px; height: 20px; object-fit: contain; }
    .sup-val { flex: 1; background: none; border: none; color: #eee; font-size: 11px; font-family: monospace; outline: none; }
    .sup-copy { background: #d63384; border: none; color: #fff; border-radius: 4px; cursor: pointer; font-size: 10px; padding: 4px 8px; }
    .auth-fix-btn { cursor: pointer; text-decoration: underline; } .auth-fix-btn:hover { color: #fff !important; }
    .batch-area { width: 100%; height: 100px; background: #0a0a0a; color: #ddd; border: 1px solid #333; padding: 10px; font-size: 11px; box-sizing: border-box; resize: vertical; margin-bottom: 10px; border-radius: 6px; }
    .yt-dl-toast { position: fixed; top: 20px; right: 20px; background: #28a745; color: white; padding: 10px 20px; border-radius: 4px; z-index: 2147483648; font-weight: bold; animation: fadein 0.5s; }
    @keyframes fadein { from { opacity:0; transform:translateY(-10px); } to { opacity:1; transform:translateY(0); } }
    .yt-dl-select { width: 100%; background: #1a1a1a; color: #fff; border: 1px solid #333; border-radius: 6px; padding: 8px; margin-bottom: 8px; font-size: 11px; outline: none; cursor: pointer; }
    .yt-dl-select:hover { border-color: #555; }
    .rec-active { background: #f00 !important; color: #fff !important; animation: pulse 1.5s infinite; }
    @keyframes pulse { 0% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.7); } 70% { box-shadow: 0 0 0 10px rgba(255, 0, 0, 0); } 100% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); } }
    .btn-group-v { display: flex; flex-direction: column; align-items: center; gap: 2px; flex: 1; }
    .sc-label { font-size: 9px; font-weight: bold; margin-top: 2px; text-align: center; white-space: nowrap; }
    .yt-dl-banner { background: linear-gradient(90deg, #d63384, #ff9800); color: white; padding: 12px; margin-bottom: 10px; border-radius: 6px; font-weight: bold; font-size: 12px; display: flex; justify-content: space-between; align-items: center; animation: slideUp 0.5s; }
  `;
  const injectCSS = () => { if(!document.getElementById("yt-dl-style")) { const s=document.createElement("style"); s.id="yt-dl-style"; s.textContent=css; document.head.appendChild(s); }};

  const toast = (msg, success=true) => {
      const existing = document.querySelector('.yt-dl-toast'); if (existing) existing.remove();
      const el=document.createElement("div"); el.className="yt-dl-toast"; el.textContent=msg;
      if(!success) el.style.background="#f44336";
      document.body.appendChild(el); setTimeout(()=> { if(el.parentNode) el.remove(); }, 3000);
  };

  let container;

  const generateListHTML = () => {
      if(state.items.length === 0) return `<div style="text-align:center;color:#444;padding:20px;">Empty list</div>`;
      return state.items.slice().reverse().slice(0,5).map(i => {
            const isAud = i.type === 'audio'; const isImg = i.type === 'image';
            let tagClass = 'tag-vid'; let tagTxt = 'MP4';
            if(isAud) { tagClass='tag-aud'; tagTxt='MP3'; }
            if(isImg) { tagClass='tag-img'; tagTxt='IMG'; }

            let statusHtml = `<span style="color:${i.status==='finished'?'#4caf50':(i.status==='error'?'#f44336':'#aaa')}">${i.status}</span>`;
            if(i.status==='auth_error') statusHtml = `<span class="auth-fix-btn" style="color:#ff9800;font-weight:bold" title="Click to Fix">${T.login_err}</span>`;
            if(i.status==='cancelled') statusHtml = `<span style="color:#f44336;font-size:10px">${T.cancel}</span>`;

            let progressHtml = '';
            if (i.status === 'downloading' || i.status === 'recording') {
                let pct = i.progress ? i.progress : 0;
                if(i.status === 'recording') pct = 100;
                progressHtml = `
                <div class="progress-bg"><div class="progress-fill" style="width:${pct}%"></div></div>
                <div class="prog-text">${i.status === 'recording' ? 'REC ●' : pct + '%'}</div>`;
            }

            let actions = '';
            if(i.status === 'downloading' || i.status === 'queued' || i.status === 'recording') {
                actions = `<button class="ctrl-btn btn-cancel" data-act="cancel" data-id="${i.id}">${T.cancel}</button>`;
            } else if(i.status === 'finished') {
                actions = `<button class="ctrl-btn" data-act="open" data-file="${encodeURIComponent(i.filename)}">▶️</button> <button class="ctrl-btn" data-act="folder" data-type="${i.type}">📂</button>`;
            } else if(i.status === 'error' || i.status === 'cancelled' || i.status === 'auth_error') {
                actions = `<button class="ctrl-btn btn-retry" data-act="retry" data-url="${i.url}" data-type="${i.type}" data-thumb="${i.thumb}">${T.retry}</button>`;
            }

            let thumbSrc = ""; let useTunnel = false; let dataTunnel = "";
            if (i.thumb && i.thumb.length > 5) { thumbSrc = i.thumb; if (!i.thumb.startsWith('https://')) useTunnel = true; dataTunnel = i.thumb; }
            else if (i.status === 'finished' && i.type === 'image' && i.filename) { dataTunnel = `/file/${encodeURIComponent(i.filename)}`; useTunnel = true; }
            if(imgCache[i.id]) { thumbSrc = imgCache[i.id]; useTunnel = false; }

            const imgHTML = `<img class="yt-dl-thumb" src="${useTunnel ? '' : thumbSrc}" ${useTunnel && !imgCache[i.id] ? `data-tunnel="${dataTunnel}" data-id="${i.id}"` : ''} onerror="this.style.display='none'">`;
            return `<div class="yt-dl-item">${imgHTML}<div class="yt-dl-info"><div class="yt-dl-name" title="${i.title}">${isAud?'🎵':(isImg?'🖼️':'🎬')} ${i.title||'...'}</div><div class="yt-dl-status"><span class="tag-type ${tagClass}">${tagTxt}</span>${statusHtml}</div>${progressHtml}</div><div style="display:flex; flex-direction:column; gap:2px;">${actions}</div></div>`;
      }).join('');
  };

  const updateListContent = () => {
      if(!container || state.uiMode !== 2) return;
      const listEl = document.getElementById('yt-dl-list');
      const statsEl = document.getElementById('yt-dl-stats-bar');
      const newHtml = generateListHTML();
      if(listEl && newHtml !== lastHtml) {
          listEl.innerHTML = safeHTML(newHtml); lastHtml = newHtml;
          listEl.querySelectorAll('img[data-tunnel]').forEach(img => { const url = img.getAttribute('data-tunnel'); const id = img.getAttribute('data-id'); if(url && id) tunnelUniversalImage(img, url, id); });
          bindListButtons();
      }
      if(statsEl) statsEl.innerHTML = safeHTML(`<span>${T.queue}: <b style="color:#ffeb3b">${state.stats.in_progress||0}</b></span> <span>${T.done}: <b style="color:#4caf50">${state.stats.finished||0}</b></span> <span>${T.err}: <b style="color:#f44336">${state.stats.errors||0}</b></span>`);
  };

  const bindListButtons = () => {
      if(!container) return;
      container.querySelectorAll('.ctrl-btn').forEach(b => {
          b.onclick = (e) => {
              const d = e.target.dataset;
              if(d.act === 'open') openLocalFile(decodeURIComponent(d.file));
              if(d.act === 'folder') openFolder(d.type);
              if(d.act === 'cancel') cancelDownload(d.id);
              if(d.act === 'retry') send(d.type, d.url, d.thumb);
          };
      });
      container.querySelectorAll('.auth-fix-btn').forEach(b => { b.onclick = (e) => { e.preventDefault(); GM_openInTab(`${SERVER_URL}/panel?tab=cook`, {active: true}); }; });
  };

  const renderUI = () => {
      injectCSS();
      if(!container) { container=document.createElement('div'); container.className='yt-dl-container'; document.body.appendChild(container); makeDraggable(container); }
      if(state.uiMode === 0) { container.style.display = 'none'; return; }
      container.style.display = 'block';

      if(state.uiMode === 1) {
          container.innerHTML = safeHTML(`<div class="yt-dl-bubble" id="yt-dl-bubble-btn" title="${T.open}"><img src="${ICONS.bubble}"></div>`);
          document.getElementById('yt-dl-bubble-btn').onclick = () => { if(container.dataset.moved !== "true") setUIMode(2); };
          return;
      }

      let bannerHtml = '';
      if (!GM_getValue('yt_dl_seen_v691_banner', false)) {
          bannerHtml = `
          <div class="yt-dl-banner" id="yt-dl-banner-v69">
            <span>${T.banner_msg}</span>
            <button id="btn-close-banner" style="background:white;color:#d63384;border:none;border-radius:4px;cursor:pointer;padding:2px 8px;font-weight:bold">${T.banner_btn}</button>
          </div>`;
      }

      const dlContent = `
        ${bannerHtml}
        <select id="yt-dl-quality" class="yt-dl-select">
            <option value="best">${T.q_best}</option>
            <option value="1080">${T.q_1080}</option>
            <option value="720">${T.q_720}</option>
            <option value="480">${T.q_480}</option>
        </select>

        <div class="yt-dl-btn-group">
            <div class="btn-group-v">
                <button class="yt-dl-btn btn-blue" id="btn-vid" style="width:100%">${T.vid}</button>
                <div class="sc-label" style="color:#3ea6ff">${T.sc_vid}</div>
            </div>
            <div class="btn-group-v">
                <button class="yt-dl-btn btn-purple" id="btn-aud" style="width:100%">${T.aud}</button>
                <div class="sc-label" style="color:#d63384">${T.sc_aud}</div>
            </div>
            <div class="btn-group-v">
                <button class="yt-dl-btn btn-orange" id="btn-img" style="width:100%">${T.img}</button>
                <div class="sc-label" style="color:#ff9800">${T.sc_img}</div>
            </div>
        </div>

        <div style="display:flex; gap:8px; margin:10px 0;">
            <div class="btn-group-v">
                <button class="yt-dl-btn btn-gray" id="btn-rec" style="width:100%">${T.rec}</button>
                <div class="sc-label" style="color:#f44336">${T.sc_rec}</div>
            </div>
            <div class="btn-group-v">
                <button class="yt-dl-btn btn-purple" id="btn-ss" style="width:100%">${T.ss_btn}</button>
                <div class="sc-label" style="color:#9c27b0">${T.sc_ss}</div>
            </div>
        </div>

        <div style="background:#222; color:#ffeb3b; padding:8px; border-radius:6px; font-size:10px; margin-bottom:12px; line-height:1.4; border:1px solid #444;">${T.pro_tip}</div>
        <div id="yt-dl-stats-bar" style="font-size:10px; color:#aaa; display:flex; justify-content:space-between; margin-bottom:10px; background:#1a1a1a; padding:8px; border-radius:6px;"><span>${T.queue}: ...</span></div>
        <div id="yt-dl-list">${generateListHTML()}</div>
        <div style="margin-top:15px; display:flex; gap:5px;">
            <button class="yt-dl-btn btn-gray" id="btn-refresh" style="font-size:11px; padding:6px; flex:1;">${T.refresh}</button>
            <button class="yt-dl-btn btn-blue" id="btn-open-panel" style="font-size:11px; padding:6px; flex:1;">${T.btn_panel}</button>
            <button class="yt-dl-btn btn-red" id="btn-clear" style="font-size:11px; padding:6px; flex:1;">${T.clear}</button>
        </div>`;

      const batchContent = `<div style="padding:5px"><textarea id="yt-dl-batch-area" class="batch-area" placeholder="${T.batch_ph}"></textarea><button id="btn-batch-proc" class="yt-dl-btn btn-purple" style="width:100%">${T.batch_btn}</button></div>`;

      const helpContent = `
        <div style="padding:20px; text-align:center;">
            <img src="${ICONS.warn}" style="width:50px;margin-bottom:10px;" onerror="this.src='https://img.icons8.com/?size=100&id=42452&format=png&color=ff9800'">
            <div style="background:#ffeb3b; color:#000; padding:10px; border-radius:6px; font-weight:bold; margin-bottom:15px; font-size:11px; border:2px solid #fbc02d;">${T.server_update_warn}</div>
            <h3 style="color:#fff;margin:0 0 15px 0;font-size:16px;">${T.help_title}</h3>
            <div style="background:#1a1a1a; border-radius:8px; padding:20px; text-align:left; font-size:12px; line-height:1.8; color:#ccc; border:1px solid #333;">
                <div style="margin-bottom:5px"><b>${T.help_s1}</b></div>
                <div style="margin-bottom:5px"><b>${T.help_s2}</b></div>
                <div style="margin-bottom:5px"><b>${T.help_s3}</b></div>
            </div>
            <p style="color:#f44336; font-size:11px; font-weight:bold; margin:15px 0 15px;">${T.help_warn}</p>
            <button id="btn-do-download" style="background:#4caf50; color:white; border:none; padding:12px 20px; border-radius:6px; font-weight:bold; cursor:pointer; width:100%; font-size:14px; box-shadow:0 4px 15px rgba(76,175,80,0.3); text-transform:uppercase;">${T.help_btn_dl}</button>
            <div style="margin-top:10px; font-size:10px; color:#4caf50; font-weight:bold">${T.univ_note}</div>
            <div style="background:#b71c1c; color:#fff; font-weight:bold; padding:8px; border-radius:6px; font-size:11px; margin-top:10px;">${T.help_login_err}</div>
            <div id="btn-back-dl" style="margin-top:20px; font-size:12px; color:#aaa; cursor:pointer; text-decoration:underline;">${T.back}</div>
        </div>`;

      const cryptoList = [ {img: ICONS.btc, name: "BTC", val: "bc1q6gz3dtj9qvlxyyh3grz35x8xc7hkuj07knlemn"}, {img: ICONS.eth, name: "ETH", val: "0xd8724d0b19d355e9817d2a468f49e8ce067e70a6"}, {img: ICONS.sol, name: "SOL", val: "7ztAogE7SsyBw7mwVHhUr5ZcjUXQr99JoJ6oAgP99aCn"}, {img: ICONS.usdt, name: "USDT", val: "0xd8724d0b19d355e9817d2a468f49e8ce067e70a6"} ].map(c => `<div class="sup-row"><img src="${c.img}" class="sup-icon"><span style="font-size:9px;color:#888;width:30px">${c.name}</span><input type="text" class="sup-val" readonly value="${c.val}"><button class="sup-copy" data-val="${c.val}">${T.btn_copy}</button></div>`).join('');
      const supContent = `<div style="padding:15px;text-align:center"><div style="color:#d63384;font-weight:bold;margin-bottom:5px">${T.sup_title}</div><div style="color:#aaa;font-size:11px;margin-bottom:15px">${T.sup_desc}</div><div style="text-align:left;color:#d63384;font-weight:bold;font-size:10px;margin-bottom:5px">${T.lbl_pix}</div><div class="sup-row"><img src="${ICONS.pix}" class="sup-icon"><input type="text" class="sup-val" readonly value="69993230419"><button class="sup-copy" data-val="69993230419">${T.btn_copy}</button></div><div style="text-align:left;color:#d63384;font-weight:bold;font-size:10px;margin:15px 0 5px">${T.wallet_title}</div>${cryptoList}<div style="text-align:center;margin-top:20px;"><a href="https://www.paypal.com/donate/?business=4J4UK7ACU3DS6" target="_blank" style="display:inline-flex;align-items:center;gap:8px;background:#003087;color:white;padding:8px 20px;border-radius:20px;text-decoration:none;font-weight:bold;font-size:12px"><img src="${ICONS.paypal}" style="height:20px"> PayPal</a></div></div>`;

      let activeContent = dlContent;
      if (state.activeTab === 'sup') activeContent = supContent;
      if (state.activeTab === 'help') activeContent = helpContent;
      if (state.activeTab === 'batch') activeContent = batchContent;

      const panelHtml = `
      <div class="yt-dl-panel">
          <div class="yt-dl-head"><span style="font-weight:700;color:#fff;font-size:13px;">${T.title}</span><div style="display:flex;gap:10px;align-items:center"><span id="yt-dl-help-btn" style="cursor:pointer;font-size:12px;color:${state.activeTab==='help'?'#fff':'#4caf50'};font-weight:bold" title="${T.help_btn}">[?]</span><span class="yt-dl-min-btn" id="yt-dl-min" title="Minimize">▼</span></div></div>
          <div class="yt-dl-tabs"><div class="yt-dl-tab ${state.activeTab==='dl'?'active':''}" id="tab-btn-dl">${T.tab_dl}</div><div class="yt-dl-tab ${state.activeTab==='batch'?'active':''}" id="tab-btn-batch">${T.tab_batch}</div><div class="yt-dl-tab ${state.activeTab==='sup'?'active':''}" id="tab-btn-sup">${T.tab_sup}</div><div class="yt-dl-tab ${state.activeTab==='help'?'active':''}" id="tab-btn-help">${T.tab_help}</div></div>
          <div class="yt-dl-body">${activeContent}</div>
          <div class="yt-dl-footer">${T.footer_msg}</div>
      </div>`;

      container.innerHTML = safeHTML(panelHtml);

      const btnCloseBanner = document.getElementById('btn-close-banner');
      if (btnCloseBanner) btnCloseBanner.onclick = () => { GM_setValue('yt_dl_seen_v691_banner', true); GM_openInTab(DRIVE_LINK, {active:true}); renderUI(); };

      document.getElementById('yt-dl-min').onclick = () => setUIMode(1);
      document.getElementById('yt-dl-help-btn').onclick = () => { state.activeTab='help'; renderUI(); };
      document.getElementById('tab-btn-dl').onclick = () => { state.activeTab='dl'; renderUI(); };
      document.getElementById('tab-btn-batch').onclick = () => { state.activeTab='batch'; renderUI(); };
      document.getElementById('tab-btn-sup').onclick = () => { state.activeTab='sup'; renderUI(); };
      document.getElementById('tab-btn-help').onclick = () => { state.activeTab='help'; renderUI(); };

      if(state.activeTab === 'dl') {
          document.getElementById('btn-vid').onclick = () => send('video');
          document.getElementById('btn-aud').onclick = () => send('audio');
          document.getElementById('btn-img').onclick = () => send('image');
          document.getElementById('btn-rec').onclick = toggleRecording;
          document.getElementById('btn-ss').onclick = captureFrame;
          document.getElementById('btn-refresh').onclick = refreshData;
          document.getElementById('btn-clear').onclick = clearList;
          document.getElementById('btn-open-panel').onclick = () => GM_openInTab(SERVER_URL + '/panel', {active:true});
          bindListButtons();
      } else if (state.activeTab === 'batch') { document.getElementById('btn-batch-proc').onclick = processBatch;
      } else if (state.activeTab === 'help') { document.getElementById('btn-do-download').onclick = () => GM_openInTab(DRIVE_LINK, {active:true}); document.getElementById('btn-back-dl').onclick = () => { state.activeTab='dl'; renderUI(); };
      } else { container.querySelectorAll('.sup-copy').forEach(btn => { btn.onclick = (e) => copyToClipboard(e.target.dataset.val); }); }
      updateListContent();
      updateButtonState();
  };

  const addInlineButtons = () => {
      const container = document.querySelector('[id^="top-level-buttons"]');
      if (!container || container.querySelector("#yt-dl-inline-vid")) return;
      const style = "height:36px; padding:0 16px; border-radius:18px; margin-left:8px; cursor:pointer; font-weight:500; font-size:14px; border:none; display:inline-flex; align-items:center; justify-content:center;";
      const btnV = document.createElement("button"); btnV.id = "yt-dl-inline-vid"; btnV.textContent = T.vid; btnV.style.cssText = style + "background:#3ea6ff; color:#0f0f0f;"; btnV.onclick = (e) => { e.preventDefault(); send('video'); };
      const btnA = document.createElement("button"); btnA.id = "yt-dl-inline-aud"; btnA.textContent = T.aud; btnA.style.cssText = style + "background:#d63384; color:#fff;"; btnA.onclick = (e) => { e.preventDefault(); send('audio'); };
      container.appendChild(btnV); container.appendChild(btnA);
  };
  const observer = new MutationObserver(addInlineButtons);
  observer.observe(document.body, { childList: true, subtree: true });

  setInterval(refreshData, POLLING_INTERVAL);

  window.addEventListener("keydown", (e) => {
      if (e.altKey && e.shiftKey && (e.key === "Y" || e.key === "y")) { setUIMode(state.uiMode === 0 ? 1 : 0); e.preventDefault(); }
      if (e.altKey && (e.key === "R" || e.key === "r")) { toggleRecording(); e.preventDefault(); }
      if (e.altKey && (e.key === "S" || e.key === "s")) { captureFrame(); e.preventDefault(); }
  });

  GM_registerMenuCommand(T.menu_update, () => GM_openInTab(UPDATE_URL, {active:true}));
  GM_registerMenuCommand(T.menu_toggle, () => setUIMode(state.uiMode === 0 ? 1 : 0));
  GM_registerMenuCommand(T.menu_help, () => { state.activeTab='help'; setUIMode(2); });
  GM_registerMenuCommand(T.menu_panel, () => GM_openInTab(SERVER_URL + '/panel', {active:true}));
  GM_registerMenuCommand(T.menu_dl, () => GM_openInTab(DRIVE_LINK, {active:true}));
  GM_registerMenuCommand(T.rec, toggleRecording);
  GM_registerMenuCommand(T.ss_btn, captureFrame);

  setTimeout(() => renderUI(), 1000);
  refreshData();
})();