Spaces:
Running
Running
// Index page functionality | |
document.addEventListener('DOMContentLoaded', () => { | |
const youtubeUrlInput = document.getElementById('youtube-url'); | |
const processButton = document.getElementById('process-button'); | |
const processStatus = document.getElementById('process-status'); | |
const processingIndicator = document.getElementById('processing'); | |
const recentlyProcessedCard = document.getElementById('recently-processed'); | |
const videoListContainer = document.getElementById('video-list'); | |
// Check for search parameter in URL | |
const urlParams = new URLSearchParams(window.location.search); | |
const searchQuery = urlParams.get('search'); | |
if (searchQuery) { | |
// Display search results | |
displaySearchResults(searchQuery); | |
} | |
// Example video buttons | |
const exampleButtons = document.querySelectorAll('.example-video'); | |
// Process button click handler | |
processButton.addEventListener('click', () => processVideo()); | |
// Enter key in input field | |
youtubeUrlInput.addEventListener('keypress', (e) => { | |
if (e.key === 'Enter') processVideo(); | |
}); | |
// Example video buttons | |
exampleButtons.forEach(button => { | |
button.addEventListener('click', () => { | |
youtubeUrlInput.value = button.dataset.url; | |
processVideo(); | |
}); | |
}); | |
// Process video function | |
function processVideo() { | |
const youtubeUrl = youtubeUrlInput.value.trim(); | |
if (!youtubeUrl) { | |
processStatus.innerHTML = '<div class="alert alert-warning">Please enter a YouTube URL</div>'; | |
return; | |
} | |
// Extract video ID | |
const videoId = extractVideoId(youtubeUrl); | |
if (!videoId) { | |
processStatus.innerHTML = '<div class="alert alert-error">Invalid YouTube URL</div>'; | |
return; | |
} | |
// Show loading indicator with spinner and text | |
processStatus.innerHTML = ` | |
<div class="flex items-center justify-center my-4"> | |
<span class="loading loading-spinner loading-md text-primary"></span> | |
<span class="ml-2">Processing video... This may take a few moments</span> | |
</div> | |
`; | |
// Set a timeout to handle overly long processing | |
const timeoutId = setTimeout(() => { | |
processStatus.innerHTML = ` | |
<div class="alert alert-warning"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> | |
</svg> | |
<span>Processing is taking longer than expected. Please wait...</span> | |
</div> | |
`; | |
}, 20000); // 20 seconds | |
// Send request to process the video | |
fetch('/api/video/process', { | |
method: 'POST', | |
headers: { | |
'Content-Type': 'application/json' | |
}, | |
body: JSON.stringify({ url: youtubeUrl }) | |
}) | |
.then(response => { | |
if (!response.ok) { | |
throw new Error('Failed to process video'); | |
} | |
return response.json(); | |
}) | |
.then(data => { | |
// Clear timeout for long-running process | |
clearTimeout(timeoutId); | |
// Extract video ID from response (handles both old and new API formats) | |
const videoId = data.video ? data.video.video_id : data.video_id; | |
const isNewlyProcessed = data.newly_processed !== undefined ? data.newly_processed : true; | |
if (!videoId) { | |
throw new Error('Invalid response: Missing video ID'); | |
} | |
// Get video title (for display) | |
const videoTitle = data.video ? data.video.title : (data.title || `Video ${videoId}`); | |
// Log for debugging | |
console.log('Process response:', {videoId, isNewlyProcessed, data}); | |
// Show success message | |
processStatus.innerHTML = ` | |
<div role="alert" class="alert alert-success"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
</svg> | |
<span>${isNewlyProcessed ? 'Video processed successfully!' : 'Video was already processed!'}</span> | |
<div> | |
<a href="/video/${videoId}" class="btn btn-sm btn-primary"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
</svg> | |
Open Video | |
</a> | |
</div> | |
</div> | |
`; | |
// Update recent videos lists | |
displayRecentVideos(); | |
loadFooterRecentVideos(); // Update footer videos as well | |
}) | |
.catch(error => { | |
// Clear timeout for long-running process | |
clearTimeout(timeoutId); | |
// Show error message | |
console.error('Process error:', error); | |
processStatus.innerHTML = handleError(error); | |
}); | |
} | |
// Display recently processed videos | |
function displayRecentVideos() { | |
// Show loading state | |
recentlyProcessedCard.classList.remove('hidden'); | |
videoListContainer.innerHTML = ` | |
<div class="flex justify-center items-center p-4"> | |
<span class="loading loading-spinner loading-md"></span> | |
<span class="ml-2">Loading recent videos...</span> | |
</div> | |
`; | |
const carouselPrev = document.getElementById('carousel-prev'); | |
const carouselNext = document.getElementById('carousel-next'); | |
// Fetch recent videos from server | |
fetch('/api/video/recent?limit=5') | |
.then(response => { | |
if (!response.ok) { | |
throw new Error('Failed to fetch recent videos'); | |
} | |
return response.json(); | |
}) | |
.then(videos => { | |
if (videos && videos.length > 0) { | |
// Limit to 5 videos | |
const limitedVideos = videos.slice(0, 5); | |
// Generate carousel items | |
const carouselItems = limitedVideos.map((video, index) => { | |
// Format date if available | |
let formattedDate = ''; | |
if (video.created_at) { | |
const date = new Date(video.created_at * 1000); // Convert Unix timestamp to milliseconds | |
formattedDate = date.toLocaleDateString(); | |
} | |
// Use title or default | |
const videoTitle = video.title || `Video ${video.video_id}`; | |
return ` | |
<div id="video-${index}" class="carousel-item"> | |
<a href="/video/${video.video_id}" class="card bg-base-100 shadow-sm hover:shadow-md transition-all w-64 md:w-72 flex flex-col"> | |
<figure class="w-full h-36 overflow-hidden"> | |
<img src="https://img.youtube.com/vi/${video.video_id}/mqdefault.jpg" alt="Thumbnail" class="w-full h-full object-cover"> | |
</figure> | |
<div class="card-body p-3"> | |
<h3 class="card-title text-sm line-clamp-2">${videoTitle}</h3> | |
<div class="text-xs opacity-70">${formattedDate}</div> | |
</div> | |
</a> | |
</div> | |
`; | |
}).join(''); | |
// Add carousel items to container | |
videoListContainer.innerHTML = carouselItems; | |
// Setup navigation arrows | |
if (limitedVideos.length > 1) { | |
// Show arrows for multiple videos | |
let currentIndex = 0; | |
const maxIndex = limitedVideos.length - 1; | |
// Show navigation arrows | |
carouselPrev.classList.remove('hidden'); | |
carouselNext.classList.remove('hidden'); | |
// Left button is disabled by default (we're at the start) | |
const prevButton = carouselPrev.querySelector('button'); | |
const nextButton = carouselNext.querySelector('button'); | |
prevButton.classList.add('btn-disabled'); | |
// Functions to update button states | |
const updateButtonStates = () => { | |
if (currentIndex === 0) { | |
prevButton.classList.add('btn-disabled'); | |
} else { | |
prevButton.classList.remove('btn-disabled'); | |
} | |
if (currentIndex === maxIndex) { | |
nextButton.classList.add('btn-disabled'); | |
} else { | |
nextButton.classList.remove('btn-disabled'); | |
} | |
}; | |
// Setup navigation buttons | |
prevButton.addEventListener('click', () => { | |
if (currentIndex > 0) { | |
currentIndex--; | |
document.getElementById(`video-${currentIndex}`).scrollIntoView({ | |
behavior: 'smooth', | |
block: 'nearest', | |
inline: 'center' | |
}); | |
updateButtonStates(); | |
} | |
}); | |
nextButton.addEventListener('click', () => { | |
if (currentIndex < maxIndex) { | |
currentIndex++; | |
document.getElementById(`video-${currentIndex}`).scrollIntoView({ | |
behavior: 'smooth', | |
block: 'nearest', | |
inline: 'center' | |
}); | |
updateButtonStates(); | |
} | |
}); | |
} else { | |
// Hide arrows for single video | |
carouselPrev.classList.add('hidden'); | |
carouselNext.classList.add('hidden'); | |
} | |
} else { | |
recentlyProcessedCard.classList.add('hidden'); | |
carouselPrev.classList.add('hidden'); | |
carouselNext.classList.add('hidden'); | |
} | |
}) | |
.catch(error => { | |
console.error('Error fetching recent videos:', error); | |
videoListContainer.innerHTML = ` | |
<div class="alert alert-error"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
</svg> | |
<span>Failed to load recent videos</span> | |
</div> | |
`; | |
carouselPrev.classList.add('hidden'); | |
carouselNext.classList.add('hidden'); | |
}); | |
} | |
// Display recent videos on page load | |
displayRecentVideos(); | |
// Function to display search results | |
function displaySearchResults(query) { | |
// Try to use the modal if available | |
const searchModal = document.getElementById('search-results-modal'); | |
const searchResultsContainer = document.getElementById('search-results-container'); | |
if (searchModal && searchResultsContainer) { | |
// Update modal title with search term | |
const modalTitle = searchModal.querySelector('h3'); | |
if (modalTitle) { | |
modalTitle.textContent = `Search Results for "${query}"`; | |
} | |
// Show the modal | |
searchModal.showModal(); | |
// Clear previous results and show loading state | |
searchResultsContainer.innerHTML = ` | |
<div class="flex justify-center items-center p-4"> | |
<span class="loading loading-spinner loading-md"></span> | |
<span class="ml-2">Searching...</span> | |
</div> | |
`; | |
// Fetch and display results in the modal | |
fetchAndDisplayResults(query, searchResultsContainer); | |
} else { | |
// Fallback to the old implementation if modal is not available | |
// Show a search results card with loading indicator | |
processStatus.innerHTML = ` | |
<div class="card bg-base-100 shadow-xl mt-4"> | |
<div class="card-body"> | |
<h2 class="card-title">Search Results for "${query}"</h2> | |
<div class="mt-4"> | |
<div class="flex justify-center items-center p-4"> | |
<span class="loading loading-spinner loading-md"></span> | |
<span class="ml-2">Searching...</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
`; | |
// Fetch and display results in the processStatus element | |
fetchAndDisplayResults(query, null, processStatus); | |
} | |
} | |
// Helper function to fetch and display search results | |
function fetchAndDisplayResults(query, container, fallbackContainer) { | |
// Fetch search results from API | |
fetch(`/api/video/search?query=${encodeURIComponent(query)}&limit=10`) | |
.then(response => { | |
if (!response.ok) { | |
throw new Error('Failed to perform search'); | |
} | |
return response.json(); | |
}) | |
.then(results => { | |
if (results && results.length > 0) { | |
// Group results by video | |
const videoGroups = {}; | |
// First pass: collect all video IDs that need titles | |
const videoIds = []; | |
results.forEach(result => { | |
const videoId = result.segment.video_id; | |
if (!videoGroups[videoId]) { | |
videoGroups[videoId] = { | |
videoId: videoId, | |
title: `Video ${videoId}`, // Default title, will be updated | |
segments: [] | |
}; | |
// Add to list of IDs to fetch titles for | |
videoIds.push(videoId); | |
} | |
videoGroups[videoId].segments.push(result); | |
}); | |
// If we have video IDs, fetch their proper titles | |
const videoPromises = videoIds.map(videoId => { | |
return fetch(`/api/video/info/${videoId}`) | |
.then(response => response.ok ? response.json() : null) | |
.then(videoInfo => { | |
if (videoInfo && videoInfo.title && videoGroups[videoId]) { | |
videoGroups[videoId].title = videoInfo.title; | |
} | |
}) | |
.catch(error => console.error(`Error fetching video info for ${videoId}:`, error)); | |
}); | |
// After all video titles are fetched, continue with rendering | |
Promise.all(videoPromises).then(() => { | |
// Generate results HTML | |
const resultsHTML = Object.values(videoGroups).map(group => { | |
const segmentsHTML = group.segments.map(result => { | |
return ` | |
<a href="/video/${group.videoId}?t=${Math.floor(result.segment.start)}" | |
class="block p-2 hover:bg-base-200 rounded-md transition-colors duration-150 mb-2"> | |
<div class="flex items-start"> | |
<div class="text-primary font-mono mr-2">${formatTime(result.segment.start)}</div> | |
<div class="flex-grow"> | |
<p class="truncate-3-lines">${result.segment.text}</p> | |
<div class="text-xs opacity-70 mt-1">Score: ${(result.score * 100).toFixed(1)}%</div> | |
</div> | |
</div> | |
</a> | |
`; | |
}).join(''); | |
return ` | |
<div class="mb-6"> | |
<div class="flex items-center mb-2"> | |
<img src="https://img.youtube.com/vi/${group.videoId}/default.jpg" alt="Thumbnail" | |
class="w-12 h-12 rounded-md mr-2"> | |
<h3 class="text-lg font-bold"> | |
<a href="/video/${group.videoId}" class="link">${group.title}</a> | |
</h3> | |
</div> | |
<div class="pl-4 border-l-2 border-primary"> | |
${segmentsHTML} | |
</div> | |
</div> | |
`; | |
}).join(''); | |
// Display results in the appropriate container | |
if (container) { | |
container.innerHTML = resultsHTML; | |
} else if (fallbackContainer) { | |
fallbackContainer.innerHTML = ` | |
<div class="card bg-base-100 shadow-xl mt-4"> | |
<div class="card-body"> | |
<h2 class="card-title">Search Results for "${query}"</h2> | |
<div class="mt-4"> | |
${resultsHTML} | |
</div> | |
</div> | |
</div> | |
`; | |
} | |
}); | |
} else { | |
// No results found - display immediately, no need to wait for titles | |
const noResultsHTML = ` | |
<div class="alert alert-info"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
</svg> | |
<span>No results found. Try a different search term or process more videos.</span> | |
</div> | |
`; | |
if (container) { | |
container.innerHTML = noResultsHTML; | |
} else if (fallbackContainer) { | |
fallbackContainer.innerHTML = ` | |
<div class="card bg-base-100 shadow-xl mt-4"> | |
<div class="card-body"> | |
<h2 class="card-title">Search Results for "${query}"</h2> | |
<div class="mt-4"> | |
${noResultsHTML} | |
</div> | |
</div> | |
</div> | |
`; | |
} | |
} | |
}) | |
.catch(error => { | |
console.error('Search error:', error); | |
const errorHTML = ` | |
<div class="alert alert-error"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
</svg> | |
<span>Error performing search: ${error.message}</span> | |
</div> | |
`; | |
if (container) { | |
container.innerHTML = errorHTML; | |
} else if (fallbackContainer) { | |
fallbackContainer.innerHTML = ` | |
<div class="card bg-base-100 shadow-xl mt-4"> | |
<div class="card-body"> | |
<h2 class="card-title">Search Results for "${query}"</h2> | |
<div class="mt-4"> | |
${errorHTML} | |
</div> | |
</div> | |
</div> | |
`; | |
} | |
}); | |
} | |
}); | |