// ==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 * // @downloadURL https://update.greasyfork.icu/scripts/557579/YouTube%20Downloader%20-%20Local%20Server%20Interface%20-%20PRO.user.js // @updateURL https://update.greasyfork.icu/scripts/557579/YouTube%20Downloader%20-%20Local%20Server%20Interface%20-%20PRO.meta.js // ==/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 `
Empty list
`; 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 = `${i.status}`; if(i.status==='auth_error') statusHtml = `${T.login_err}`; if(i.status==='cancelled') statusHtml = `${T.cancel}`; let progressHtml = ''; if (i.status === 'downloading' || i.status === 'recording') { let pct = i.progress ? i.progress : 0; if(i.status === 'recording') pct = 100; progressHtml = `
${i.status === 'recording' ? 'REC ●' : pct + '%'}
`; } let actions = ''; if(i.status === 'downloading' || i.status === 'queued' || i.status === 'recording') { actions = ``; } else if(i.status === 'finished') { actions = ` `; } else if(i.status === 'error' || i.status === 'cancelled' || i.status === 'auth_error') { actions = ``; } 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 = ``; return `
${imgHTML}
${isAud?'🎵':(isImg?'🖼️':'🎬')} ${i.title||'...'}
${tagTxt} ${statusHtml}
${progressHtml}
${actions}
`; }).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(`${T.queue}: ${state.stats.in_progress||0} ${T.done}: ${state.stats.finished||0} ${T.err}: ${state.stats.errors||0}`); }; 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(`
`); document.getElementById('yt-dl-bubble-btn').onclick = () => { if(container.dataset.moved !== "true") setUIMode(2); }; return; } const dlContent = `
${T.sc_vid}
${T.sc_aud}
${T.sc_img}
${T.pro_tip}
${T.queue}: ...
${generateListHTML()}
`; const batchContent = `
`; const helpContent = `

${T.help_title}

${T.help_s1}
${T.help_s2}
${T.help_s3}

${T.help_warn}

${T.univ_note}
${T.tip_title}
${T.tip_1}
${T.tip_2}
${T.tip_3}
${T.help_login_err}
${T.back}
`; 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 => `
${c.name}
`).join(''); const supContent = `
${T.sup_title}
${T.sup_desc}
${T.lbl_pix}
${T.wallet_title}
${cryptoList} PayPal
`; let activeContent = dlContent; if (state.activeTab === 'sup') activeContent = supContent; if (state.activeTab === 'help') activeContent = helpContent; if (state.activeTab === 'batch') activeContent = batchContent; const panelHtml = `
${T.title}
[?]
${T.tab_dl}
${T.tab_batch}
${T.tab_sup}
${T.tab_help}
${activeContent}
`; 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(); })();