document.addEventListener('DOMContentLoaded', function() { const socket = io(); let currentJobId = null; let activeJobs = new Map(); // Track all jobs by ID // Elements const youtubeUrlInput = document.getElementById('youtube-url'); const transcribeBtn = document.getElementById('transcribe-btn'); const queueContainer = document.getElementById('queue-container'); const queueList = document.getElementById('queue-list'); const statusContainer = document.getElementById('status-container'); const statusText = document.getElementById('status-text'); const jobIdElement = document.getElementById('job-id'); const progressBar = document.getElementById('progress-bar'); const progressText = document.getElementById('progress-text'); const currentMessage = document.getElementById('current-message'); const resultContainer = document.getElementById('result-container'); const transcriptPreview = document.getElementById('transcript-preview'); const downloadTxtBtn = document.getElementById('download-txt'); const downloadSrtBtn = document.getElementById('download-srt'); const errorContainer = document.getElementById('error-container'); const errorText = document.getElementById('error-text'); // Handle form submission transcribeBtn.addEventListener('click', function() { const youtubeUrl = youtubeUrlInput.value.trim(); if (!youtubeUrl) { showError('Please enter a valid YouTube URL'); return; } if (!isValidYouTubeUrl(youtubeUrl)) { showError('The URL does not appear to be a valid YouTube URL'); return; } startTranscription(youtubeUrl); }); // Set up socket listeners once socket.on('status_update', handleStatusUpdate); socket.on('queue_update', handleQueueUpdate); // Start the transcription process function startTranscription(youtubeUrl) { // Reset result UI resetResultUI(); // Disable the button during processing transcribeBtn.disabled = true; transcribeBtn.innerText = 'Processing...'; // Send the transcription request fetch('/api/transcribe', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ youtube_url: youtubeUrl }) }) .then(response => { if (!response.ok) { throw new Error('Failed to start transcription'); } return response.json(); }) .then(data => { // Handle multiple jobs (playlist) or single job if (data.job_ids) { // Playlist case showMessage(`Added ${data.job_ids.length} videos to the queue`); // Join socket rooms for all jobs data.job_ids.forEach(jobId => { socket.emit('join', { job_id: jobId }); }); // Show the queue immediately fetchAndRenderQueue(); } else { // Single video case currentJobId = data.job_id; // Join the socket.io room for this job socket.emit('join', { job_id: currentJobId }); // Show the queue fetchAndRenderQueue(); } // Reset the input field and button after submission resetButton(); }) .catch(error => { showError(error.message); resetButton(); }); } // Fetch and display the current queue function fetchAndRenderQueue() { fetch('/api/queue') .then(response => response.json()) .then(data => { handleQueueUpdate(data); }) .catch(error => { console.error("Error fetching queue:", error); }); } // Handle queue updates from WebSocket function handleQueueUpdate(data) { const queue = data.queue || []; // Show or hide queue container based on if there are items if (queue.length > 0) { queueContainer.classList.remove('hidden'); renderQueue(queue); } else { queueContainer.classList.add('hidden'); } } // Render the queue items function renderQueue(queue) { // Clear the existing queue queueList.innerHTML = ''; // Add each queue item queue.forEach(job => { // Store in our local tracking activeJobs.set(job.job_id, job); // Create the queue item element const queueItem = document.createElement('div'); queueItem.className = 'queue-item'; queueItem.dataset.jobId = job.job_id; // Create the thumbnail let thumbnailUrl = '/api/thumbnail/' + job.job_id; if (!job.thumbnail || job.thumbnail === '') { thumbnailUrl = 'https://via.placeholder.com/120x68?text=No+Thumbnail'; } // Format duration let durationText = 'Unknown'; if (job.duration && !isNaN(job.duration)) { const minutes = Math.floor(job.duration / 60); const seconds = job.duration % 60; durationText = `${minutes}:${seconds.toString().padStart(2, '0')}`; } // Set the HTML content for the queue item queueItem.innerHTML = ` Video thumbnail
${job.title || 'Unknown Title'}
${capitalize(job.status)} ${durationText}
${job.job_id}
${job.status === 'queued' ? '' : ''}
`; // Add event listener for the remove button const removeBtn = queueItem.querySelector('.queue-remove'); if (removeBtn) { removeBtn.addEventListener('click', function(event) { event.stopPropagation(); cancelJob(job.job_id); }); } // Add event listener to show details when clicking on a queue item queueItem.addEventListener('click', function() { showJobDetails(job.job_id); }); // Add to the queue list queueList.appendChild(queueItem); }); } // Cancel a job function cancelJob(jobId) { fetch(`/api/cancel/${jobId}`, { method: 'POST' }) .then(response => response.json()) .then(data => { console.log('Job cancelled:', data); }) .catch(error => { console.error('Error cancelling job:', error); }); } // Show details for a specific job function showJobDetails(jobId) { const job = activeJobs.get(jobId); if (!job) return; // Set current job ID currentJobId = jobId; // Update the UI based on job status if (job.status === 'completed') { completeTranscription(job); } else { // Show status container for in-progress jobs statusContainer.classList.remove('hidden'); resultContainer.classList.add('hidden'); // Update status display statusText.innerText = capitalize(job.status); jobIdElement.innerText = `Job ID: ${job.job_id}`; // Update progress progressBar.style.width = `${job.progress || 0}%`; progressText.innerText = `${Math.round(job.progress || 0)}%`; // Update message currentMessage.innerText = job.message || ''; } } // Handle status updates from WebSocket function handleStatusUpdate(data) { // Store in our tracking map activeJobs.set(data.job_id, data); // Update queue item if it exists const queueItem = queueList.querySelector(`[data-job-id="${data.job_id}"]`); if (queueItem) { // Update the progress bar const progressBar = queueItem.querySelector('.queue-progress'); if (progressBar) { progressBar.style.width = `${data.progress || 0}%`; } // Update the status const statusSpan = queueItem.querySelector('.queue-status span:first-child'); if (statusSpan) { statusSpan.textContent = capitalize(data.status); } } // If this is the current job being viewed, update the status display if (currentJobId === data.job_id) { // Update status text and progress statusText.innerText = capitalize(data.status); progressBar.style.width = `${data.progress || 0}%`; progressText.innerText = `${Math.round(data.progress || 0)}%`; // Update message if (data.message) { currentMessage.innerText = data.message; } // Handle completion or failure if (data.status === 'completed') { completeTranscription(data); } else if (data.status === 'failed') { showError(data.message || 'Transcription failed'); } } } // Process completed transcription function completeTranscription(data) { // Hide status container statusContainer.classList.add('hidden'); // Show result container resultContainer.classList.remove('hidden'); // Set preview text if (data.preview) { transcriptPreview.innerText = data.preview; } else { transcriptPreview.innerText = 'Preview not available'; } // Set download links if (data.txt_file) { downloadTxtBtn.href = data.txt_file; } if (data.srt_file) { downloadSrtBtn.href = data.srt_file; } // Reset button resetButton(); } // Show error message function showError(message) { errorContainer.classList.remove('hidden'); errorText.innerText = message; // Hide other containers statusContainer.classList.add('hidden'); resultContainer.classList.add('hidden'); } // Reset the result UI elements function resetResultUI() { errorContainer.classList.add('hidden'); statusContainer.classList.add('hidden'); resultContainer.classList.add('hidden'); } // Reset the transcribe button function resetButton() { transcribeBtn.disabled = false; transcribeBtn.innerText = 'Transcribe'; youtubeUrlInput.value = ''; } // Show a message in the error container, but styled as a message function showMessage(message) { errorContainer.classList.remove('hidden'); errorContainer.style.backgroundColor = '#e8f4f8'; errorContainer.style.border = '1px solid #b8daff'; errorText.style.color = '#0c5460'; errorText.innerText = message; } // Validate YouTube URL function isValidYouTubeUrl(url) { const pattern = /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+/; return pattern.test(url); } // Capitalize first letter function capitalize(string) { if (!string) return ''; return string.charAt(0).toUpperCase() + string.slice(1); } // Initial fetch of the queue on page load fetchAndRenderQueue(); });