Greasy Fork

Greasy Fork is available in English.

YouTube Downloader - Local Server Interface - PRO

最好的YouTube下载器!通过本地服务器下载视频(全高清/4K/8K)和音频(MP3)。无限制,无需等待,最高速度。功能:类型图标,自动清理。

当前为 2025-12-02 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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.11.1
// @description  The Best YouTube Downloader! Download Video (Full HD/4K/8K) & Audio (MP3) via Local Server. No limits, no waiting, max speed. Features: Type icons (Video/Audio), auto-clean, fixed UI.
// @description:pt-BR A melhor ferramenta para baixar YouTube! Baixe Vídeos (Full HD/4K/8K) e Áudio (MP3) via Servidor Local. Sem limites, sem espera, velocidade máxima. Recursos: Ícones de tipo, limpeza automática, UI fixa.
// @description:es   ¡El mejor descargador de YouTube! Descarga video (Full HD/4K/8K) y audio (MP3) a través del servidor local. Sin límites, sin esperas, máxima velocidad. Características: Iconos de tipo, limpieza automática.
// @description:zh-CN 最好的YouTube下载器!通过本地服务器下载视频(全高清/4K/8K)和音频(MP3)。无限制,无需等待,最高速度。功能:类型图标,自动清理。
// @description:ru   Лучший загрузчик YouTube! Скачивайте видео (Full HD/4K/8K) и аудио (MP3) через локальный сервер. Без ограничений, без ожиданий, максимальная скорость.
// @description:fr   Le meilleur téléchargeur YouTube ! Téléchargez Vidéo (Full HD/4K/8K) et Audio (MP3) via serveur local. Sans limites, sans attente, vitesse maximale.
// @description:de   Der beste YouTube-Downloader! Video (Full HD/4K/8K) & Audio (MP3) über lokalen Server herunterladen. Keine Limits, keine Wartezeit, maximale Geschwindigkeit.
// @description:ja   最高のYouTubeダウンローダー!ローカルサーバー経由でビデオ(フルHD / 4K / 8K)とオーディオ(MP3)をダウンロードします。制限なし、待機なし、最高速度。
// @description:it   Il miglior downloader di YouTube! Scarica video (Full HD/4K/8K) e audio (MP3) tramite server locale. Nessun limite, nessuna attesa, massima velocità.
// @description:hi   सर्वश्रेष्ठ यूट्यूब डाउनलोडर! स्थानीय सर्वर के माध्यम से वीडियो (पूर्ण एचडी/4K/8K) और ऑडियो (MP3) डाउनलोड करें। कोई सीमा नहीं, कोई प्रतीक्षा नहीं, अधिकतम गति।
// @description:id   Pengunduh YouTube Terbaik! Unduh Video (Full HD/4K/8K) & Audio (MP3) melalui Server Lokal. Tanpa batas, tanpa menunggu, kecepatan maksimal.
// @description:ko   최고의 YouTube 다운로더! 로컬 서버를 통해 비디오(Full HD/4K/8K) 및 오디오(MP3)를 다운로드하십시오. 제한 없음, 대기 없음, 최대 속도.
// @description:ar   أفضل تنزيل يوتيوب! قم بتنزيل الفيديو (Full HD/4K/8K) والصوت (MP3) عبر الخادم المحلي. لا حدود ، لا انتظار ، أقصى سرعة.
// @copyright    2025, Tauã B. Kloch Leite - All Rights Reserved.
// @author       Tauã B. Kloch Leite
// @icon         https://img.icons8.com/?size=100&id=9F8aDI7mYs6V&format=png&color=000000
// @match        https://www.youtube.com/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_openInTab
// @grant        GM_setClipboard
// ==/UserScript==

