/** * YouTube Downloader - Desktop Web UI * واجهة المستخدم لتحميل فيديوهات يوتيوب */ class YouTubeDownloaderApp { constructor() { this.ws = null; this.currentVideoUrl = ''; this.isDownloading = false; this.reconnectAttempts = 0; this.initElements(); this.initWebSocket(); this.bindEvents(); } initElements() { // URL Input this.urlInput = document.getElementById('videoUrl'); this.btnPaste = document.getElementById('btnPaste'); this.btnFetchInfo = document.getElementById('btnFetchInfo'); // Video Info this.videoInfoSection = document.getElementById('videoInfoSection'); this.videoThumbnail = document.getElementById('videoThumbnail'); this.videoDuration = document.getElementById('videoDuration'); this.videoTitle = document.getElementById('videoTitle'); this.videoUploader = document.getElementById('videoUploader'); this.videoViews = document.getElementById('videoViews'); this.videoDescription = document.getElementById('videoDescription'); // Download Options this.downloadOptionsSection = document.getElementById('downloadOptionsSection'); this.autoSubtitle = document.getElementById('autoSubtitle'); // Progress this.progressSection = document.getElementById('progressSection'); this.progressTitle = document.getElementById('progressTitle'); this.progressFill = document.getElementById('progressFill'); this.progressPercent = document.getElementById('progressPercent'); this.progressSpeed = document.getElementById('progressSpeed'); this.progressEta = document.getElementById('progressEta'); this.progressMessage = document.getElementById('progressMessage'); // Steps this.steps = { step1: document.getElementById('step1'), step2: document.getElementById('step2'), step3: document.getElementById('step3'), step4: document.getElementById('step4'), }; this.connectors = document.querySelectorAll('.step-connector'); // Buttons this.btnDownloadFull = document.getElementById('btnDownloadFull'); this.btnDownloadSubtitleOnly = document.getElementById('btnDownloadSubtitleOnly'); this.btnDownloadVideoOnly = document.getElementById('btnDownloadVideoOnly'); this.btnCancel = document.getElementById('btnCancel'); this.btnViewDownloads = document.getElementById('btnViewDownloads'); this.btnAntiBanStatus = document.getElementById('btnAntiBanStatus'); // Completed this.completedSection = document.getElementById('completedSection'); this.completedFiles = document.getElementById('completedFiles'); // Modals this.downloadsModal = document.getElementById('downloadsModal'); this.antiBanModal = document.getElementById('antiBanModal'); this.downloadsList = document.getElementById('downloadsList'); this.antiBanInfo = document.getElementById('antiBanInfo'); // Cookies this.cookiesPasteArea = document.getElementById('cookiesPasteArea'); this.btnCookiesSave = document.getElementById('btnCookiesSave'); this.btnCookiesClear = document.getElementById('btnCookiesClear'); this.btnCookiesChooseFile = document.getElementById('btnCookiesChooseFile'); this.btnCookiesPaste = document.getElementById('btnCookiesPaste'); this.cookiesFileInput = document.getElementById('cookiesFileInput'); this.cookiesFileName = document.getElementById('cookiesFileName'); this.cookiesBadge = document.getElementById('cookiesBadge'); this.cookiesStatusBar = document.getElementById('cookiesStatusBar'); this.cookiesStatusText = document.getElementById('cookiesStatusText'); this.cookiesStatusSize = document.getElementById('cookiesStatusSize'); this.cookiesDetectMessage = document.getElementById('cookiesDetectMessage'); } initWebSocket() { if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) { return; } const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws`; this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { console.log('WebSocket connected'); this.reconnectAttempts = 0; // Keep alive this.wsPing = setInterval(() => { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: 'ping' })); } }, 30000); }; this.ws.onmessage = (event) => { try { const data = JSON.parse(event.data); this.handleWebSocketMessage(data); } catch (e) { console.error('WS message parse error:', e); } }; this.ws.onclose = () => { console.log('WebSocket closed'); clearInterval(this.wsPing); this.ws = null; // Reconnect after delay setTimeout(() => { this.reconnectAttempts++; if (this.reconnectAttempts < 10) { this.initWebSocket(); } }, 3000); }; this.ws.onerror = (error) => { console.error('WebSocket error:', error); }; } handleWebSocketMessage(data) { switch (data.type) { case 'progress': this.updateProgress(data); break; case 'download_complete': this.onDownloadComplete(data); break; case 'download_error': this.onDownloadError(data.error); break; case 'pong': break; } } bindEvents() { // Paste button this.btnPaste.addEventListener('click', () => this.pasteFromClipboard()); // Fetch info this.btnFetchInfo.addEventListener('click', () => this.fetchVideoInfo()); this.urlInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') this.fetchVideoInfo(); }); // Download buttons this.btnDownloadFull.addEventListener('click', () => this.startDownload('full')); this.btnDownloadSubtitleOnly.addEventListener('click', () => this.startDownload('subtitle')); this.btnDownloadVideoOnly.addEventListener('click', () => this.startDownload('video')); this.btnCancel.addEventListener('click', () => this.cancelDownload()); // Modals this.btnViewDownloads.addEventListener('click', () => this.showDownloadsModal()); document.getElementById('btnCloseModal').addEventListener('click', () => { this.downloadsModal.style.display = 'none'; }); this.btnAntiBanStatus.addEventListener('click', () => this.showAntiBanModal()); document.getElementById('btnCloseAntiBan').addEventListener('click', () => { this.antiBanModal.style.display = 'none'; }); document.getElementById('btnResetAntiBan').addEventListener('click', () => this.resetAntiBan()); // Close modal on backdrop click this.downloadsModal.addEventListener('click', (e) => { if (e.target === this.downloadsModal) this.downloadsModal.style.display = 'none'; }); this.antiBanModal.addEventListener('click', (e) => { if (e.target === this.antiBanModal) this.antiBanModal.style.display = 'none'; }); // Cookies this.btnCookiesSave.addEventListener('click', () => this.saveCookies()); this.btnCookiesClear.addEventListener('click', () => this.clearCookies()); this.btnCookiesChooseFile.addEventListener('click', () => this.cookiesFileInput.click()); this.btnCookiesPaste.addEventListener('click', () => this.pasteCookiesFromClipboard()); this.cookiesFileInput.addEventListener('change', (e) => this.uploadCookiesFile(e)); this.cookiesPasteArea.addEventListener('input', () => this.detectCookiesContent()); } async pasteFromClipboard() { try { const text = await navigator.clipboard.readText(); this.urlInput.value = text; this.urlInput.focus(); } catch { this.showToast('لا يمكن الوصول إلى الحافظة', 'error'); } } async fetchVideoInfo() { const url = this.urlInput.value.trim(); if (!url) { this.showToast('الرجاء إدخال رابط يوتيوب', 'error'); return; } if (!this.isValidYouTubeUrl(url)) { this.showToast('رابط يوتيوب غير صالح', 'error'); return; } this.currentVideoUrl = url; this.btnFetchInfo.disabled = true; this.btnFetchInfo.innerHTML = ' جاري البحث...'; // Timeout للطلب - لا ننتظر أكثر من 50 ثانية const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 50000); try { const response = await fetch(`/api/video/info?url=${encodeURIComponent(url)}`, { signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'فشل جلب معلومات الفيديو'); } const info = await response.json(); this.displayVideoInfo(info); // إظهار مصدر المعلومات const sourceNames = { 'oembed': 'oEmbed (سريع)', 'piped': 'Piped API', 'invidious': 'Invidious API', 'yt_dlp': 'yt-dlp', }; const sourceLabel = sourceNames[info.info_source] || info.info_source; this.showToast(`تم جلب المعلومات بنجاح عبر ${sourceLabel}`, 'success'); } catch (error) { clearTimeout(timeoutId); if (error.name === 'AbortError') { this.showToast('انتهت مهلة البحث. حاول مرة أخرى أو جرب فيديو آخر.', 'error'); } else { this.showToast(error.message || 'فشل جلب المعلومات', 'error'); } } finally { this.btnFetchInfo.disabled = false; this.btnFetchInfo.innerHTML = ' بحث'; } } displayVideoInfo(info) { this.videoThumbnail.src = info.thumbnail || ''; this.videoDuration.textContent = this.formatDuration(info.duration); this.videoTitle.textContent = info.title; this.videoUploader.textContent = info.uploader || ''; this.videoViews.textContent = info.view_count ? this.formatViews(info.view_count) : ''; this.videoDescription.textContent = info.description || ''; // إظهار مصدر المعلومات بجانب العنوان const sourceNames = { 'oembed': 'oEmbed', 'piped': 'Piped', 'invidious': 'Invidious', 'yt_dlp': 'yt-dlp', }; const sourceBadge = info.info_source ? `[${sourceNames[info.info_source] || info.info_source}]` : ''; if (sourceBadge && !this.videoTitle.textContent.includes(sourceBadge)) { this.videoTitle.textContent = info.title; } this.videoInfoSection.style.display = 'block'; this.downloadOptionsSection.style.display = 'block'; // Smooth scroll to info this.videoInfoSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); } async startDownload(type) { if (!this.currentVideoUrl) { this.showToast('الرجاء البحث عن فيديو أولاً', 'error'); return; } const subtitleLang = document.querySelector('input[name="subtitleLang"]:checked')?.value || 'ar'; const subtitleFormat = document.querySelector('input[name="subtitleFormat"]:checked')?.value || 'srt'; const videoQuality = document.querySelector('input[name="videoQuality"]:checked')?.value || 'best'; this.isDownloading = true; this.resetSteps(); // Show progress section this.progressSection.style.display = 'block'; this.completedSection.style.display = 'none'; this.progressSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); // Disable download buttons this.btnDownloadFull.disabled = true; this.btnDownloadSubtitleOnly.disabled = true; this.btnDownloadVideoOnly.disabled = true; try { if (type === 'full') { const response = await fetch('/api/download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: this.currentVideoUrl, subtitle_lang: subtitleLang, subtitle_format: subtitleFormat, quality: videoQuality, auto_subtitle: this.autoSubtitle.checked, }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'فشل بدء التحميل'); } } else if (type === 'video') { const params = new URLSearchParams({ url: this.currentVideoUrl, quality: videoQuality }); const response = await fetch(`/api/download/video?${params}`, { method: 'POST' }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'فشل تحميل الفيديو'); } const result = await response.json(); this.onDownloadComplete({ video: result.file }); } else { // Subtitle only const params = new URLSearchParams({ url: this.currentVideoUrl, lang: subtitleLang, format: subtitleFormat, auto: this.autoSubtitle.checked, }); const response = await fetch(`/api/download/subtitle?${params}`, { method: 'POST' }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'فشل تحميل الترجمة'); } const result = await response.json(); this.onDownloadComplete({ subtitle: result.file }); } } catch (error) { this.onDownloadError(error.message); } finally { this.isDownloading = false; this.btnDownloadFull.disabled = false; this.btnDownloadSubtitleOnly.disabled = false; } } async cancelDownload() { try { await fetch('/api/cancel', { method: 'POST' }); this.showToast('تم إلغاء التحميل', 'info'); this.progressSection.style.display = 'none'; this.isDownloading = false; this.btnDownloadFull.disabled = false; this.btnDownloadSubtitleOnly.disabled = false; this.btnDownloadVideoOnly.disabled = false; } catch (error) { this.showToast('فشل إلغاء التحميل', 'error'); } } updateProgress(data) { // Update progress bar this.progressFill.style.width = `${data.percent}%`; this.progressPercent.textContent = `${Math.round(data.percent)}%`; this.progressSpeed.textContent = data.speed || ''; this.progressEta.textContent = data.eta ? `الوقت المتبقي: ${data.eta}` : ''; // إظهار مصدر التحميل مع الرسالة let sourceLabel = ''; if (data.source) { const sourceNames = { 'invidious': 'Invidious', 'invidious_direct': 'Invidious', 'invidious_adaptive': 'Invidious', 'yt_dlp': 'YouTube', 'cobalt': 'Cobalt', 'piped': 'Piped', }; sourceLabel = sourceNames[data.source] || data.source; } const messageText = data.message || ''; this.progressMessage.textContent = sourceLabel ? `[${sourceLabel}] ${messageText}` : messageText; // Update steps based on status this.updateSteps(data.status); } updateSteps(status) { const stepMap = { 'fetching_info': 1, 'downloading_subtitle': 2, 'waiting_anti_ban': 3, 'downloading_video': 4, 'fallback_download': 4, 'merging_video': 4, }; const currentStep = stepMap[status] || 0; Object.entries(this.steps).forEach(([key, el], index) => { const stepNum = index + 1; el.classList.remove('active', 'completed'); if (stepNum < currentStep) { el.classList.add('completed'); } else if (stepNum === currentStep) { el.classList.add('active'); } }); this.connectors.forEach((conn, index) => { conn.classList.remove('active', 'completed'); if (index + 1 < currentStep) { conn.classList.add('completed'); } else if (index + 1 === currentStep) { conn.classList.add('active'); } }); } resetSteps() { Object.values(this.steps).forEach(el => { el.classList.remove('active', 'completed'); }); this.connectors.forEach(conn => { conn.classList.remove('active', 'completed'); }); this.progressFill.style.width = '0%'; this.progressPercent.textContent = '0%'; this.progressSpeed.textContent = ''; this.progressEta.textContent = ''; this.progressMessage.textContent = ''; } onDownloadComplete(data) { this.progressSection.style.display = 'none'; this.completedSection.style.display = 'block'; this.completedFiles.innerHTML = ''; if (data.video) { this.addFileItem(data.video, 'video'); } if (data.subtitle) { this.addFileItem(data.subtitle, 'subtitle'); } this.showToast('تم التحميل بنجاح!', 'success'); } addFileItem(filepath, type) { const filename = filepath.split('/').pop(); const isVideo = type === 'video'; const item = document.createElement('div'); item.className = 'file-item'; const fileIcon = document.createElement('div'); fileIcon.className = `file-icon ${type}`; const iconEl = document.createElement('i'); iconEl.className = `fas ${isVideo ? 'fa-film' : 'fa-closed-captioning'}`; fileIcon.appendChild(iconEl); const fileInfo = document.createElement('div'); fileInfo.className = 'file-info'; const h4 = document.createElement('h4'); h4.textContent = filename; const p = document.createElement('p'); p.textContent = isVideo ? 'ملف فيديو' : 'ملف ترجمة'; fileInfo.appendChild(h4); fileInfo.appendChild(p); const fileActions = document.createElement('div'); fileActions.className = 'file-actions'; const saveBtn = document.createElement('button'); saveBtn.className = 'btn-save'; const saveIcon = document.createElement('i'); saveIcon.className = 'fas fa-save'; saveBtn.appendChild(saveIcon); saveBtn.appendChild(document.createTextNode(' حفظ باسم')); saveBtn.addEventListener('click', () => this.saveFile(filepath)); fileActions.appendChild(saveBtn); item.appendChild(fileIcon); item.appendChild(fileInfo); item.appendChild(fileActions); this.completedFiles.appendChild(item); } async saveFile(filepath) { // فتح نافذة حفظ باسم عبر تحميل الملف const link = document.createElement('a'); link.href = `/api/download/file?path=${encodeURIComponent(filepath)}`; link.download = filepath.split('/').pop(); link.click(); this.showToast('جاري حفظ الملف...', 'info'); } onDownloadError(error) { this.progressSection.style.display = 'none'; this.isDownloading = false; this.btnDownloadFull.disabled = false; this.btnDownloadSubtitleOnly.disabled = false; this.btnDownloadVideoOnly.disabled = false; // رسائل خطأ أكثر وضوحاً let errorMsg = error || 'حدث خطأ أثناء التحميل'; if (typeof errorMsg === 'string') { if (errorMsg.includes('429') || errorMsg.includes('Too Many')) { errorMsg = 'تم حظر الطلبات مؤقتاً. انتظر بضع دقائق ثم حاول مرة أخرى.'; } else if (errorMsg.includes('Video unavailable') || errorMsg.includes('Private')) { errorMsg = 'الفيديو غير متاح أو خاص.'; } else if (errorMsg.includes('timeout') || errorMsg.includes('Timeout')) { errorMsg = 'انتهت مهلة الاتصال. تحقق من الإنترنت وحاول مرة أخرى.'; } } this.showToast(errorMsg, 'error'); } async showDownloadsModal() { try { const response = await fetch('/api/downloads'); const data = await response.json(); this.downloadsList.innerHTML = ''; if (data.files.length === 0) { this.downloadsList.innerHTML = `
لا توجد ملفات محملة