Files
ytts/static/js/main.js
2025-04-02 21:44:17 -07:00

349 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
});