| |
| |
| |
| |
|
|
| class YouTubeDownloaderApp { |
| constructor() { |
| this.ws = null; |
| this.currentVideoUrl = ''; |
| this.isDownloading = false; |
| this.reconnectAttempts = 0; |
|
|
| this.initElements(); |
| this.initWebSocket(); |
| this.bindEvents(); |
| } |
|
|
| initElements() { |
| |
| this.urlInput = document.getElementById('videoUrl'); |
| this.btnPaste = document.getElementById('btnPaste'); |
| this.btnFetchInfo = document.getElementById('btnFetchInfo'); |
|
|
| |
| 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'); |
|
|
| |
| this.downloadOptionsSection = document.getElementById('downloadOptionsSection'); |
| this.autoSubtitle = document.getElementById('autoSubtitle'); |
|
|
| |
| 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'); |
|
|
| |
| this.steps = { |
| step1: document.getElementById('step1'), |
| step2: document.getElementById('step2'), |
| step3: document.getElementById('step3'), |
| step4: document.getElementById('step4'), |
| }; |
| this.connectors = document.querySelectorAll('.step-connector'); |
|
|
| |
| 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'); |
|
|
| |
| this.completedSection = document.getElementById('completedSection'); |
| this.completedFiles = document.getElementById('completedFiles'); |
|
|
| |
| this.downloadsModal = document.getElementById('downloadsModal'); |
| this.antiBanModal = document.getElementById('antiBanModal'); |
| this.downloadsList = document.getElementById('downloadsList'); |
| this.antiBanInfo = document.getElementById('antiBanInfo'); |
|
|
| |
| 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; |
| |
| 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; |
| |
| 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() { |
| |
| this.btnPaste.addEventListener('click', () => this.pasteFromClipboard()); |
|
|
| |
| this.btnFetchInfo.addEventListener('click', () => this.fetchVideoInfo()); |
| this.urlInput.addEventListener('keypress', (e) => { |
| if (e.key === 'Enter') this.fetchVideoInfo(); |
| }); |
|
|
| |
| 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()); |
|
|
| |
| 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()); |
|
|
| |
| 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'; |
| }); |
|
|
| |
| 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 = '<i class="fas fa-spinner fa-spin"></i> <span>جاري البحث...</span>'; |
|
|
| |
| 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 = '<i class="fas fa-search"></i> <span>بحث</span>'; |
| } |
| } |
|
|
| 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'; |
|
|
| |
| 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(); |
|
|
| |
| this.progressSection.style.display = 'block'; |
| this.completedSection.style.display = 'none'; |
| this.progressSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); |
|
|
| |
| 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 { |
| |
| 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) { |
| |
| 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; |
|
|
| |
| 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 = ` |
| <div style="text-align:center; padding:40px; color:var(--text-muted);"> |
| <i class="fas fa-inbox" style="font-size:40px; margin-bottom:12px; display:block;"></i> |
| <p>لا توجد ملفات محملة</p> |
| </div> |
| `; |
| } else { |
| data.files.forEach(file => { |
| const isVideo = file.type === 'video'; |
| const sizeStr = this.formatFileSize(file.size); |
|
|
| const item = document.createElement('div'); |
| item.className = 'download-item'; |
|
|
| const iconDiv = document.createElement('div'); |
| iconDiv.className = 'download-item-icon'; |
| iconDiv.style.background = isVideo ? 'rgba(255,68,68,0.15)' : 'rgba(68,138,255,0.15)'; |
| iconDiv.style.color = isVideo ? 'var(--primary)' : 'var(--info)'; |
| const iconI = document.createElement('i'); |
| iconI.className = `fas ${isVideo ? 'fa-film' : 'fa-closed-captioning'}`; |
| iconDiv.appendChild(iconI); |
|
|
| const infoDiv = document.createElement('div'); |
| infoDiv.className = 'download-item-info'; |
| const nameH = document.createElement('h4'); |
| nameH.textContent = file.name; |
| const sizeP = document.createElement('p'); |
| sizeP.textContent = sizeStr; |
| infoDiv.appendChild(nameH); |
| infoDiv.appendChild(sizeP); |
|
|
| const actionsDiv = document.createElement('div'); |
| actionsDiv.className = 'download-item-actions'; |
| const saveBtn = document.createElement('button'); |
| saveBtn.className = 'btn-sm save'; |
| const saveI = document.createElement('i'); |
| saveI.className = 'fas fa-download'; |
| saveBtn.appendChild(saveI); |
| saveBtn.addEventListener('click', () => this.saveFile(file.path)); |
| const delBtn = document.createElement('button'); |
| delBtn.className = 'btn-sm delete'; |
| const delI = document.createElement('i'); |
| delI.className = 'fas fa-trash'; |
| delBtn.appendChild(delI); |
| delBtn.addEventListener('click', () => this.deleteFile(file.path)); |
| actionsDiv.appendChild(saveBtn); |
| actionsDiv.appendChild(delBtn); |
|
|
| item.appendChild(iconDiv); |
| item.appendChild(infoDiv); |
| item.appendChild(actionsDiv); |
|
|
| this.downloadsList.appendChild(item); |
| }); |
| } |
|
|
| this.downloadsModal.style.display = 'flex'; |
|
|
| } catch (error) { |
| this.showToast('فشل تحميل قائمة الملفات', 'error'); |
| } |
| } |
|
|
| async deleteFile(filepath) { |
| try { |
| await fetch(`/api/download/file?path=${encodeURIComponent(filepath)}`, { method: 'DELETE' }); |
| this.showToast('تم حذف الملف', 'success'); |
| this.showDownloadsModal(); |
| } catch { |
| this.showToast('فشل حذف الملف', 'error'); |
| } |
| } |
|
|
| async showAntiBanModal() { |
| try { |
| const antiBanResponse = await fetch('/api/anti-ban/status'); |
| const data = await antiBanResponse.json(); |
|
|
| this.antiBanInfo.innerHTML = ` |
| <div class="anti-ban-item"> |
| <label>استراتيجية التحميل</label> |
| <span style="font-size:11px;">Cobalt → Piped → Invidious → yt-dlp</span> |
| </div> |
| <div class="anti-ban-item"> |
| <label>عدد الطلبات</label> |
| <span>${data.request_count}</span> |
| </div> |
| <div class="anti-ban-item"> |
| <label>محاولات فاشلة</label> |
| <span class="${data.failed_attempts > 0 ? 'status-warning' : 'status-active'}">${data.failed_attempts}</span> |
| </div> |
| <div class="anti-ban-item"> |
| <label>حالة الجلسة</label> |
| <span class="${data.session_active ? 'status-active' : 'status-warning'}"> |
| ${data.session_active ? 'نشطة' : 'تحتاج تبريد'} |
| </span> |
| </div> |
| <div class="anti-ban-item"> |
| <label>العميل الحالي (yt-dlp)</label> |
| <span>${data.current_client || 'غير محدد'}</span> |
| </div> |
| <div class="anti-ban-item"> |
| <label>الكوكيز</label> |
| <span class="${data.cookies_active ? 'status-active' : 'status-warning'}"> |
| ${data.cookies_active ? '✅ نشطة' : '❌ غير نشطة'} |
| </span> |
| </div> |
| `; |
|
|
| this.antiBanModal.style.display = 'flex'; |
|
|
| } catch { |
| this.showToast('فشل جلب حالة الحماية', 'error'); |
| } |
| } |
|
|
| async resetAntiBan() { |
| try { |
| await fetch('/api/anti-ban/reset', { method: 'POST' }); |
| this.showToast('تم إعادة تعيين الجلسة', 'success'); |
| this.showAntiBanModal(); |
| } catch { |
| this.showToast('فشل إعادة التعيين', 'error'); |
| } |
| } |
|
|
| |
| async updateCookiesStatus() { |
| try { |
| const response = await fetch('/api/cookies/status'); |
| const info = await response.json(); |
|
|
| if (info.active && info.has_youtube) { |
| this.cookiesBadge.textContent = 'نشطة ✓'; |
| this.cookiesBadge.className = 'badge badge-active'; |
| this.cookiesStatusBar.style.display = 'flex'; |
| this.cookiesStatusBar.className = 'cookies-status'; |
| this.cookiesStatusText.textContent = `🍪 كوكيز يوتيوب نشطة — ${info.lines} سطر`; |
| this.cookiesStatusSize.textContent = info.size > 0 ? this.formatFileSize(info.size) : ''; |
| } else { |
| this.cookiesBadge.textContent = 'غير نشطة'; |
| this.cookiesBadge.className = 'badge badge-inactive'; |
| this.cookiesStatusBar.style.display = 'none'; |
| } |
| } catch { |
| this.cookiesBadge.textContent = 'خطأ'; |
| } |
| } |
|
|
| async pasteCookiesFromClipboard() { |
| try { |
| const text = await navigator.clipboard.readText(); |
| if (text) { |
| this.cookiesPasteArea.value = text; |
| this.detectCookiesContent(); |
| this.showToast('تم لصق المحتوى من الحافظة', 'info'); |
| } |
| } catch { |
| this.showToast('لا يمكن الوصول إلى الحافظة', 'error'); |
| } |
| } |
|
|
| detectCookiesContent() { |
| const content = this.cookiesPasteArea.value.trim(); |
| if (!content) { |
| this.cookiesDetectMessage.style.display = 'none'; |
| return; |
| } |
|
|
| const hasYoutube = content.split('\n').some(line => |
| line.includes('youtube.com') || line.includes('.youtube.com') |
| ); |
|
|
| if (hasYoutube) { |
| this.cookiesDetectMessage.className = 'cookies-detect success'; |
| this.cookiesDetectMessage.innerHTML = '🍪 تم الكشف عن كوكيز يوتيوب ✓ جاهز للحفظ'; |
| this.cookiesDetectMessage.style.display = 'block'; |
| } else { |
| this.cookiesDetectMessage.className = 'cookies-detect error'; |
| this.cookiesDetectMessage.innerHTML = '⚠️ هذا المحتوى لا يبدو أنه كوكيز يوتيوب'; |
| this.cookiesDetectMessage.style.display = 'block'; |
| } |
| } |
|
|
| async saveCookies() { |
| const content = this.cookiesPasteArea.value.trim(); |
| if (!content) { |
| this.showToast('الرجاء لصق محتويات الكوكيز أولاً', 'error'); |
| return; |
| } |
|
|
| this.btnCookiesSave.disabled = true; |
| this.btnCookiesSave.innerHTML = '<i class="fas fa-spinner fa-spin"></i> جاري الحفظ...'; |
|
|
| try { |
| const response = await fetch('/api/cookies/set', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ content }), |
| }); |
|
|
| if (response.ok) { |
| this.showToast('✅ تم حفظ الكوكيز بنجاح! حد 429 مرتفع الآن', 'success'); |
| this.cookiesPasteArea.value = ''; |
| this.cookiesDetectMessage.style.display = 'none'; |
| await this.updateCookiesStatus(); |
| } else { |
| const err = await response.json(); |
| this.showToast(err.detail || 'فشل حفظ الكوكيز', 'error'); |
| } |
| } catch { |
| this.showToast('خطأ في الاتصال بالخادم', 'error'); |
| } finally { |
| this.btnCookiesSave.disabled = false; |
| this.btnCookiesSave.innerHTML = '<i class="fas fa-save"></i> حفظ الكوكيز'; |
| } |
| } |
|
|
| async clearCookies() { |
| try { |
| const response = await fetch('/api/cookies/remove', { method: 'DELETE' }); |
| if (response.ok) { |
| this.showToast('🗑️ تم حذف الكوكيز', 'info'); |
| await this.updateCookiesStatus(); |
| } |
| } catch { |
| this.showToast('فشل حذف الكوكيز', 'error'); |
| } |
| } |
|
|
| async uploadCookiesFile(event) { |
| const file = event.target.files[0]; |
| if (!file) return; |
|
|
| this.cookiesFileName.textContent = file.name; |
|
|
| const formData = new FormData(); |
| formData.append('file', file); |
|
|
| try { |
| const response = await fetch('/api/cookies/upload', { |
| method: 'POST', |
| body: formData, |
| }); |
|
|
| if (response.ok) { |
| this.showToast('✅ تم رفع الكوكيز بنجاح!', 'success'); |
| await this.updateCookiesStatus(); |
| } else { |
| const err = await response.json(); |
| this.showToast(err.detail || 'فشل رفع الكوكيز', 'error'); |
| } |
| } catch { |
| this.showToast('خطأ في رفع الملف', 'error'); |
| } |
|
|
| |
| this.cookiesFileInput.value = ''; |
| } |
|
|
| |
| isValidYouTubeUrl(url) { |
| const patterns = [ |
| /^https?:\/\/(www\.)?youtube\.com\/watch\?v=/, |
| /^https?:\/\/youtu\.be\//, |
| /^https?:\/\/(www\.)?youtube\.com\/shorts\//, |
| /^https?:\/\/(www\.)?youtube\.com\/embed\//, |
| /^https?:\/\/m\.youtube\.com\/watch\?v=/, |
| ]; |
| return patterns.some(pattern => pattern.test(url)); |
| } |
|
|
| formatDuration(seconds) { |
| if (!seconds) return '0:00'; |
| const h = Math.floor(seconds / 3600); |
| const m = Math.floor((seconds % 3600) / 60); |
| const s = seconds % 60; |
| if (h > 0) return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; |
| return `${m}:${s.toString().padStart(2, '0')}`; |
| } |
|
|
| formatViews(count) { |
| if (!count) return '0'; |
| if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`; |
| if (count >= 1000) return `${(count / 1000).toFixed(1)}K`; |
| return count.toString(); |
| } |
|
|
| formatFileSize(bytes) { |
| if (bytes >= 1073741824) return `${(bytes / 1073741824).toFixed(2)} GB`; |
| if (bytes >= 1048576) return `${(bytes / 1048576).toFixed(1)} MB`; |
| if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`; |
| return `${bytes} B`; |
| } |
|
|
| showToast(message, type = 'info') { |
| |
| const existing = document.querySelector('.toast'); |
| if (existing) existing.remove(); |
|
|
| const toast = document.createElement('div'); |
| toast.className = `toast ${type}`; |
|
|
| const icons = { |
| success: 'fa-check-circle', |
| error: 'fa-exclamation-circle', |
| info: 'fa-info-circle', |
| }; |
|
|
| toast.innerHTML = `<i class="fas ${icons[type] || icons.info}"></i> ${message}`; |
| document.body.appendChild(toast); |
|
|
| requestAnimationFrame(() => { |
| toast.classList.add('show'); |
| }); |
|
|
| setTimeout(() => { |
| toast.classList.remove('show'); |
| setTimeout(() => toast.remove(), 300); |
| }, 3000); |
| } |
| } |
|
|
| |
| let app; |
| document.addEventListener('DOMContentLoaded', () => { |
| app = new YouTubeDownloaderApp(); |
| |
| setTimeout(() => app.updateCookiesStatus(), 1000); |
| }); |
|
|