(function () {
  'use strict';

  // --- POLÍTICA DE SEGURANÇA (TrustedTypes) ---
  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/13TOlyMFAzz7eEOTqAaf6Ex7eBR6rx1ex/view?usp=drive_link";
  const POLLING_INTERVAL = 1500;

  // --- ÍCONES ---
  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",
        bnb: "https://cryptologos.cc/logos/bnb-bnb-logo.svg?v=025",
        matic: "https://cryptologos.cc/logos/polygon-matic-logo.svg?v=025",
        usdt: "https://cryptologos.cc/logos/tether-usdt-logo.svg?v=025",
        bubble: "https://img.icons8.com/?size=100&id=9F8aDI7mYs6V&format=png&color=ffffff",
        // Ícone de aviso estável (Wikimedia)
        warn: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Antu_dialog-warning.svg/200px-Antu_dialog-warning.svg.png"
  };

  // --- TRADUÇÕES ---
  const STRINGS = {
    en: { title: "Local Downloader PRO", tab_dl: "Downloads", tab_sup: "Donate", vid: "🎬 Video", aud: "🎵 Audio", queue: "Queue", done: "Done", err: "Error", refresh: "🔄 Refresh", clear: "🗑️ Clear", conn_err: "Server Offline?", 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 (Click to fix)", retry: "Retry", cancel: "Cancel", open_panel: "🚀 Open Server Panel", toggle: "👁️ Show/Hide UI", help_btn: "❓ Help / Install", help_title: "SETUP REQUIRED", help_s1: "1. Download YT_Downloader.exe", help_s2: "2. Open the App", help_s3: "3. Click 'Start Server'", help_btn_dl: "DOWNLOAD SERVER", help_warn: "Script requires this app!", back: "Back to Panel" },
    pt: { title: "Downloader Local PRO", tab_dl: "Downloads", tab_sup: "Doação", vid: "🎬 Vídeo", aud: "🎵 Áudio", queue: "Fila", done: "Prontos", err: "Erros", refresh: "🔄 Atualizar", clear: "🗑️ Limpar", conn_err: "Servidor Offline?", 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 (Clique para arrumar)", retry: "🔄 Reiniciar", cancel: "❌ Cancelar", open_panel: "🚀 Abrir Painel Server", toggle: "👁️ Mostrar/Ocultar UI", help_btn: "❓ Ajuda / Instalação", help_title: "INSTALAÇÃO NECESSÁRIA", help_s1: "1. Baixe o YT_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!", back: "Voltar para o Painel" },
    es: { title: "Descargador Local PRO", tab_dl: "Descargas", tab_sup: "Donación", vid: "🎬 Video", aud: "🎵 Audio", queue: "Cola", done: "Hecho", err: "Error", refresh: "🔄 Actualizar", clear: "🗑️ Limpiar", conn_err: "¿Servidor Offline?", open: "Abrir", folder: "Carpeta", sup_title: "APOYA EL CÓDIGO", sup_desc: "¡Ayuda a mantener las actualizaciones!", lbl_pix: "CLAVE PIX", btn_copy: "COPIAR", auto_dl: "⬇️ Guardado: ", wallet_title: "BILLETERAS CRIPTO", login_err: "⚠️ LOGIN REQUERIDO", retry: "Reintentar", cancel: "Cancelar", open_panel: "🚀 Abrir Panel", toggle: "👁️ Mostrar/Ocultar", help_btn: "❓ Ayuda / Instalación", help_title: "INSTALACIÓN REQUERIDA", help_s1: "1. Descarga YT_Downloader.exe", help_s2: "2. Abre la Aplicación", help_s3: "3. Haz clic en 'Start Server'", help_btn_dl: "DESCARGAR SERVIDOR", help_warn: "¡Se requiere la app!", back: "Volver al Panel" },
    ru: { title: "Локальный Загрузчик", tab_dl: "Скачать", tab_sup: "Донат", vid: "🎬 Видео", aud: "🎵 Аудио", queue: "Очередь", done: "Готово", err: "Ошибка", refresh: "🔄 Обновить", clear: "🗑️ Очистить", conn_err: "Сервер офлайн?", open: "Открыть", folder: "Папка", sup_title: "ПОДДЕРЖАТЬ КОД", sup_desc: "Помогите выпускать обновления!", lbl_pix: "PIX KEY", btn_copy: "КОПИРОВАТЬ", auto_dl: "⬇️ Сохранено: ", wallet_title: "КРИПТО КОШЕЛЬКИ", login_err: "⚠️ НУЖЕН ВХОД", retry: "Повтор", cancel: "Отмена", open_panel: "🚀 Открыть панель", toggle: "👁️ Скрыть/Показать", help_btn: "❓ Помощь / Установка", help_title: "ТРЕБУЕТСЯ УСТАНОВКА", help_s1: "1. Скачать YT_Downloader.exe", help_s2: "2. Открыть приложение", help_s3: "3. Нажать 'Start Server'", help_btn_dl: "СКАЧАТЬ СЕРВЕР", help_warn: "Скрипт требует это!", back: "Назад в панель" },
    fr: { title: "Téléchargeur Local", tab_dl: "Téléch.", tab_sup: "Don", vid: "🎬 Vidéo", aud: "🎵 Audio", queue: "File", done: "Fait", err: "Erreur", refresh: "🔄 Rafraîchir", clear: "🗑️ Vider", conn_err: "Serveur Hors ligne?", open: "Ouvrir", folder: "Dossier", sup_title: "SOUTENIR LE CODE", sup_desc: "Aidez à garder les mises à jour!", lbl_pix: "CLÉ PIX", btn_copy: "COPIER", auto_dl: "⬇️ Enregistré: ", wallet_title: "PORTEFEUILLES CRYPTO", login_err: "⚠️ CONNEXION REQUISE", retry: "Réessayer", cancel: "Annuler", open_panel: "🚀 Ouvrir Panneau", toggle: "👁️ Afficher/Masquer", help_btn: "❓ Aide / Installation", help_title: "INSTALLATION REQUISE", help_s1: "1. Télécharger YT_Downloader.exe", help_s2: "2. Ouvrir l'application", help_s3: "3. Cliquez sur 'Start Server'", help_btn_dl: "TÉLÉCHARGER SERVEUR", help_warn: "Le script a besoin de ça!", back: "Retour au panneau" },
    de: { title: "Lokaler Downloader", tab_dl: "Downloads", tab_sup: "Spenden", vid: "🎬 Video", aud: "🎵 Audio", queue: "Warteschlange", done: "Fertig", err: "Fehler", refresh: "🔄 Aktualisieren", clear: "🗑️ Löschen", conn_err: "Server Offline?", open: "Öffnen", folder: "Ordner", sup_title: "CODE UNTERSTÜTZEN", sup_desc: "Helfen Sie mit Updates!", lbl_pix: "PIX KEY", btn_copy: "KOPIEREN", auto_dl: "⬇️ Gespeichert: ", wallet_title: "KRYPTO-WALLETS", login_err: "⚠️ LOGIN ERFORDERLICH", retry: "Wiederholen", cancel: "Abbrechen", open_panel: "🚀 Panel öffnen", toggle: "👁️ Ein/Ausblenden", help_btn: "❓ Hilfe / Installation", help_title: "INSTALLATION ERFORDERLICH", help_s1: "1. YT_Downloader.exe herunterladen", help_s2: "2. App öffnen", help_s3: "3. 'Start Server' klicken", help_btn_dl: "SERVER HERUNTERLADEN", help_warn: "Skript benötigt dies!", back: "Zurück zum Panel" },
    it: { title: "Downloader Locale", tab_dl: "Scarica", tab_sup: "Donazione", vid: "🎬 Video", aud: "🎵 Audio", queue: "Coda", done: "Fatto", err: "Errore", refresh: "🔄 Aggiorna", clear: "🗑️ Pulisci", conn_err: "Server Offline?", open: "Apri", folder: "Cartella", sup_title: "SUPPORTA IL CODICE", sup_desc: "Aiuta gli aggiornamenti!", lbl_pix: "CHIAVE PIX", btn_copy: "COPIA", auto_dl: "⬇️ Salvato: ", wallet_title: "PORTAFOGLI CRYPTO", login_err: "⚠️ LOGIN RICHIESTO", retry: "Riprova", cancel: "Annulla", open_panel: "🚀 Apri Pannello", toggle: "👁️ Mostra/Nascondi", help_btn: "❓ Aiuto / Installazione", help_title: "INSTALLAZIONE RICHIESTA", help_s1: "1. Scarica YT_Downloader.exe", help_s2: "2. Apri l'App", help_s3: "3. Clicca 'Start Server'", help_btn_dl: "SCARICA SERVER", help_warn: "Lo script richiede questo!", back: "Torna al Pannello" },
    zh: { title: "本地下载器 PRO", tab_dl: "下载", tab_sup: "捐赠", vid: "🎬 视频", aud: "🎵 音频", queue: "队列", done: "完成", err: "错误", refresh: "🔄 刷新", clear: "🗑️ 清除", conn_err: "服务器离线?", open: "打开", folder: "文件夹", sup_title: "支持代码", sup_desc: "帮助保持更新!", lbl_pix: "PIX 密钥", btn_copy: "复制", auto_dl: "⬇️ 已保存: ", wallet_title: "加密钱包", login_err: "⚠️ 需要登录", retry: "重试", cancel: "取消", open_panel: "🚀 打开面板", toggle: "👁️ 显示/隐藏", help_btn: "❓ 帮助 / 安装", help_title: "需要安装", help_s1: "1. 下载 YT_Downloader.exe", help_s2: "2. 打开应用程序", help_s3: "3. 点击 'Start Server'", help_btn_dl: "下载服务器", help_warn: "脚本需要此应用!", back: "返回面板" },
    ja: { title: "ローカルダウンローダー", tab_dl: "ダウンロード", tab_sup: "寄付", vid: "🎬 ビデオ", aud: "🎵 オーディオ", queue: "キュー", done: "完了", err: "エラー", refresh: "🔄 更新", clear: "🗑️ クリア", conn_err: "サーバーオフライン?", open: "開く", folder: "フォルダ", sup_title: "コードをサポート", sup_desc: "更新を続けるのを手伝ってください!", lbl_pix: "PIXキー", btn_copy: "コピー", auto_dl: "⬇️ 保存済み: ", wallet_title: "暗号ウォレット", login_err: "⚠️ ログインが必要", retry: "再試行", cancel: "キャンセル", open_panel: "🚀 パネルを開く", toggle: "👁️ 表示/非表示", help_btn: "❓ ヘルプ / インストール", help_title: "インストールが必要", help_s1: "1. YT_Downloader.exe をダウンロード", help_s2: "2. アプリを開く", help_s3: "3. 'Start Server' をクリック", help_btn_dl: "サーバーをダウンロード", help_warn: "スクリプトにはこれが必要です!", back: "パネルに戻る" }
  };

  const getLang = () => {
      const l = navigator.language || "en";
      if (l.startsWith("pt")) return STRINGS.pt;
      if (l.startsWith("es")) return STRINGS.es;
      if (l.startsWith("ru")) return STRINGS.ru;
      if (l.startsWith("fr")) return STRINGS.fr;
      if (l.startsWith("de")) return STRINGS.de;
      if (l.startsWith("it")) return STRINGS.it;
      if (l.startsWith("zh")) return STRINGS.zh;
      if (l.startsWith("ja")) return STRINGS.ja;
      return STRINGS.en;
  };
  const T = getLang();

  // ESTADO DA UI
  const state = { uiMode: GM_getValue("yt_dl_uiMode", 1), stats: {}, items: [], activeTab: 'dl' };
  const setUIMode = (m) => { state.uiMode = m; GM_setValue("yt_dl_uiMode", m); renderUI(); };

  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); }};

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

  const refreshData = async () => {
      try {
          const [sRes, fRes] = await Promise.all([ fetch(`${SERVER_URL}/stats`), fetch(`${SERVER_URL}/files`) ]);
          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(); // Agora atualiza com segurança
      } catch (e) {}
  };

  const send = async (type, retryUrl = null, retryThumb = null) => {
      try {
          let finalUrl = retryUrl || location.href;
          let thumbUrl = retryThumb;
          if (!thumbUrl) {
              try {
                  const urlObj = new URL(finalUrl);
                  const vidId = urlObj.searchParams.get("v");
                  if (vidId) thumbUrl = `https://i.ytimg.com/vi/${vidId}/maxresdefault.jpg`;
                  else { const meta = document.querySelector('meta[property="og:image"]'); if(meta) thumbUrl = meta.content; }
              } catch(e) {}
          }
          await fetch(`${SERVER_URL}/${type === 'video' ? 'download' : 'download_audio'}`, {
              method: 'POST', headers: {'Content-Type': 'application/json'},
              body: JSON.stringify({ videoUrl: finalUrl, thumb: thumbUrl })
          });
          refreshData();
          toast("OK 🚀");
          if(state.uiMode === 1) setUIMode(2);
      } catch(e) { toast(T.conn_err, false); }
  };

  // --- CSS ---
  const css = `
    .yt-dl-container { font-family: 'Roboto', sans-serif; z-index: 2147483647; position: fixed; bottom: 20px; left: 20px; }
    .yt-dl-bubble { width: 50px; height: 50px; background: #d63384; border-radius: 50%; box-shadow: 0 4px 15px rgba(0,0,0,0.5); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: transform 0.2s; border: 2px solid #fff; }
    .yt-dl-bubble:hover { transform: scale(1.1); }
    .yt-dl-bubble img { width: 30px; height: 30px; }
    .yt-dl-panel { width: 350px; 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); display: flex; flex-direction: column; overflow: hidden; 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; }
    .yt-dl-min-btn { cursor: pointer; font-size: 18px; color: #aaa; padding: 0 5px; }
    .yt-dl-min-btn:hover { color: #fff; }
    .yt-dl-tabs { display: flex; background: #111; }
    /* ATUALIZADO: Cor da fonte mudou de #666 para #aaa para maior visibilidade */
    .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: 11px; }
    .yt-dl-tab.active { color: #fff; border-bottom: 2px solid #d63384; background: #222; }
    .yt-dl-body { max-height: 400px; overflow-y: auto; padding: 15px; }
    .yt-dl-btn-group { display: flex; gap: 8px; margin-bottom: 15px; }
    .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; }
    .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; }
    .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; }
    .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); } }
  `;
  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 el=document.createElement("div"); el.className="yt-dl-toast"; el.textContent=msg; if(!success) el.style.background="#f44336"; document.body.appendChild(el); setTimeout(()=>el.remove(), 3000); };

  let container;

  // --- HTML GENERATOR ---
  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 tagClass = isAud ? 'tag-aud' : 'tag-vid';
            const tagText = isAud ? 'MP3' : 'MP4';
            let statusHtml = `<span style="color:${i.status==='finished'?'#4caf50':(i.status==='error'?'#f44336':'#aaa')}">${i.status}</span>`;

            // CORREÇÃO DO AVISO DE LOGIN
            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 actions = '';
            if(i.status === 'downloading' || i.status === 'queued') {
                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>`;
            }

            return `
            <div class="yt-dl-item">
                <img class="yt-dl-thumb" src="${i.thumb||''}">
                <div class="yt-dl-info">
                    <div class="yt-dl-name" title="${i.title}">${isAud?'🎵':'🎬'} ${i.title||'...'}</div>
                    <div class="yt-dl-status"><span class="tag-type ${tagClass}">${tagText}</span>${statusHtml}</div>
                </div>
                <div style="display:flex; flex-direction:column; gap:2px;">${actions}</div>
            </div>`;
        }).join('');
  };

  // --- ATUALIZA APENAS O CONTEÚDO (COM PROTEÇÃO SAFEHTML) ---
  const updateListContent = () => {
      if(!container || state.uiMode !== 2) return;
      const listEl = document.getElementById('yt-dl-list');
      const statsEl = document.getElementById('yt-dl-stats-bar');
      if(!listEl) return;

      // Atualiza estatísticas com segurança
      if(statsEl) {
          const statsHtml = `
            <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>
          `;
          statsEl.innerHTML = safeHTML(statsHtml);
      }

      // Atualiza lista com segurança
      listEl.innerHTML = safeHTML(generateListHTML());
      bindListButtons();
  };

  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});
          };
      });
  };

  // --- RENDERIZA A ESTRUTURA (SOMENTE QUANDO MUDA O MODO) ---
  const renderUI = () => {
      injectCSS();
      if(!container) { container=document.createElement('div'); container.className='yt-dl-container'; document.body.appendChild(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 = () => setUIMode(2);
          return;
      }

      const dlContent = `
        <div class="yt-dl-btn-group">
            <button class="yt-dl-btn btn-blue" id="btn-vid">${T.vid}</button>
            <button class="yt-dl-btn btn-purple" id="btn-aud">${T.aud}</button>
        </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}: <b style="color:#ffeb3b">...</b></span>
        </div>
        <div id="yt-dl-list">${generateListHTML()}</div>
        <div style="margin-top:15px; display:flex; gap:10px;">
            <button class="yt-dl-btn btn-gray" id="btn-refresh" style="font-size:11px; padding:6px; flex:2;">${T.refresh}</button>
            <button class="yt-dl-btn btn-red" id="btn-clear" style="font-size:11px; padding:6px; flex:1;">${T.clear}</button>
        </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"},
          {img: ICONS.bnb, name: "BNB", val: "0xd8724d0b19d355e9817d2a468f49e8ce067e70a6"},
          {img: ICONS.matic, name: "MATIC", 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}<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;margin-top:20px;font-size:12px"><img src="${ICONS.paypal}" style="height:20px"> PayPal</a></div>`;

      // CONTEÚDO DA TELA DE AJUDA
      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'">
             <h3 style="color:#fff;margin:0 0 10px 0;font-size:16px;">${T.help_title}</h3>
             <div style="background:#1a1a1a; border-radius:8px; padding:15px; text-align:left; font-size:12px; line-height:1.6; 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:10px 0 15px;">${T.help_warn}</p>
             <button id="btn-do-download" style="background:#4caf50; color:white; border:none; padding:10px 20px; border-radius:6px; font-weight:bold; cursor:pointer; width:100%; font-size:13px;">${T.help_btn_dl}</button>
             <div id="btn-back-dl" style="margin-top:15px; font-size:11px; color:#aaa; cursor:pointer; text-decoration:underline;">${T.back}</div>
        </div>
      `;

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

      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==='sup'?'active':''}" id="tab-btn-sup">${T.tab_sup}</div>
          </div>
          <div class="yt-dl-body">${activeContent}</div>
      </div>`;

      container.innerHTML = safeHTML(panelHtml);

      // Eventos Principais
      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-sup').onclick = () => { state.activeTab='sup'; renderUI(); };

      if(state.activeTab === 'dl') {
          document.getElementById('btn-vid').onclick = () => send('video');
          document.getElementById('btn-aud').onclick = () => send('audio');
          document.getElementById('btn-refresh').onclick = refreshData;
          document.getElementById('btn-clear').onclick = clearList;
          bindListButtons(); // Conecta os botões da lista inicial
      } else if (state.activeTab === 'help') {
          // Ação do botão de download na tela de ajuda
          document.getElementById('btn-do-download').onclick = () => GM_openInTab(DRIVE_LINK, {active:true});
          // Ação do botão voltar
          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();
  };

  const addInlineButtons = () => {
    const container = document.querySelector('[id^="top-level-buttons"]');
    if (!container || container.querySelector("#yt-dl-inline-vid")) return;

    // ESTILO COMUM PARA FICAR GRANDE E PARECIDO COM NATIVO (Altura 36px, Padding maior)
    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;";

    // BOTÃO VIDEO (Maior, Azul)
    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'); };

    // BOTÃO AUDIO (NOVO - Mesmo tamanho, Roxo)
    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);

  // ATALHOS E MENU TAMPERMONKEY
  window.addEventListener("keydown", (e) => {
      if (e.altKey && e.shiftKey && (e.key === "Y" || e.key === "y")) {
          setUIMode(state.uiMode === 0 ? 1 : 0);
          e.preventDefault();
      }
  });

  GM_registerMenuCommand(T.toggle + " (Alt+Shift+Y)", () => setUIMode(state.uiMode === 0 ? 1 : 0));
  GM_registerMenuCommand(T.open_panel, () => GM_openInTab(SERVER_URL + '/panel', {active:true}));
  GM_registerMenuCommand(T.help_btn, () => { state.activeTab='help'; setUIMode(2); });

  // INICIALIZAÇÃO
  setTimeout(() => renderUI(), 1000);
  refreshData();
})();