The Best YouTube Downloader! Download Video (Full HD/4K/8K), Audio (MP3) & Images via Local Server. Features: Universal Support, Batch Download, Shortcuts, Resizable UI.
// ==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.3
// @description The Best YouTube Downloader! Download Video (Full HD/4K/8K), Audio (MP3) & Images via Local Server. Features: Universal Support, Batch Download, Shortcuts, Resizable UI.
// @description:pt-BR A melhor ferramenta para baixar YouTube! Baixe Vídeos (Full HD/4K/8K), Áudio (MP3) e Imagens via Servidor Local. Recursos: Suporte Universal, Download em Lote, Atalhos, UI Redimensionável.
// @description:es ¡El mejor descargador de YouTube! Descarga Video (Full HD/4K/8K), Audio (MP3) e Imágenes a través del Servidor Local. Características: Soporte Universal, Descarga por Lotes, Atajos, 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) et Images via serveur local. Fonctionnalités : Support Universel, Raccourcis clavier, UI redimensionnable.
// @description:de Der beste YouTube-Downloader! Video (Full HD/4K/8K), Audio (MP3) & Bilder über lokalen Server herunterladen. Features: Universelle Unterstützung, Tastenkürzel, 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) e Immagini tramite server locale. Funzioni: Supporto Universale, Scorciatoie da tastiera, UI ridimensionabile.
// @description:hi सर्वश्रेष्ठ यूट्यूब डाउनलोडर! स्थानीय सर्वर के माध्यम से वीडियो (पूर्ण एचडी/4K/8K), ऑडियो (MP3) और चित्र डाउनलोड करें। विशेषताएं: यूनिवर्सल समर्थन, कीबोर्ड शॉर्टकट, आकार बदलने योग्य यूआई।
// @description:id Pengunduh YouTube Terbaik! Unduh Video (Full HD/4K/8K), Audio (MP3) & Gambar melalui Server Lokal. Fitur: Dukungan Universal, Pintasan Keyboard, 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://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
// @grant GM_xmlhttpRequest
// @connect 127.0.0.1
// @connect *
// ==/UserScript==
(function () {
'use strict';
// --- PREVENT IFRAME INJECTION ---
// This ensures the script only runs on the main page, not in chat frames
if (window.self !== window.top) return;
// --- SECURITY POLICY (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;
// --- CONFIGURATION ---
const SERVER_URL = "http://127.0.0.1:5000";
const DRIVE_LINK = "https://drive.google.com/file/d/1MHOYc9haviNrfOZX_IeFwszBLj6K-f3o/view?usp=sharing";
const UPDATE_URL = "https://greasyfork.org/en/scripts/557579-youtube-downloader-local-server-interface-pro";
const POLLING_INTERVAL = 1500;
// --- ICONS ---
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",
warn: "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f7/Antu_dialog-warning.svg/200px-Antu_dialog-warning.svg.png"
};
// --- LOCALIZATION ---
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 (Home or Sidebar) to download instantly.",
err_old_ver: "⚠️ Requires New Universal Server! (See Help)",
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 (works on any site). 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",
tip_title: "SHORTCUTS (Right Click on Thumb):", tip_1: "SHIFT: Video", tip_2: "ALT: Audio", tip_3: "CTRL: Image"
};
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 direto na miniatura (Home ou Lateral) para baixar instantaneamente.",
err_old_ver: "⚠️ Requer Novo Servidor Universal! (Ver Ajuda)",
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 (funciona em todos os sites). 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",
tip_title: "ATALHOS (Clique Direito na Miniatura):", tip_1: "SHIFT: Vídeo", tip_2: "ALT: Áudio", tip_3: "CTRL: Imagem"
},
es: { ...EN_BASE, title: "Descargador Local PRO", sc_vid: "SHIFT + Clic Derecho", sc_aud: "ALT + Clic Derecho", sc_img: "CTRL + Clic Derecho", pro_tip: "💡 TIP PRO: ¡Usa atajos con Clic Derecho en la miniatura para descargar sin abrir el video!", err_old_ver: "⚠️ ¡Requiere Nuevo Servidor Universal!", univ_note: "NOTA: El nuevo servidor es UNIVERSAL. ¡Actualiza!", menu_help: "❓ Ayuda / Atajos", btn_panel: "Panel", conn_err: "¿Servidor Offline? ¡Inicia la App!", menu_update: "🔄 Buscar Actualización", tip_title: "ATAJOS:", tip_1: "SHIFT: Video", tip_2: "ALT: Audio", tip_3: "CTRL: Imagen" },
ru: { ...EN_BASE, title: "Локальный Загрузчик PRO", sc_vid: "SHIFT + ПКМ", sc_aud: "ALT + ПКМ", sc_img: "CTRL + ПКМ", pro_tip: "💡 СОВЕТ: Используйте горячие клавиши + ПКМ по миниатюре для быстрой загрузки!", err_old_ver: "⚠️ Требуется новый универсальный сервер!", univ_note: "ПРИМЕЧАНИЕ: Новый сервер УНИВЕРСАЛЕН. Обновите!", menu_help: "❓ Помощь / Ярлыки", btn_panel: "Панель", conn_err: "Сервер офлайн? Запустите приложение!", menu_update: "🔄 Проверить Обновление", tip_title: "ГОРЯЧИЕ КЛАВИШИ:", tip_1: "SHIFT: Видео", tip_2: "ALT: Аудио", tip_3: "CTRL: Фото" },
fr: { ...EN_BASE, title: "Téléchargeur Local PRO", sc_vid: "SHIFT + Clic Droit", sc_aud: "ALT + Clic Droit", sc_img: "CTRL + Clic Droit", pro_tip: "💡 ASTUCE PRO : Utilisez les raccourcis + Clic Droit sur la miniature pour télécharger sans ouvrir !", err_old_ver: "⚠️ Nouveau serveur universel requis !", univ_note: "NOTE : Le nouveau serveur est UNIVERSEL. Mettez à jour !", menu_help: "❓ Aide / Raccourcis", btn_panel: "Panneau", conn_err: "Serveur hors ligne ? Démarrez l'appli !", menu_update: "🔄 Vérifier Mise à Jour", tip_title: "RACCOURCIS:", tip_1: "SHIFT: Vidéo", tip_2: "ALT: Audio", tip_3: "CTRL: Image" },
de: { ...EN_BASE, title: "Lokaler Downloader PRO", sc_vid: "SHIFT + Rechtsklick", sc_aud: "ALT + Rechtsklick", sc_img: "CTRL + Rechtsklick", pro_tip: "💡 PRO TIPP: Tastenkürzel + Rechtsklick auf Thumbnail zum sofortigen Download!", err_old_ver: "⚠️ Neuer Universal-Server erforderlich!", univ_note: "HINWEIS: Der neue Server ist UNIVERSELL. Aktualisieren!", menu_help: "❓ Hilfe / Verknüpfungen", btn_panel: "Panel", conn_err: "Server offline? Starten Sie die App!", menu_update: "🔄 Update Prüfen", tip_title: "VERKNÜPFUNGEN:", tip_1: "SHIFT: Video", tip_2: "ALT: Audio", tip_3: "CTRL: Bild" },
it: { ...EN_BASE, title: "Downloader Locale PRO", sc_vid: "SHIFT + Tasto Destro", sc_aud: "ALT + Tasto Destro", sc_img: "CTRL + Tasto Destro", pro_tip: "💡 SUGGERIMENTO: Usa scorciatoie + Tasto Destro sulla miniatura per scaricare subito!", err_old_ver: "⚠️ Richiede Nuovo Server Universale!", univ_note: "NOTA: Il nuovo server è UNIVERSALE. Aggiorna!", menu_help: "❓ Aiuto / Scorciatoie", btn_panel: "Pannello", conn_err: "Server offline? Avvia l'app!", menu_update: "🔄 Controlla Aggiornamento", tip_title: "SCORCIATOIE:", tip_1: "SHIFT: Video", tip_2: "ALT: Audio", tip_3: "CTRL: Immagine" },
zh: { ...EN_BASE, title: "本地下载器 PRO", sc_vid: "SHIFT + 右键", sc_aud: "ALT + 右键", sc_img: "CTRL + 右键", pro_tip: "💡以此提示:使用快捷键+右键点击缩略图即可直接下载!", err_old_ver: "⚠️ 需要新的通用服务器!", univ_note: "注意:新服务器是通用的。请更新!", menu_help: "❓ 帮助/快捷方式", btn_panel: "面板", conn_err: "服务器离线?启动应用程序!", menu_update: "🔄 检查更新", tip_title: "快捷键:", tip_1: "SHIFT: 视频", tip_2: "ALT: 音频", tip_3: "CTRL: 图片" },
ja: { ...EN_BASE, title: "ローカルダウンローダー PRO", sc_vid: "SHIFT + 右クリック", sc_aud: "ALT + 右クリック", sc_img: "CTRL + 右クリック", pro_tip: "💡 ヒント: ショートカットキーを押しながらサムネイルを右クリックすると、すぐにダウンロードできます!", err_old_ver: "⚠️ 新しいユニバーサルサーバーが必要です!", univ_note: "注意:新しいサーバーはユニバーサルです。更新してください!", menu_help: "❓ ヘルプ / ショートカット", btn_panel: "パネル", conn_err: "サーバーオフライン?アプリを起動!", menu_update: "🔄 更新を確認", tip_title: "ショートカット:", tip_1: "SHIFT: ビデオ", tip_2: "ALT: オーディオ", tip_3: "CTRL: 画像" },
ko: { ...EN_BASE, title: "로컬 다운로더 PRO", sc_vid: "SHIFT + 우클릭", sc_aud: "ALT + 우클릭", sc_img: "CTRL + 우클릭", pro_tip: "💡 팁: 단축키를 누른 상태에서 썸네일을 우클릭하면 즉시 다운로드됩니다!", err_old_ver: "⚠️ 새로운 유니버설 서버 필요!", univ_note: "참고: 새 서버는 범용입니다. 업데이트하세요!", menu_help: "❓ 도움말 / 단축키", btn_panel: "패널", conn_err: "서버 오프라인? 앱 실행!", menu_update: "🔄 업데이트 확인", tip_title: "단축키:", tip_1: "SHIFT: 비디오", tip_2: "ALT: 오디오", tip_3: "CTRL: 이미지" }
};
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();
// --- STATE ---
const state = { uiMode: GM_getValue("yt_dl_uiMode", 1), stats: {}, items: [], activeTab: 'dl' };
const imgCache = {};
let lastHtml = '';
let isServerOnline = false;
let isProcessingClick = 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 bRect = container.getBoundingClientRect();
container.style.bottom = 'auto'; container.style.right = 'auto';
let startLeft = bubblePos.left;
if(!startLeft || startLeft === 'auto') startLeft = '20px';
let calcTop = parseInt(bubblePos.top);
if (bubblePos.bottom && bubblePos.bottom !== 'auto') {
const winH = window.innerHeight;
const bottomVal = parseInt(bubblePos.bottom);
calcTop = winH - bottomVal - 460;
} else {
if (!calcTop) calcTop = 60;
}
if (calcTop < 10) calcTop = 10;
if (calcTop > window.innerHeight - 100) calcTop = window.innerHeight - 450;
container.style.left = startLeft;
container.style.top = calcTop + 'px';
}
}
};
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); }};
// --- HELPERS ---
const cleanFileName = (name) => name.replace(/[^a-z0-9\u00a0-\uffff _-]/gi, '_').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 getImgFromContext = (el) => {
if (!el) return null;
if (el.tagName === 'IMG') return el;
let img = el.querySelector('img');
if (img) return img;
let link = el.closest('a');
if (link) img = link.querySelector('img');
if (img) return img;
let parent = el.parentElement;
for(let i=0; i<5 && parent; i++) {
img = parent.querySelector('img');
if(img) return img;
parent = parent.parentElement;
}
return null;
};
// --- SMART GRABBER ---
const findMediaUrl = (target, mode) => {
let foundUrl = null, foundThumb = null, foundTitle = null;
// 1. Image Specific Logic
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 };
}
}
}
// 2. Logic for Video/Audio
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') || target.closest('ytd-grid-video-renderer');
if (container) {
const titleEl = container.querySelector('#video-title');
if (titleEl) foundTitle = titleEl.textContent.trim();
}
}
}
// 3. Fallback to current page
if (!foundUrl) {
foundUrl = window.location.href;
foundTitle = document.title.replace(" - YouTube", "");
}
// 4. MAIN PAGE THUMBNAIL FIX
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`;
}
}
// 5. Ensure Title is unique
if (!foundTitle) foundTitle = "Media";
const uniqueTitle = `${cleanFileName(foundTitle)}_${generateRandomId()}`;
return { url: foundUrl, thumb: foundThumb, title: uniqueTitle };
};
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');
});
// --- DRAG LOGIC ---
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);
};
// --- API ---
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) { } };
// --- BUTTON STATE CHECKER ---
const updateButtonState = () => {
if(!container || state.uiMode !== 2) return;
const path = window.location.pathname;
const isVideoPage = path.startsWith('/watch') || path.startsWith('/shorts/');
['btn-vid', 'btn-aud', 'btn-img'].forEach(id => {
const btn = document.getElementById(id);
if(btn) btn.disabled = !isVideoPage;
});
};
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 response = await gmFetch(`${SERVER_URL}/${endpoint}`, {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ videoUrl: finalUrl, thumb: thumbUrl, type: type, title: title }),
customTimeout: 2500
});
if (!response.ok) {
if (type === 'image') throw new Error("OLD_SERVER");
throw new Error("Generic Error");
}
lastHtml = ''; // Force refresh
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);
}
}
};
// --- CSS ---
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: 50px; height: 50px; background: #d63384; border-radius: 50%; box-shadow: 0 4px 15px rgba(0,0,0,0.5); cursor: move; 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; min-width: 320px; min-height: 200px; max-width: 95vw; max-height: 95vh;
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); }
.yt-dl-btn:disabled { opacity: 0.5; cursor: not-allowed; filter: grayscale(100%); }
.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); } }
`;
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;
// --- 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 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, 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}); }; });
};
// --- UI RENDERER ---
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;
}
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>
<button class="yt-dl-btn btn-orange" id="btn-img">${T.img}</button>
</div>
<div style="display:flex; gap:8px; margin-bottom:5px; text-align:center; font-size:9px; font-weight:bold;">
<div style="flex:1; color:#3ea6ff;">${T.sc_vid}</div>
<div style="flex:1; color:#d63384;">${T.sc_aud}</div>
<div style="flex:1; color:#ff9800;">${T.sc_img}</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'">
<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 class="tip-box" style="margin-top:20px;">
<div style="color:#ffeb3b;font-weight:bold;margin-bottom:5px">${T.tip_title}</div>
<div>${T.tip_1}</div>
<div>${T.tip_2}</div>
<div>${T.tip_3}</div>
</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}<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>`;
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);
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-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();
};
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();
}
});
// --- MENU TAMPERMONKEY ---
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}));
setTimeout(() => renderUI(), 1000);
refreshData();
})();