Spaces:
Running
Running
| class YouTubeUploader { | |
| constructor() { | |
| this.isAuthenticated = false; | |
| this.currentTaskId = null; | |
| this.statusCheckInterval = null; | |
| this.init(); | |
| } | |
| init() { | |
| this.cacheElements(); | |
| this.attachEventListeners(); | |
| this.checkAuthentication(); | |
| } | |
| cacheElements() { | |
| this.authSection = document.getElementById('authSection'); | |
| this.authBtn = document.getElementById('authBtn'); | |
| this.channelInfo = document.getElementById('channelInfo'); | |
| this.channelAvatar = document.getElementById('channelAvatar'); | |
| this.channelName = document.getElementById('channelName'); | |
| this.channelStats = document.getElementById('channelStats'); | |
| this.uploadSection = document.getElementById('uploadSection'); | |
| this.reelUrl = document.getElementById('reelUrl'); | |
| this.uploadBtn = document.getElementById('uploadBtn'); | |
| this.progressSection = document.getElementById('progressSection'); | |
| this.progressTitle = document.getElementById('progressTitle'); | |
| this.progressPercent = document.getElementById('progressPercent'); | |
| this.progressFill = document.getElementById('progressFill'); | |
| this.progressMessage = document.getElementById('progressMessage'); | |
| this.metadataPreview = document.getElementById('metadataPreview'); | |
| this.previewTitle = document.getElementById('previewTitle'); | |
| this.previewDescription = document.getElementById('previewDescription'); | |
| this.previewTags = document.getElementById('previewTags'); | |
| this.previewHashtags = document.getElementById('previewHashtags'); | |
| this.successResult = document.getElementById('successResult'); | |
| this.errorResult = document.getElementById('errorResult'); | |
| this.watchBtn = document.getElementById('watchBtn'); | |
| this.errorMessage = document.getElementById('errorMessage'); | |
| this.retryBtn = document.getElementById('retryBtn'); | |
| this.uploadAnotherBtn = document.getElementById('uploadAnotherBtn'); | |
| this.loadingOverlay = document.getElementById('loadingOverlay'); | |
| this.toast = document.getElementById('toast'); | |
| // Navbar elements | |
| this.navAuthButtons = document.getElementById('navAuthButtons'); | |
| this.navSignInBtn = document.getElementById('navSignInBtn'); | |
| this.navUserMenu = document.getElementById('navUserMenu'); | |
| this.navUserAvatarImg = document.getElementById('navUserAvatarImg'); | |
| this.navUserName = document.getElementById('navUserName'); | |
| this.navUserStats = document.getElementById('navUserStats'); | |
| this.navLogoutBtn = document.getElementById('navLogoutBtn'); | |
| // Mobile menu elements | |
| this.mobileSignInBtn = document.getElementById('mobileSignInBtn'); | |
| this.mobileUserInfo = document.getElementById('mobileUserInfo'); | |
| this.mobileUserName = document.getElementById('mobileUserName'); | |
| this.mobileUserStats = document.getElementById('mobileUserStats'); | |
| this.mobileLogoutBtn = document.getElementById('mobileLogoutBtn'); | |
| // Music upload elements | |
| this.musicUrl = document.getElementById('musicUrl'); | |
| this.musicVolume = document.getElementById('musicVolume'); | |
| this.enableEditingToggle = document.getElementById('enableEditingToggle'); | |
| // Music Source Tab elements | |
| this.musicTabUrl = document.getElementById('musicTabUrl'); | |
| this.musicTabFile = document.getElementById('musicTabFile'); | |
| this.musicUrlSection = document.getElementById('musicUrlSection'); | |
| this.musicFileSection = document.getElementById('musicFileSection'); | |
| this.musicFileInput = document.getElementById('musicFileInput'); | |
| this.musicFileName = document.getElementById('musicFileName'); | |
| } | |
| attachEventListeners() { | |
| this.authBtn.addEventListener('click', () => this.handleAuthentication()); | |
| this.uploadBtn.addEventListener('click', () => this.handleUpload()); | |
| this.retryBtn.addEventListener('click', () => this.resetForm()); | |
| this.uploadAnotherBtn.addEventListener('click', () => this.resetForm()); | |
| this.navLogoutBtn.addEventListener('click', () => this.handleLogout()); | |
| this.reelUrl.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') this.handleUpload(); | |
| }); | |
| // Navbar sign in button | |
| if (this.navSignInBtn) { | |
| this.navSignInBtn.addEventListener('click', () => this.handleAuthentication()); | |
| } | |
| // Mobile sign in and logout | |
| if (this.mobileSignInBtn) { | |
| this.mobileSignInBtn.addEventListener('click', () => { | |
| document.getElementById('mobileMenu').classList.remove('active'); | |
| this.handleAuthentication(); | |
| }); | |
| } | |
| if (this.mobileLogoutBtn) { | |
| this.mobileLogoutBtn.addEventListener('click', () => { | |
| document.getElementById('mobileMenu').classList.remove('active'); | |
| this.handleLogout(); | |
| }); | |
| } | |
| // Music Source Tab Switching | |
| if (this.musicTabUrl && this.musicTabFile) { | |
| this.musicTabUrl.addEventListener('click', () => { | |
| this.currentMusicSource = 'url'; | |
| this.musicTabUrl.style.opacity = '1'; | |
| this.musicTabFile.style.opacity = '0.6'; | |
| this.musicUrlSection.style.display = 'block'; | |
| this.musicFileSection.style.display = 'none'; | |
| this.uploadedMusicFile = null; | |
| }); | |
| this.musicTabFile.addEventListener('click', () => { | |
| this.currentMusicSource = 'file'; | |
| this.musicTabFile.style.opacity = '1'; | |
| this.musicTabUrl.style.opacity = '0.6'; | |
| this.musicFileSection.style.display = 'block'; | |
| this.musicUrlSection.style.display = 'none'; | |
| document.getElementById('musicUrl').value = ''; | |
| }); | |
| } | |
| // Handle music file selection | |
| if (this.musicFileInput) { | |
| this.musicFileInput.addEventListener('change', async (e) => { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| // Validate file type | |
| const validTypes = ['audio/mpeg', 'audio/wav', 'audio/mp4', 'audio/x-m4a', 'audio/aac']; | |
| if (!validTypes.includes(file.type) && !file.name.match(/\.(mp3|wav|m4a|aac)$/i)) { | |
| this.showToast('Please select a valid audio file (MP3, WAV, M4A, AAC)', 'error'); | |
| return; | |
| } | |
| // Check file size (max 50MB) | |
| if (file.size > 50 * 1024 * 1024) { | |
| this.showToast('File size too large. Maximum 50MB allowed.', 'error'); | |
| return; | |
| } | |
| // Update UI | |
| this.musicFileName.value = file.name; | |
| this.showToast('Audio file selected. It will be uploaded when you start the upload process.', 'success'); | |
| // Store file for later upload | |
| this.uploadedMusicFile = file; | |
| }); | |
| } | |
| } | |
| async checkAuthentication() { | |
| try { | |
| const response = await fetch('/check-auth'); | |
| const data = await response.json(); | |
| if (data.authenticated) { | |
| this.isAuthenticated = true; | |
| if (data.channel) { | |
| this.displayChannelInfo(data.channel); | |
| } | |
| this.showUploadSection(); | |
| } else { | |
| this.showAuthSection(); | |
| } | |
| } catch (error) { | |
| console.error('Auth check failed:', error); | |
| this.showAuthSection(); | |
| } | |
| } | |
| displayChannelInfo(channel) { | |
| this.channelAvatar.src = channel.thumbnail || 'https://via.placeholder.com/80'; | |
| this.channelName.textContent = channel.title; | |
| this.channelStats.textContent = `${this.formatNumber(channel.subscriberCount)} subscribers • ${this.formatNumber(channel.videoCount)} videos`; | |
| this.channelInfo.style.display = 'block'; | |
| // Update navbar user menu | |
| this.navUserAvatarImg.src = channel.thumbnail || 'https://via.placeholder.com/40'; | |
| this.navUserName.textContent = channel.title; | |
| this.navUserStats.textContent = `${this.formatNumber(channel.subscriberCount)} subscribers`; | |
| this.navUserMenu.style.display = 'block'; | |
| this.navAuthButtons.style.display = 'none'; | |
| // Update mobile menu | |
| this.mobileUserName.textContent = channel.title; | |
| this.mobileUserStats.textContent = `${this.formatNumber(channel.subscriberCount)} subscribers`; | |
| this.mobileUserInfo.style.display = 'block'; | |
| this.mobileLogoutBtn.style.display = 'block'; | |
| this.mobileSignInBtn.style.display = 'none'; | |
| } | |
| async handleAuthentication() { | |
| this.showLoading(); | |
| try { | |
| const response = await fetch('/auth/start'); | |
| const data = await response.json(); | |
| if (data.auth_url) { | |
| const authWindow = window.open(data.auth_url, 'YouTube Authentication', 'width=600,height=700'); | |
| const checkAuth = setInterval(async () => { | |
| if (authWindow.closed) { | |
| clearInterval(checkAuth); | |
| this.hideLoading(); | |
| await this.checkAuthentication(); | |
| } | |
| }, 1000); | |
| } | |
| } catch (error) { | |
| this.showToast('Authentication failed: ' + error.message, 'error'); | |
| } finally { | |
| this.hideLoading(); | |
| } | |
| } | |
| async handleLogout() { | |
| if (!confirm('Are you sure you want to logout?')) return; | |
| try { | |
| const response = await fetch('/logout', { method: 'POST' }); | |
| const data = await response.json(); | |
| if (data.success) { | |
| this.showToast('Logged out successfully', 'success'); | |
| this.isAuthenticated = false; | |
| this.showAuthSection(); | |
| } | |
| } catch (error) { | |
| this.showToast('Logout failed: ' + error.message, 'error'); | |
| } | |
| } | |
| async handleUpload() { | |
| if (!this.isAuthenticated) { | |
| this.showToast('Please sign in with YouTube first', 'error'); | |
| return; | |
| } | |
| const url = this.reelUrl.value.trim(); | |
| if (!url) { | |
| this.showToast('Please enter a valid Instagram Reel URL', 'error'); | |
| return; | |
| } | |
| // Get video editing preferences | |
| const enableEditing = document.getElementById('enableEditingToggle').checked; | |
| let editingOptions = null; | |
| if (enableEditing) { | |
| let musicSource = null; | |
| // ✅ NEW: Handle local music file upload | |
| const currentMusicSource = this.musicTabUrl.style.opacity === '1' ? 'url' : 'file'; | |
| if (currentMusicSource === 'file' && this.uploadedMusicFile) { | |
| this.showLoading(); | |
| try { | |
| // Upload music file to server | |
| const formData = new FormData(); | |
| formData.append('music', this.uploadedMusicFile); | |
| const uploadResponse = await fetch('/upload-music', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const uploadResult = await uploadResponse.json(); | |
| if (!uploadResult.success) { | |
| throw new Error(uploadResult.error || 'Music upload failed'); | |
| } | |
| musicSource = uploadResult.filepath; | |
| this.showToast('Music file uploaded successfully', 'success'); | |
| } catch (error) { | |
| this.hideLoading(); | |
| this.showToast('Failed to upload music: ' + error.message, 'error'); | |
| return; | |
| } finally { | |
| this.hideLoading(); | |
| } | |
| } else if (currentMusicSource === 'url') { | |
| musicSource = document.getElementById('musicUrl').value.trim(); | |
| } | |
| const musicVolume = parseInt(document.getElementById('musicVolume').value) / 100; | |
| // Get all text overlays | |
| const textOverlays = []; | |
| document.querySelectorAll('.text-overlay-item').forEach(item => { | |
| const text = item.querySelector('.overlay-text').value.trim(); | |
| const position = item.querySelector('.overlay-position').value; | |
| const duration = parseInt(item.querySelector('.overlay-duration').value); | |
| if (text) { | |
| textOverlays.push({ text, position, duration }); | |
| } | |
| }); | |
| editingOptions = { | |
| enabled: true, | |
| music_url: currentMusicSource === 'url' ? musicSource : null, | |
| music_file: currentMusicSource === 'file' ? musicSource : null, | |
| music_volume: musicVolume, | |
| text_overlays: textOverlays.length > 0 ? textOverlays : null | |
| }; | |
| } | |
| this.hideResults(); | |
| this.showProgress(); | |
| try { | |
| const response = await fetch('/auto-upload-async', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| url: url, | |
| editing: editingOptions | |
| }) | |
| }); | |
| const data = await response.json(); | |
| if (data.success) { | |
| this.currentTaskId = data.task_id; | |
| this.startStatusPolling(); | |
| } else { | |
| throw new Error(data.error || 'Upload failed'); | |
| } | |
| } catch (error) { | |
| this.showError(error.message); | |
| } | |
| } | |
| startStatusPolling() { | |
| this.statusCheckInterval = setInterval(async () => { | |
| await this.checkTaskStatus(); | |
| }, 2000); | |
| } | |
| async checkTaskStatus() { | |
| if (!this.currentTaskId) return; | |
| try { | |
| const response = await fetch(`/task-status/${this.currentTaskId}`); | |
| const data = await response.json(); | |
| if (data.success) { | |
| const task = data.task; | |
| this.updateProgress(task); | |
| if (task.status === 'completed') { | |
| this.stopStatusPolling(); | |
| this.showSuccess(task); | |
| } else if (task.status === 'failed') { | |
| this.stopStatusPolling(); | |
| this.showError(task.error || 'Upload failed'); | |
| } | |
| } | |
| } catch (error) { | |
| console.error('Status check failed:', error); | |
| } | |
| } | |
| stopStatusPolling() { | |
| if (this.statusCheckInterval) { | |
| clearInterval(this.statusCheckInterval); | |
| this.statusCheckInterval = null; | |
| } | |
| } | |
| updateProgress(task) { | |
| this.progressTitle.textContent = this.getStatusTitle(task.status); | |
| this.progressPercent.textContent = `${task.progress}%`; | |
| this.progressFill.style.width = `${task.progress}%`; | |
| this.progressMessage.textContent = task.message; | |
| if (task.metadata && task.status === 'uploading') { | |
| this.displayMetadata(task.metadata); | |
| } | |
| } | |
| getStatusTitle(status) { | |
| const titles = { | |
| 'started': 'Starting...', | |
| 'downloading': 'Downloading Reel', | |
| 'editing': 'Editing Video', // ✅ NEW: Add editing status | |
| 'generating_metadata': 'AI Analyzing Video', | |
| 'uploading': 'Uploading to YouTube', | |
| 'completed': 'Upload Complete', | |
| 'failed': 'Upload Failed' | |
| }; | |
| return titles[status] || 'Processing...'; | |
| } | |
| displayMetadata(metadata) { | |
| this.previewTitle.textContent = metadata.title || '-'; | |
| this.previewDescription.textContent = metadata.description || '-'; | |
| this.previewTags.innerHTML = ''; | |
| if (metadata.tags && metadata.tags.length > 0) { | |
| metadata.tags.slice(0, 15).forEach(tag => { | |
| const span = document.createElement('span'); | |
| span.textContent = tag; | |
| this.previewTags.appendChild(span); | |
| }); | |
| } | |
| this.previewHashtags.innerHTML = ''; | |
| if (metadata.hashtags && metadata.hashtags.length > 0) { | |
| metadata.hashtags.slice(0, 20).forEach(hashtag => { | |
| const span = document.createElement('span'); | |
| span.textContent = hashtag; | |
| this.previewHashtags.appendChild(span); | |
| }); | |
| } | |
| this.metadataPreview.style.display = 'block'; | |
| } | |
| showProgress() { | |
| this.progressSection.style.display = 'block'; | |
| this.metadataPreview.style.display = 'none'; | |
| this.successResult.style.display = 'none'; | |
| this.errorResult.style.display = 'none'; | |
| } | |
| showSuccess(task) { | |
| this.progressSection.style.display = 'none'; | |
| this.successResult.style.display = 'block'; | |
| if (task.youtube_url) { | |
| this.watchBtn.href = task.youtube_url; | |
| } | |
| this.showToast('Video uploaded successfully! 🎉', 'success'); | |
| } | |
| showError(message) { | |
| this.progressSection.style.display = 'none'; | |
| this.errorResult.style.display = 'block'; | |
| this.errorMessage.textContent = message; | |
| this.showToast('Upload failed: ' + message, 'error'); | |
| } | |
| hideResults() { | |
| this.successResult.style.display = 'none'; | |
| this.errorResult.style.display = 'none'; | |
| this.metadataPreview.style.display = 'none'; | |
| } | |
| resetForm() { | |
| this.reelUrl.value = ''; | |
| this.hideResults(); | |
| this.progressSection.style.display = 'none'; | |
| this.currentTaskId = null; | |
| this.stopStatusPolling(); | |
| } | |
| showUploadSection() { | |
| this.authSection.style.display = 'none'; | |
| this.uploadSection.style.display = 'block'; | |
| } | |
| showAuthSection() { | |
| this.authSection.style.display = 'block'; | |
| this.uploadSection.style.display = 'none'; | |
| this.channelInfo.style.display = 'none'; | |
| // Update navbar | |
| this.navUserMenu.style.display = 'none'; | |
| this.navAuthButtons.style.display = 'flex'; | |
| // Update mobile menu | |
| this.mobileUserInfo.style.display = 'none'; | |
| this.mobileLogoutBtn.style.display = 'none'; | |
| this.mobileSignInBtn.style.display = 'block'; | |
| } | |
| showLoading() { | |
| this.loadingOverlay.style.display = 'flex'; | |
| } | |
| hideLoading() { | |
| this.loadingOverlay.style.display = 'none'; | |
| } | |
| showToast(message, type = 'info') { | |
| this.toast.textContent = message; | |
| this.toast.className = 'toast show'; | |
| if (type === 'success') { | |
| this.toast.style.borderLeft = '4px solid var(--success)'; | |
| } else if (type === 'error') { | |
| this.toast.style.borderLeft = '4px solid var(--error)'; | |
| } | |
| setTimeout(() => { | |
| this.toast.classList.remove('show'); | |
| }, 4000); | |
| } | |
| formatNumber(num) { | |
| if (num >= 1000000) { | |
| return (num / 1000000).toFixed(1) + 'M'; | |
| } else if (num >= 1000) { | |
| return (num / 1000).toFixed(1) + 'K'; | |
| } | |
| return num; | |
| } | |
| } | |
| document.addEventListener('DOMContentLoaded', () => { | |
| new YouTubeUploader(); | |
| }); | |
| // Helper functions | |
| function showLoadingOverlay(message) { | |
| const overlay = document.getElementById('loadingOverlay'); | |
| if (overlay) { | |
| overlay.querySelector('p').textContent = message; | |
| overlay.style.display = 'flex'; | |
| } | |
| } | |
| function hideLoadingOverlay() { | |
| const overlay = document.getElementById('loadingOverlay'); | |
| if (overlay) { | |
| overlay.style.display = 'none'; | |
| } | |
| } | |
| function showToast(message, type = 'info') { | |
| const toast = document.getElementById('toast'); | |
| if (toast) { | |
| toast.textContent = message; | |
| toast.classList.add('show'); | |
| setTimeout(() => toast.classList.remove('show'), 3000); | |
| } | |
| } | |
| // Modified upload function to handle music file | |
| async function startUpload() { | |
| const reelUrl = document.getElementById('reelUrl').value.trim(); | |
| if (!reelUrl) { | |
| showToast('Please enter an Instagram Reel URL', 'error'); | |
| return; | |
| } | |
| // Check if editing is enabled | |
| const editingEnabled = document.getElementById('enableEditingToggle').checked; | |
| let editingOptions = null; | |
| if (editingEnabled) { | |
| let musicPath = null; | |
| // Handle music upload if local file is selected | |
| if (currentMusicSource === 'file' && uploadedMusicFile) { | |
| showLoadingOverlay('Uploading background music...'); | |
| try { | |
| const formData = new FormData(); | |
| formData.append('music', uploadedMusicFile); | |
| const uploadResponse = await fetch('/upload-music', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const uploadResult = await uploadResponse.json(); | |
| if (!uploadResult.success) { | |
| throw new Error(uploadResult.error || 'Music upload failed'); | |
| } | |
| musicPath = uploadResult.filepath; | |
| hideLoadingOverlay(); | |
| } catch (error) { | |
| hideLoadingOverlay(); | |
| showToast('Failed to upload music file: ' + error.message, 'error'); | |
| return; | |
| } | |
| } else if (currentMusicSource === 'url') { | |
| musicPath = document.getElementById('musicUrl').value.trim(); | |
| } | |
| // Collect text overlays | |
| const textOverlays = []; | |
| document.querySelectorAll('.text-overlay-item').forEach(item => { | |
| const text = item.querySelector('.overlay-text').value.trim(); | |
| if (text) { | |
| textOverlays.push({ | |
| text: text, | |
| position: item.querySelector('.overlay-position').value, | |
| duration: parseInt(item.querySelector('.overlay-duration').value) || 5 | |
| }); | |
| } | |
| }); | |
| editingOptions = { | |
| enabled: true, | |
| music_url: currentMusicSource === 'url' ? musicPath : null, | |
| music_file: currentMusicSource === 'file' ? musicPath : null, | |
| music_volume: parseInt(document.getElementById('musicVolume').value) / 100, | |
| text_overlays: textOverlays | |
| }; | |
| } | |
| // Start async upload | |
| try { | |
| showLoadingOverlay('Starting upload process...'); | |
| const response = await fetch('/auto-upload-async', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| url: reelUrl, | |
| editing: editingOptions | |
| }) | |
| }); | |
| const result = await response.json(); | |
| hideLoadingOverlay(); | |
| if (result.success) { | |
| // Start polling for status | |
| pollTaskStatus(result.task_id); | |
| } else { | |
| showToast(result.error || 'Failed to start upload', 'error'); | |
| } | |
| } catch (error) { | |
| hideLoadingOverlay(); | |
| showToast('Error: ' + error.message, 'error'); | |
| } | |
| } | |