Spaces:
Sleeping
Sleeping
| // Universal Media Downloader - Enhanced JavaScript | |
| // Professional media downloader with API integration | |
| class UniversalMediaDownloader { | |
| constructor() { | |
| this.apiBaseUrl = '/api'; | |
| this.currentFormats = []; | |
| this.currentMediaInfo = null; | |
| this.isProcessing = false; | |
| this.downloadQueue = []; | |
| this.settings = this.loadSettings(); | |
| this.activeDownloads = new Map(); | |
| this.progressIntervals = new Map(); | |
| this.initializeApp(); | |
| } | |
| initializeApp() { | |
| this.setupEventListeners(); | |
| this.setupKeyboardNavigation(); | |
| this.checkApiHealth(); | |
| this.setupServiceWorker(); | |
| this.loadDownloadHistory(); | |
| this.announceAppReady(); | |
| } | |
| // Settings Management | |
| loadSettings() { | |
| const defaultSettings = { | |
| autoUpdate: true, | |
| privacyMode: false, | |
| defaultQuality: 'best', | |
| theme: 'dark' | |
| }; | |
| try { | |
| const saved = localStorage.getItem('downloader-settings'); | |
| return { ...defaultSettings, ...(saved ? JSON.parse(saved) : {}) }; | |
| } catch { | |
| return defaultSettings; | |
| } | |
| } | |
| saveSettings() { | |
| try { | |
| localStorage.setItem('downloader-settings', JSON.stringify(this.settings)); | |
| this.announceToScreenReader('Settings saved successfully'); | |
| } catch (error) { | |
| console.error('Failed to save settings:', error); | |
| } | |
| } | |
| // API Communication | |
| async checkApiHealth() { | |
| try { | |
| const response = await fetch(`${this.apiBaseUrl}/health`); | |
| const data = await response.json(); | |
| if (data.status === 'healthy') { | |
| this.updateStatus('API connected and ready', 'success'); | |
| this.updateApiInfo(data); | |
| } else { | |
| throw new Error('API not healthy'); | |
| } | |
| } catch (error) { | |
| console.error('API health check failed:', error); | |
| this.showError('Unable to connect to download service. Please refresh the page.'); | |
| } | |
| } | |
| async getFormats(url) { | |
| try { | |
| const response = await fetch(`${this.apiBaseUrl}/formats`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ url }) | |
| }); | |
| const data = await response.json(); | |
| if (!response.ok) { | |
| // Check if we have fallback download options | |
| if (data.fallback && data.download_options) { | |
| return { | |
| success: false, | |
| fallback: true, | |
| download_options: data.download_options, | |
| basic_info: data.basic_info, | |
| message: data.message, | |
| instruction: data.instruction | |
| }; | |
| } | |
| throw new Error(data.message || data.error || 'Failed to get formats'); | |
| } | |
| return data; | |
| } catch (error) { | |
| console.error('Get formats error:', error); | |
| throw error; | |
| } | |
| } | |
| async startDownload(url, formatId) { | |
| try { | |
| const downloadId = `dl_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; | |
| const response = await fetch(`${this.apiBaseUrl}/download`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify({ | |
| url, | |
| format_id: formatId, | |
| download_id: downloadId | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (!response.ok) { | |
| throw new Error(data.message || data.error || 'Failed to start download'); | |
| } | |
| // Start monitoring progress | |
| this.startProgressMonitoring(downloadId); | |
| return { ...data, downloadId }; | |
| } catch (error) { | |
| console.error('Start download error:', error); | |
| throw error; | |
| } | |
| } | |
| async getProgress(downloadId) { | |
| try { | |
| const response = await fetch(`${this.apiBaseUrl}/progress/${downloadId}`); | |
| const data = await response.json(); | |
| return data; | |
| } catch (error) { | |
| console.error('Get progress error:', error); | |
| return null; | |
| } | |
| } | |
| async getSupportedPlatforms() { | |
| try { | |
| const response = await fetch(`${this.apiBaseUrl}/supported-platforms`); | |
| const data = await response.json(); | |
| return data; | |
| } catch (error) { | |
| console.error('Get platforms error:', error); | |
| return null; | |
| } | |
| } | |
| async updateYtdlp() { | |
| try { | |
| this.updateStatus('Checking for yt-dlp updates...', 'info'); | |
| const response = await fetch(`${this.apiBaseUrl}/update`, { | |
| method: 'POST' | |
| }); | |
| const data = await response.json(); | |
| if (data.success) { | |
| this.updateStatus(`yt-dlp updated to version: ${data.version}`, 'success'); | |
| this.announceToScreenReader(`yt-dlp updated to version ${data.version}`); | |
| } else { | |
| this.updateStatus(data.message || 'Update check completed', 'info'); | |
| } | |
| return data; | |
| } catch (error) { | |
| console.error('Update yt-dlp error:', error); | |
| this.showError('Failed to update yt-dlp'); | |
| throw error; | |
| } | |
| } | |
| // Progress Monitoring | |
| startProgressMonitoring(downloadId) { | |
| if (this.progressIntervals.has(downloadId)) { | |
| clearInterval(this.progressIntervals.get(downloadId)); | |
| } | |
| const interval = setInterval(async () => { | |
| const progress = await this.getProgress(downloadId); | |
| if (progress && progress.success) { | |
| this.updateDownloadProgress(progress); | |
| if (progress.progress.status === 'finished' || progress.progress.status === 'error') { | |
| clearInterval(interval); | |
| this.progressIntervals.delete(downloadId); | |
| if (progress.progress.status === 'finished') { | |
| this.onDownloadComplete(downloadId, progress.progress); | |
| } else { | |
| this.onDownloadError(downloadId, progress.progress); | |
| } | |
| } | |
| } | |
| }, 1000); | |
| this.progressIntervals.set(downloadId, interval); | |
| } | |
| updateDownloadProgress(progressData) { | |
| const { downloadId, progress } = progressData; | |
| // Update queue item | |
| const queueItem = document.querySelector(`[data-download-id="${downloadId}"]`); | |
| if (queueItem) { | |
| this.updateQueueItem(queueItem, progress); | |
| } | |
| // Update global progress | |
| if (progress.status === 'downloading') { | |
| this.showProgress(progress.percentage, progress.speed, progress.eta); | |
| } | |
| } | |
| // UI Updates | |
| updateStatus(message, type = 'info') { | |
| const statusContainer = document.getElementById('status-container'); | |
| const statusIcon = type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️'; | |
| statusContainer.innerHTML = ` | |
| <div class="status-item"> | |
| <span class="status-icon">${statusIcon}</span> | |
| <span class="status-text">${message}</span> | |
| </div> | |
| `; | |
| this.announceToScreenReader(message); | |
| } | |
| showError(message) { | |
| const errorSection = document.getElementById('error-section'); | |
| const errorContainer = document.getElementById('error-messages'); | |
| errorContainer.innerHTML = ` | |
| <div class="error-item"> | |
| <span class="error-icon">❌</span> | |
| <span class="error-text">${message}</span> | |
| </div> | |
| `; | |
| errorSection.style.display = 'block'; | |
| this.announceToScreenReader(`Error: ${message}`); | |
| } | |
| hideError() { | |
| const errorSection = document.getElementById('error-section'); | |
| errorSection.style.display = 'none'; | |
| } | |
| showProgress(percentage, speed = null, eta = null) { | |
| const progressSection = document.getElementById('progress-section'); | |
| const progressBar = document.getElementById('progress-bar'); | |
| const progressPercentage = document.getElementById('progress-percentage'); | |
| const progressSpeed = document.getElementById('progress-speed'); | |
| const progressTitle = document.getElementById('progress-title'); | |
| progressSection.style.display = 'block'; | |
| progressBar.value = percentage; | |
| progressPercentage.textContent = `${Math.round(percentage)}%`; | |
| if (speed) { | |
| progressSpeed.textContent = this.formatSpeed(speed); | |
| } | |
| if (eta) { | |
| progressTitle.textContent = `Downloading... ETA: ${this.formatTime(eta)}`; | |
| } | |
| } | |
| hideProgress() { | |
| const progressSection = document.getElementById('progress-section'); | |
| progressSection.style.display = 'none'; | |
| } | |
| displayMediaInfo(info) { | |
| const section = document.getElementById('media-info-section'); | |
| const title = document.getElementById('media-title'); | |
| const uploader = document.getElementById('media-uploader'); | |
| const platform = document.getElementById('media-platform'); | |
| const duration = document.getElementById('media-duration'); | |
| const views = document.getElementById('media-views'); | |
| const likes = document.getElementById('media-likes'); | |
| const thumb = document.getElementById('media-thumb'); | |
| const placeholder = document.getElementById('thumb-placeholder'); | |
| // Update content | |
| title.textContent = info.title || 'Unknown Title'; | |
| uploader.textContent = info.uploader || 'Unknown Uploader'; | |
| platform.textContent = info.platform || 'Unknown'; | |
| platform.className = `platform-badge platform-${(info.platform || '').toLowerCase()}`; | |
| duration.textContent = info.duration ? this.formatTime(info.duration) : ''; | |
| views.textContent = info.view_count ? `${this.formatNumber(info.view_count)} views` : ''; | |
| likes.textContent = info.like_count ? `${this.formatNumber(info.like_count)} likes` : ''; | |
| // Handle thumbnail | |
| if (info.thumbnail) { | |
| thumb.src = info.thumbnail; | |
| thumb.alt = `Thumbnail for ${info.title}`; | |
| thumb.style.display = 'block'; | |
| placeholder.style.display = 'none'; | |
| } else { | |
| thumb.style.display = 'none'; | |
| placeholder.style.display = 'flex'; | |
| } | |
| section.style.display = 'block'; | |
| } | |
| displayFormats(formats) { | |
| const section = document.getElementById('formats-section'); | |
| const formatList = document.getElementById('format-list'); | |
| // Clear existing formats | |
| formatList.innerHTML = ''; | |
| // Sort formats by quality and type | |
| const sortedFormats = this.sortFormats(formats); | |
| sortedFormats.forEach((format, index) => { | |
| const formatElement = this.createFormatElement(format, index); | |
| formatList.appendChild(formatElement); | |
| }); | |
| section.style.display = 'block'; | |
| this.currentFormats = sortedFormats; | |
| } | |
| createFormatElement(format, index) { | |
| const element = document.createElement('div'); | |
| element.className = 'format-item'; | |
| element.setAttribute('role', 'listitem'); | |
| element.setAttribute('data-format-id', format.id); | |
| element.setAttribute('data-format-type', format.type); | |
| element.setAttribute('tabindex', '0'); | |
| const fileSize = format.filesize ? this.formatFileSize(format.filesize) : 'Unknown size'; | |
| const quality = format.width && format.height ? `${format.height}p` : format.format_note || 'Unknown'; | |
| const codec = format.vcodec !== 'none' ? format.vcodec.split('.')[0] : format.acodec.split('.')[0]; | |
| element.innerHTML = ` | |
| <div class="format-info"> | |
| <div class="format-title">${this.getFormatTitle(format)}</div> | |
| <div class="format-details">${format.ext.toUpperCase()} • ${quality} • ${codec} • ${fileSize}</div> | |
| </div> | |
| <div class="format-actions"> | |
| <button class="primary-btn download-btn" data-format-id="${format.id}" aria-label="Download ${this.getFormatTitle(format)}"> | |
| <span class="btn-text">Download</span> | |
| </button> | |
| </div> | |
| `; | |
| // Add event listeners | |
| element.addEventListener('click', (e) => { | |
| if (!e.target.closest('.download-btn')) { | |
| this.selectFormat(format); | |
| } | |
| }); | |
| element.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter' || e.key === ' ') { | |
| e.preventDefault(); | |
| this.selectFormat(format); | |
| } | |
| }); | |
| const downloadBtn = element.querySelector('.download-btn'); | |
| downloadBtn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| this.handleDownload(format, downloadBtn); | |
| }); | |
| return element; | |
| } | |
| createQueueItem(progress) { | |
| const element = document.createElement('div'); | |
| element.className = 'queue-item'; | |
| element.setAttribute('data-download-id', progress.download_id); | |
| const fileName = progress.filename ? progress.filename.split('/').pop() : 'Downloading...'; | |
| element.innerHTML = ` | |
| <div class="queue-thumbnail"> | |
| <svg class="placeholder-icon" viewBox="0 0 24 24"> | |
| <path d="M8.5,13.5L11,16.51L14.5,12L19,18H5M21,19V5C21,3.89 20.1,3 19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19Z"/> | |
| </svg> | |
| </div> | |
| <div class="queue-info"> | |
| <div class="queue-title">${fileName}</div> | |
| <div class="queue-url">${progress.url || 'Downloading...'}</div> | |
| </div> | |
| <div class="queue-progress"> | |
| <progress class="progress-bar" value="0" max="100"></progress> | |
| <div class="queue-status">Starting...</div> | |
| </div> | |
| `; | |
| return element; | |
| } | |
| updateQueueItem(element, progress) { | |
| const progressBar = element.querySelector('.progress-bar'); | |
| const statusText = element.querySelector('.queue-status'); | |
| if (progress.status === 'downloading') { | |
| progressBar.value = progress.percentage || 0; | |
| statusText.textContent = `${Math.round(progress.percentage || 0)}%`; | |
| } else if (progress.status === 'finished') { | |
| element.classList.add('completed'); | |
| statusText.textContent = 'Complete'; | |
| progressBar.value = 100; | |
| } else if (progress.status === 'error') { | |
| element.classList.add('error'); | |
| statusText.textContent = 'Error'; | |
| } | |
| } | |
| // Event Handlers | |
| setupEventListeners() { | |
| // Form submission | |
| const form = document.getElementById('download-form'); | |
| form.addEventListener('submit', (e) => this.handleFormSubmit(e)); | |
| // URL input | |
| const urlInput = document.getElementById('url-input'); | |
| urlInput.addEventListener('input', () => this.validateURL()); | |
| urlInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| e.preventDefault(); | |
| this.handleFormSubmit(e); | |
| } | |
| }); | |
| // Format filters | |
| const filterBtns = document.querySelectorAll('.filter-btn'); | |
| filterBtns.forEach(btn => { | |
| btn.addEventListener('click', (e) => this.handleFilterClick(e)); | |
| }); | |
| // Settings modal | |
| const settingsBtn = document.getElementById('settings-btn'); | |
| const settingsModal = document.getElementById('settings-modal'); | |
| const modalClose = document.querySelector('.modal-close'); | |
| const saveSettingsBtn = document.getElementById('save-settings'); | |
| settingsBtn.addEventListener('click', () => this.openSettings()); | |
| modalClose.addEventListener('click', () => this.closeSettings()); | |
| settingsModal.addEventListener('click', (e) => { | |
| if (e.target === settingsModal) this.closeSettings(); | |
| }); | |
| saveSettingsBtn.addEventListener('click', () => this.saveSettingsAndClose()); | |
| // Update button | |
| const updateBtn = document.getElementById('update-btn'); | |
| updateBtn.addEventListener('click', () => this.handleUpdateYtdlp()); | |
| // Clear queue button | |
| const clearQueueBtn = document.getElementById('clear-queue-btn'); | |
| clearQueueBtn.addEventListener('click', () => this.clearQueue()); | |
| // Bottom navigation | |
| const navItems = document.querySelectorAll('.nav-item'); | |
| navItems.forEach(item => { | |
| item.addEventListener('click', (e) => this.handleNavClick(e)); | |
| }); | |
| // Settings inputs | |
| this.setupSettingsInputs(); | |
| } | |
| setupSettingsInputs() { | |
| const autoUpdate = document.getElementById('auto-update'); | |
| const privacyMode = document.getElementById('privacy-mode'); | |
| const defaultQuality = document.getElementById('default-quality'); | |
| autoUpdate.checked = this.settings.autoUpdate; | |
| privacyMode.checked = this.settings.privacyMode; | |
| defaultQuality.value = this.settings.defaultQuality; | |
| } | |
| async handleFormSubmit(event) { | |
| event.preventDefault(); | |
| if (this.isProcessing) { | |
| return; | |
| } | |
| this.hideError(); | |
| this.hideProgress(); | |
| const url = document.getElementById('url-input').value.trim(); | |
| if (!this.isValidURL(url)) { | |
| this.showError('Please enter a valid URL. Make sure it starts with http:// or https://'); | |
| return; | |
| } | |
| this.setLoadingState(true); | |
| this.updateStatus('Analyzing URL and fetching available formats...', 'info'); | |
| try { | |
| const result = await this.getFormats(url); | |
| // Handle fallback case with download options | |
| if (result.fallback && result.download_options) { | |
| this.displayDownloadOptions(result); | |
| this.updateStatus(result.message || 'Format extraction failed, but direct download options are available.', 'warning'); | |
| return; | |
| } | |
| if (result.success) { | |
| this.currentMediaInfo = { | |
| title: result.title, | |
| uploader: result.uploader, | |
| platform: result.platform, | |
| duration: result.duration, | |
| thumbnail: result.thumbnail, | |
| view_count: result.view_count, | |
| like_count: result.like_count | |
| }; | |
| this.displayMediaInfo(this.currentMediaInfo); | |
| this.displayFormats(result.formats); | |
| this.updateStatus(`Found ${result.formats.length} available formats for ${result.platform}. Select your preferred option below.`, 'success'); | |
| // Add to queue | |
| this.addToQueue(result); | |
| } else { | |
| throw new Error(result.message || result.error || 'Failed to analyze URL'); | |
| } | |
| } catch (error) { | |
| console.error('Form submission error:', error); | |
| this.showError(error.message || 'Failed to analyze URL. Please check if the URL is valid and try again.'); | |
| } finally { | |
| this.setLoadingState(false); | |
| } | |
| } | |
| async handleDownload(format, button) { | |
| if (this.isProcessing) { | |
| return; | |
| } | |
| try { | |
| this.setDownloadButtonState(button, 'downloading'); | |
| this.updateStatus(`Starting download: ${this.getFormatTitle(format)}`, 'info'); | |
| const result = await this.startDownload( | |
| document.getElementById('url-input').value.trim(), | |
| format.id | |
| ); | |
| // Add to active downloads | |
| this.activeDownloads.set(result.downloadId, { format, startTime: Date.now() }); | |
| // Add to queue display | |
| const queueList = document.getElementById('queue-list'); | |
| const queueItem = this.createQueueItem({ | |
| download_id: result.downloadId, | |
| filename: this.getFormatTitle(format), | |
| url: document.getElementById('url-input').value.trim() | |
| }); | |
| queueList.appendChild(queueItem); | |
| this.updateStatus(`Download started: ${this.getFormatTitle(format)}`, 'success'); | |
| this.announceToScreenReader(`Download started: ${this.getFormatTitle(format)}`); | |
| } catch (error) { | |
| console.error('Download error:', error); | |
| this.setDownloadButtonState(button, 'error'); | |
| this.showError(`Download failed: ${error.message}`); | |
| } | |
| } | |
| async handleUpdateYtdlp() { | |
| const updateBtn = document.getElementById('update-btn'); | |
| updateBtn.disabled = true; | |
| updateBtn.style.opacity = '0.6'; | |
| try { | |
| await this.updateYtdlp(); | |
| } catch (error) { | |
| console.error('Update error:', error); | |
| } finally { | |
| updateBtn.disabled = false; | |
| updateBtn.style.opacity = '1'; | |
| } | |
| } | |
| handleFilterClick(event) { | |
| const filter = event.target.dataset.filter; | |
| const buttons = document.querySelectorAll('.filter-btn'); | |
| // Update active filter | |
| buttons.forEach(btn => btn.classList.remove('active')); | |
| event.target.classList.add('active'); | |
| // Filter formats | |
| const formatItems = document.querySelectorAll('.format-item'); | |
| formatItems.forEach(item => { | |
| const formatType = item.dataset.formatType; | |
| if (filter === 'all' || formatType === filter) { | |
| item.style.display = 'flex'; | |
| } else { | |
| item.style.display = 'none'; | |
| } | |
| }); | |
| } | |
| handleNavClick(event) { | |
| const tab = event.currentTarget.dataset.tab; | |
| const items = document.querySelectorAll('.nav-item'); | |
| items.forEach(item => item.classList.remove('active')); | |
| event.currentTarget.classList.add('active'); | |
| // Show/hide relevant sections based on tab | |
| this.showTabContent(tab); | |
| } | |
| showTabContent(tab) { | |
| // This would show/hide different content areas | |
| // For now, we'll just announce the tab change | |
| this.announceToScreenReader(`Switched to ${tab} tab`); | |
| } | |
| selectFormat(format) { | |
| // Highlight selected format | |
| const formatItems = document.querySelectorAll('.format-item'); | |
| formatItems.forEach(item => item.classList.remove('selected')); | |
| const selectedItem = document.querySelector(`[data-format-id="${format.id}"]`); | |
| if (selectedItem) { | |
| selectedItem.classList.add('selected'); | |
| this.announceToScreenReader(`Selected ${this.getFormatTitle(format)}`); | |
| } | |
| } | |
| // Utility Methods | |
| isValidURL(string) { | |
| try { | |
| new URL(string); | |
| return true; | |
| } catch (_) { | |
| return false; | |
| } | |
| } | |
| validateURL() { | |
| const urlInput = document.getElementById('url-input'); | |
| const url = urlInput.value.trim(); | |
| if (url === '') { | |
| urlInput.setCustomValidity(''); | |
| return true; | |
| } | |
| if (!this.isValidURL(url)) { | |
| urlInput.setCustomValidity('Please enter a valid URL'); | |
| urlInput.reportValidity(); | |
| return false; | |
| } | |
| urlInput.setCustomValidity(''); | |
| return true; | |
| } | |
| setLoadingState(isLoading) { | |
| this.isProcessing = isLoading; | |
| const button = document.getElementById('analyze-btn'); | |
| const urlInput = document.getElementById('url-input'); | |
| if (isLoading) { | |
| button.classList.add('loading'); | |
| button.disabled = true; | |
| urlInput.disabled = true; | |
| } else { | |
| button.classList.remove('loading'); | |
| button.disabled = false; | |
| urlInput.disabled = false; | |
| urlInput.focus(); | |
| } | |
| } | |
| setDownloadButtonState(button, state) { | |
| button.disabled = state === 'downloading'; | |
| button.classList.remove('downloading', 'error'); | |
| if (state === 'downloading') { | |
| button.classList.add('downloading'); | |
| button.innerHTML = '<span class="btn-loading">Downloading...</span>'; | |
| } else if (state === 'error') { | |
| button.classList.add('error'); | |
| button.innerHTML = '<span class="btn-text">Error</span>'; | |
| } else { | |
| button.innerHTML = '<span class="btn-text">Download</span>'; | |
| } | |
| } | |
| addToQueue(result) { | |
| // Add media to download queue for easy access | |
| this.downloadQueue.push({ | |
| url: document.getElementById('url-input').value.trim(), | |
| title: result.title, | |
| platform: result.platform, | |
| formats: result.formats, | |
| timestamp: Date.now() | |
| }); | |
| } | |
| clearQueue() { | |
| this.downloadQueue = []; | |
| const queueList = document.getElementById('queue-list'); | |
| queueList.innerHTML = ''; | |
| this.announceToScreenReader('Download queue cleared'); | |
| } | |
| onDownloadComplete(downloadId, progress) { | |
| this.setDownloadCompleted(downloadId); | |
| this.updateStatus('Download completed successfully!', 'success'); | |
| this.announceToScreenReader('Download completed successfully'); | |
| this.hideProgress(); | |
| } | |
| onDownloadError(downloadId, progress) { | |
| this.setDownloadError(downloadId); | |
| this.updateStatus('Download failed. Please try again.', 'error'); | |
| this.announceToScreenReader('Download failed'); | |
| this.hideProgress(); | |
| } | |
| setDownloadCompleted(downloadId) { | |
| const queueItem = document.querySelector(`[data-download-id="${downloadId}"]`); | |
| if (queueItem) { | |
| queueItem.classList.add('completed'); | |
| const statusText = queueItem.querySelector('.queue-status'); | |
| if (statusText) statusText.textContent = 'Complete'; | |
| } | |
| } | |
| setDownloadError(downloadId) { | |
| const queueItem = document.querySelector(`[data-download-id="${downloadId}"]`); | |
| if (queueItem) { | |
| queueItem.classList.add('error'); | |
| const statusText = queueItem.querySelector('.queue-status'); | |
| if (statusText) statusText.textContent = 'Error'; | |
| } | |
| } | |
| // Settings Modal | |
| openSettings() { | |
| const modal = document.getElementById('settings-modal'); | |
| modal.style.display = 'flex'; | |
| this.announceToScreenReader('Settings opened'); | |
| } | |
| closeSettings() { | |
| const modal = document.getElementById('settings-modal'); | |
| modal.style.display = 'none'; | |
| this.announceToScreenReader('Settings closed'); | |
| } | |
| saveSettingsAndClose() { | |
| const autoUpdate = document.getElementById('auto-update').checked; | |
| const privacyMode = document.getElementById('privacy-mode').checked; | |
| const defaultQuality = document.getElementById('default-quality').value; | |
| this.settings = { | |
| ...this.settings, | |
| autoUpdate, | |
| privacyMode, | |
| defaultQuality | |
| }; | |
| this.saveSettings(); | |
| this.closeSettings(); | |
| } | |
| // Formatting Utilities | |
| getFormatTitle(format) { | |
| const type = format.type === 'video' ? 'Video' : 'Audio'; | |
| const quality = format.height ? `${format.height}p` : format.format_note || 'Best'; | |
| return `${type} - ${quality}`; | |
| } | |
| sortFormats(formats) { | |
| // Sort by type (video first), then by quality | |
| return formats.sort((a, b) => { | |
| if (a.type !== b.type) { | |
| return a.type === 'video' ? -1 : 1; | |
| } | |
| if (a.height && b.height) { | |
| return b.height - a.height; | |
| } | |
| return 0; | |
| }); | |
| } | |
| formatFileSize(bytes) { | |
| if (!bytes) return 'Unknown'; | |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(1024)); | |
| return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; | |
| } | |
| formatSpeed(bytesPerSec) { | |
| if (!bytesPerSec) return ''; | |
| return this.formatFileSize(bytesPerSec) + '/s'; | |
| } | |
| formatTime(seconds) { | |
| if (!seconds) return ''; | |
| const hours = Math.floor(seconds / 3600); | |
| const minutes = Math.floor((seconds % 3600) / 60); | |
| const secs = Math.floor(seconds % 60); | |
| if (hours > 0) { | |
| return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; | |
| } | |
| return `${minutes}:${secs.toString().padStart(2, '0')}`; | |
| } | |
| formatNumber(num) { | |
| if (num >= 1000000) { | |
| return (num / 1000000).toFixed(1) + 'M'; | |
| } else if (num >= 1000) { | |
| return (num / 1000).toFixed(1) + 'K'; | |
| } | |
| return num.toString(); | |
| } | |
| updateApiInfo(data) { | |
| // Update any UI elements showing API info | |
| console.log('API Info:', data); | |
| } | |
| // Accessibility | |
| announceToScreenReader(message) { | |
| const announcement = document.createElement('div'); | |
| announcement.setAttribute('aria-live', 'polite'); | |
| announcement.setAttribute('aria-atomic', 'true'); | |
| announcement.className = 'sr-only'; | |
| announcement.textContent = message; | |
| document.body.appendChild(announcement); | |
| setTimeout(() => { | |
| document.body.removeChild(announcement); | |
| }, 1000); | |
| } | |
| setupKeyboardNavigation() { | |
| document.addEventListener('keydown', (e) => { | |
| // Global keyboard shortcuts | |
| if (e.ctrlKey || e.metaKey) { | |
| switch (e.key) { | |
| case 'u': | |
| e.preventDefault(); | |
| document.getElementById('url-input').focus(); | |
| break; | |
| case 's': | |
| e.preventDefault(); | |
| this.openSettings(); | |
| break; | |
| } | |
| } | |
| if (e.key === 'Escape') { | |
| this.closeSettings(); | |
| } | |
| }); | |
| } | |
| // Service Worker | |
| setupServiceWorker() { | |
| if ('serviceWorker' in navigator) { | |
| navigator.serviceWorker.register('/sw.js') | |
| .then(registration => { | |
| console.log('Service Worker registered:', registration); | |
| }) | |
| .catch(error => { | |
| console.log('Service Worker registration failed:', error); | |
| }); | |
| } | |
| } | |
| loadDownloadHistory() { | |
| // Load download history from localStorage | |
| try { | |
| const history = localStorage.getItem('download-history'); | |
| if (history) { | |
| const parsed = JSON.parse(history); | |
| console.log('Loaded download history:', parsed.length, 'items'); | |
| } | |
| } catch (error) { | |
| console.error('Failed to load download history:', error); | |
| } | |
| } | |
| displayDownloadOptions(result) { | |
| const section = document.getElementById('formats-section'); | |
| const formatList = document.getElementById('format-list'); | |
| // Clear existing content | |
| formatList.innerHTML = ''; | |
| // Show basic information if available | |
| if (result.basic_info) { | |
| const info = result.basic_info; | |
| this.currentMediaInfo = { | |
| title: info.title, | |
| uploader: info.uploader, | |
| platform: info.platform, | |
| url: info.url | |
| }; | |
| this.displayMediaInfo(this.currentMediaInfo); | |
| } | |
| // Create download options section | |
| const optionsContainer = document.createElement('div'); | |
| optionsContainer.className = 'download-options-container'; | |
| const titleElement = document.createElement('h3'); | |
| titleElement.textContent = 'Direct File Downloads'; | |
| titleElement.className = 'download-options-title'; | |
| const instructionElement = document.createElement('p'); | |
| instructionElement.textContent = result.instruction || 'Click on any direct file link below to download immediately:'; | |
| instructionElement.className = 'download-instruction'; | |
| optionsContainer.appendChild(titleElement); | |
| optionsContainer.appendChild(instructionElement); | |
| // Create download links for direct files | |
| if (result.download_options && Array.isArray(result.download_options)) { | |
| result.download_options.forEach((option, index) => { | |
| const linkElement = document.createElement('a'); | |
| linkElement.href = option.url; | |
| linkElement.target = '_blank'; | |
| linkElement.rel = 'noopener noreferrer'; | |
| linkElement.className = 'download-link'; | |
| // Create icon based on type | |
| const icon = document.createElement('span'); | |
| icon.className = 'download-icon'; | |
| icon.innerHTML = this.getFileTypeIcon(option.type); | |
| // Create link text with details | |
| const textContainer = document.createElement('div'); | |
| textContainer.className = 'download-text-container'; | |
| const mainText = document.createElement('div'); | |
| mainText.className = 'download-text'; | |
| mainText.textContent = option.description || `${option.type} ${option.quality}`; | |
| const fileInfo = document.createElement('div'); | |
| fileInfo.className = 'download-file-info'; | |
| const fileDetails = []; | |
| if (option.ext && option.ext !== 'unknown') { | |
| fileDetails.push(option.ext.toUpperCase()); | |
| } | |
| if (option.filesize) { | |
| fileDetails.push(option.filesize); | |
| } | |
| if (option.format_id && option.format_id !== 'unknown') { | |
| fileDetails.push(`Format: ${option.format_id}`); | |
| } | |
| fileInfo.textContent = fileDetails.join(' • '); | |
| textContainer.appendChild(mainText); | |
| textContainer.appendChild(fileInfo); | |
| linkElement.appendChild(icon); | |
| linkElement.appendChild(textContainer); | |
| optionsContainer.appendChild(linkElement); | |
| }); | |
| } else { | |
| // Fallback for simple URL links | |
| result.download_options.forEach((url, index) => { | |
| const linkElement = document.createElement('a'); | |
| linkElement.href = url; | |
| linkElement.target = '_blank'; | |
| linkElement.rel = 'noopener noreferrer'; | |
| linkElement.className = 'download-link'; | |
| const icon = document.createElement('span'); | |
| icon.className = 'download-icon'; | |
| icon.innerHTML = this.getDownloadIcon(); | |
| const text = document.createElement('span'); | |
| text.className = 'download-text'; | |
| if (url.includes('youtube.com/watch')) { | |
| text.textContent = `YouTube Direct Link ${index + 1}`; | |
| } else if (url.includes('youtu.be/')) { | |
| text.textContent = `YouTube Short Link ${index + 1}`; | |
| } else { | |
| text.textContent = `Direct Link ${index + 1}`; | |
| } | |
| linkElement.appendChild(icon); | |
| linkElement.appendChild(text); | |
| optionsContainer.appendChild(linkElement); | |
| }); | |
| } | |
| formatList.appendChild(optionsContainer); | |
| section.style.display = 'block'; | |
| // Update current formats to empty since we don't have format-specific downloads | |
| this.currentFormats = []; | |
| } | |
| getDownloadIcon() { | |
| return ` | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> | |
| <polyline points="7,10 12,15 17,10"></polyline> | |
| <line x1="12" y1="15" x2="12" y2="3"></line> | |
| </svg> | |
| `; | |
| } | |
| getFileTypeIcon(type) { | |
| const icons = { | |
| 'video': ` | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <polygon points="23 7 16 12 23 17 23 7"></polygon> | |
| <rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect> | |
| </svg> | |
| `, | |
| 'audio': ` | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M9 18V5l12-2v13"></path> | |
| <circle cx="6" cy="18" r="3"></circle> | |
| <circle cx="18" cy="16" r="3"></circle> | |
| </svg> | |
| `, | |
| 'image': ` | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect> | |
| <circle cx="8.5" cy="8.5" r="1.5"></circle> | |
| <polyline points="21,15 16,10 5,21"></polyline> | |
| </svg> | |
| `, | |
| 'thumbnail': ` | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect> | |
| <circle cx="8.5" cy="8.5" r="1.5"></circle> | |
| <line x1="21" y1="15" x2="16" y2="10"></line> | |
| </svg> | |
| `, | |
| 'direct': ` | |
| <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> | |
| <polyline points="14,2 14,8 20,8"></polyline> | |
| </svg> | |
| ` | |
| }; | |
| return icons[type] || icons['direct']; | |
| } | |
| announceAppReady() { | |
| setTimeout(() => { | |
| this.announceToScreenReader('Universal Media Downloader ready. Enter a URL to begin downloading.'); | |
| document.getElementById('url-input').focus(); | |
| }, 1000); | |
| } | |
| } | |
| // Initialize the application | |
| document.addEventListener('DOMContentLoaded', () => { | |
| window.downloader = new UniversalMediaDownloader(); | |
| }); | |
| // Global error handling | |
| window.addEventListener('error', (event) => { | |
| console.error('Global error:', event.error); | |
| if (window.downloader) { | |
| window.downloader.showError('An unexpected error occurred. Please refresh the page and try again.'); | |
| } | |
| }); | |
| window.addEventListener('unhandledrejection', (event) => { | |
| console.error('Unhandled promise rejection:', event.reason); | |
| if (window.downloader) { | |
| window.downloader.showError('A background process failed. Please check your connection and try again.'); | |
| } | |
| }); |