349 lines
12 KiB
JavaScript
349 lines
12 KiB
JavaScript
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 = `
|
||
<img class="queue-thumbnail" src="${thumbnailUrl}" alt="Video thumbnail">
|
||
<div class="queue-details">
|
||
<div class="queue-title">${job.title || 'Unknown Title'}</div>
|
||
<div class="queue-status">
|
||
<span>${capitalize(job.status)}</span>
|
||
<span>${durationText}</span>
|
||
</div>
|
||
<div class="queue-job-id">${job.job_id}</div>
|
||
</div>
|
||
${job.status === 'queued' ? '<button class="queue-remove">×</button>' : ''}
|
||
<div class="queue-progress" style="width: ${job.progress || 0}%"></div>
|
||
`;
|
||
|
||
// 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();
|
||
}); |