// NFOGuard Web Interface JavaScript // Global state let currentTab = 'dashboard'; let currentMoviesPage = 1; let currentSeriesPage = 1; let dashboardData = null; // Initialize app document.addEventListener('DOMContentLoaded', function() { initializeTabs(); initializeEventListeners(); loadDashboard(); }); // Tab management function initializeTabs() { const tabButtons = document.querySelectorAll('.nav-tab'); const tabContents = document.querySelectorAll('.tab-content'); tabButtons.forEach(button => { button.addEventListener('click', function() { const tabName = this.dataset.tab; switchTab(tabName); }); }); } function switchTab(tabName) { // Update button states document.querySelectorAll('.nav-tab').forEach(btn => btn.classList.remove('active')); document.querySelector(`[data-tab="${tabName}"]`).classList.add('active'); // Update content document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); document.getElementById(tabName).classList.add('active'); currentTab = tabName; // Load tab-specific data switch(tabName) { case 'dashboard': loadDashboard(); break; case 'movies': loadMovies(); break; case 'tv': loadSeries(); break; case 'reports': loadReport(); break; case 'tools': loadDetailedStats(); break; } } // Event listeners function initializeEventListeners() { // Search inputs document.getElementById('movies-search').addEventListener('input', debounce(loadMovies, 500)); document.getElementById('series-search').addEventListener('input', debounce(loadSeries, 500)); // Filter dropdowns document.getElementById('movies-filter-date').addEventListener('change', loadMovies); document.getElementById('movies-filter-source').addEventListener('change', loadMovies); // Forms document.getElementById('edit-form').addEventListener('submit', handleEditSubmit); document.getElementById('bulk-update-form').addEventListener('submit', handleBulkUpdate); } // API calls async function apiCall(endpoint, options = {}) { try { const response = await fetch(endpoint, { headers: { 'Content-Type': 'application/json', ...options.headers }, ...options }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } catch (error) { console.error('API call failed:', error); showToast(`API Error: ${error.message}`, 'error'); throw error; } } // Dashboard async function loadDashboard() { try { dashboardData = await apiCall('/api/dashboard'); updateDashboardStats(); updateDashboardCharts(); } catch (error) { console.error('Failed to load dashboard:', error); } } function updateDashboardStats() { if (!dashboardData) return; document.getElementById('movies-total').textContent = dashboardData.movies_total || 0; document.getElementById('movies-with-dates').textContent = `${dashboardData.movies_with_dates || 0} with dates`; document.getElementById('series-total').textContent = dashboardData.series_count || 0; document.getElementById('episodes-total').textContent = `${dashboardData.episodes_total || 0} episodes`; const missingTotal = (dashboardData.movies_missing_dates || 0) + (dashboardData.episodes_missing_dates || 0); document.getElementById('missing-dates-total').textContent = missingTotal; const noValidTotal = (dashboardData.movies_no_valid_source || 0) + (dashboardData.episodes_no_valid_source || 0); document.getElementById('no-valid-source-total').textContent = `${noValidTotal} no valid source`; document.getElementById('recent-activity').textContent = dashboardData.recent_activity_count || 0; } function updateDashboardCharts() { if (!dashboardData) return; // Movie sources chart const movieChart = document.getElementById('movie-sources-chart'); if (dashboardData.movie_sources && dashboardData.movie_sources.length > 0) { movieChart.innerHTML = createSimpleChart(dashboardData.movie_sources); } else { movieChart.innerHTML = '

No movie source data available

'; } // Episode sources chart const episodeChart = document.getElementById('episode-sources-chart'); if (dashboardData.episode_sources && dashboardData.episode_sources.length > 0) { episodeChart.innerHTML = createSimpleChart(dashboardData.episode_sources); } else { episodeChart.innerHTML = '

No episode source data available

'; } } function createSimpleChart(data) { const total = data.reduce((sum, item) => sum + item.count, 0); let html = '
'; data.forEach((item, index) => { const percentage = ((item.count / total) * 100).toFixed(1); const color = getChartColor(index); html += `
${item.source} ${item.count} (${percentage}%)
`; }); html += '
'; return html; } function getChartColor(index) { const colors = ['#007bff', '#28a745', '#ffc107', '#dc3545', '#6c757d', '#17a2b8', '#6f42c1']; return colors[index % colors.length]; } // Movies async function loadMovies(page = 1) { const search = document.getElementById('movies-search').value; const hasDate = document.getElementById('movies-filter-date').value; const sourceFilter = document.getElementById('movies-filter-source').value; const params = new URLSearchParams({ skip: (page - 1) * 100, limit: 100 }); if (search) params.append('search', search); if (hasDate) params.append('has_date', hasDate); if (sourceFilter) params.append('source_filter', sourceFilter); try { const data = await apiCall(`/api/movies?${params}`); updateMoviesTable(data); updateMoviesPagination(data); updateMoviesSourceFilter(data); currentMoviesPage = page; } catch (error) { console.error('Failed to load movies:', error); } } function updateMoviesTable(data) { const tbody = document.getElementById('movies-tbody'); if (!data.movies || data.movies.length === 0) { tbody.innerHTML = 'No movies found'; return; } tbody.innerHTML = data.movies.map(movie => { const dateadded = movie.dateadded ? formatDateTime(movie.dateadded) : ''; const hasDateBadge = movie.dateadded ? 'Yes' : 'No'; const hasVideoBadge = movie.has_video_file ? 'Yes' : 'No'; return ` ${escapeHtml(movie.title)} ${movie.imdb_id} ${movie.released || '-'} ${dateadded || '-'} ${movie.source || 'Unknown'} ${hasVideoBadge} `; }).join(''); } function updateMoviesPagination(data) { const pagination = document.getElementById('movies-pagination'); if (data.pages <= 1) { pagination.innerHTML = ''; return; } let html = ''; if (data.has_prev) { html += ``; } html += `Page ${data.page} of ${data.pages}`; if (data.has_next) { html += ``; } pagination.innerHTML = html; } function updateMoviesSourceFilter(data) { // This would be populated from dashboard data if (dashboardData && dashboardData.movie_sources) { const select = document.getElementById('movies-filter-source'); const currentValue = select.value; select.innerHTML = ''; dashboardData.movie_sources.forEach(source => { select.innerHTML += ``; }); select.value = currentValue; } } function refreshMovies() { loadMovies(currentMoviesPage); } // TV Series async function loadSeries(page = 1) { const search = document.getElementById('series-search').value; const params = new URLSearchParams({ skip: (page - 1) * 50, limit: 50 }); if (search) params.append('search', search); try { const data = await apiCall(`/api/series?${params}`); updateSeriesTable(data); updateSeriesPagination(data); currentSeriesPage = page; } catch (error) { console.error('Failed to load series:', error); } } function updateSeriesTable(data) { const tbody = document.getElementById('series-tbody'); if (!data.series || data.series.length === 0) { tbody.innerHTML = 'No series found'; return; } tbody.innerHTML = data.series.map(series => { const progressPercent = series.total_episodes > 0 ? ((series.episodes_with_dates / series.total_episodes) * 100).toFixed(1) : 0; return ` ${escapeHtml(series.title)} ${series.imdb_id} ${series.total_episodes} ${series.episodes_with_dates} (${progressPercent}%) ${series.episodes_with_video} `; }).join(''); } function updateSeriesPagination(data) { const pagination = document.getElementById('series-pagination'); if (data.pages <= 1) { pagination.innerHTML = ''; return; } let html = ''; if (data.has_prev) { html += ``; } html += `Page ${data.page} of ${data.pages}`; if (data.has_next) { html += ``; } pagination.innerHTML = html; } function refreshSeries() { loadSeries(currentSeriesPage); } async function viewSeriesEpisodes(imdbId) { try { const data = await apiCall(`/api/series/${imdbId}/episodes`); showEpisodesModal(data); } catch (error) { console.error('Failed to load episodes:', error); } } function showEpisodesModal(data) { const modalHtml = ` `; document.body.insertAdjacentHTML('beforeend', modalHtml); } function closeEpisodesModal() { const modal = document.getElementById('episodes-modal'); if (modal) { modal.remove(); } } // Reports async function loadReport() { try { const data = await apiCall('/api/reports/missing-dates'); updateReportSummary(data.summary); updateReportTables(data); } catch (error) { console.error('Failed to load report:', error); } } function updateReportSummary(summary) { document.getElementById('report-movies-with').textContent = summary.movies_with_dates; document.getElementById('report-movies-missing').textContent = summary.movies_missing_dates; document.getElementById('report-episodes-with').textContent = summary.episodes_with_dates; document.getElementById('report-episodes-missing').textContent = summary.episodes_missing_dates; } function updateReportTables(data) { // Movies missing dates const moviesTbody = document.getElementById('report-movies-tbody'); if (data.movies_missing.length === 0) { moviesTbody.innerHTML = 'No movies missing dates'; } else { moviesTbody.innerHTML = data.movies_missing.map(movie => ` ${escapeHtml(movie.title)} ${movie.imdb_id} ${movie.released || '-'} ${movie.source || 'Unknown'} `).join(''); } // Episodes missing dates const episodesTbody = document.getElementById('report-episodes-tbody'); if (data.episodes_missing.length === 0) { episodesTbody.innerHTML = 'No episodes missing dates'; } else { episodesTbody.innerHTML = data.episodes_missing.map(episode => ` ${escapeHtml(episode.series_title)} S${episode.season.toString().padStart(2, '0')}E${episode.episode.toString().padStart(2, '0')} ${episode.imdb_id} ${episode.aired || '-'} ${episode.source || 'Unknown'} `).join(''); } } function refreshReport() { loadReport(); } // Quick fix functions function quickFixMovie(imdbId, released) { let newSource = 'manual'; let newDate = null; if (released) { newSource = 'digital_release'; newDate = released + 'T00:00:00'; } else { newDate = new Date().toISOString().slice(0, 16); } updateMovieDate(imdbId, newDate, newSource); } function quickFixEpisode(imdbId, season, episode, aired) { let newSource = 'manual'; let newDate = null; if (aired) { newSource = 'airdate'; newDate = aired + 'T00:00:00'; } else { newDate = new Date().toISOString().slice(0, 16); } updateEpisodeDate(imdbId, season, episode, newDate, newSource); } // Tools async function loadDetailedStats() { try { const data = await apiCall('/api/dashboard'); const statsHtml = `
Database Size: ${data.database_size_mb} MB
Total Movies: ${data.movies_total} (${data.movies_with_video} with video files)
Movies with Dates: ${data.movies_with_dates} (${((data.movies_with_dates / data.movies_total) * 100).toFixed(1)}%)
Total Series: ${data.series_count}
Total Episodes: ${data.episodes_total} (${data.episodes_with_video} with video files)
Episodes with Dates: ${data.episodes_with_dates} (${((data.episodes_with_dates / data.episodes_total) * 100).toFixed(1)}%)
Processing History: ${data.processing_history_count} events
`; document.getElementById('detailed-stats').innerHTML = statsHtml; } catch (error) { console.error('Failed to load detailed stats:', error); } } async function handleBulkUpdate(event) { event.preventDefault(); const mediaType = document.getElementById('bulk-media-type').value; const oldSource = document.getElementById('bulk-old-source').value; const newSource = document.getElementById('bulk-new-source').value; if (!mediaType || !oldSource || !newSource) { showToast('Please fill in all fields', 'warning'); return; } if (!confirm(`This will update all ${mediaType} with source "${oldSource}" to "${newSource}". Continue?`)) { return; } try { const result = await apiCall('/api/bulk/update-source', { method: 'POST', body: JSON.stringify({ media_type: mediaType, old_source: oldSource, new_source: newSource }) }); showToast(result.message, 'success'); // Reset form document.getElementById('bulk-update-form').reset(); // Refresh current tab if (currentTab === 'movies') loadMovies(currentMoviesPage); if (currentTab === 'tv') loadSeries(currentSeriesPage); if (currentTab === 'reports') loadReport(); if (currentTab === 'dashboard') loadDashboard(); } catch (error) { console.error('Bulk update failed:', error); } } // Edit modal functions function editMovie(imdbId, dateadded, source) { document.getElementById('modal-title').textContent = `Edit Movie: ${imdbId}`; document.getElementById('edit-imdb-id').value = imdbId; document.getElementById('edit-media-type').value = 'movie'; document.getElementById('edit-season').value = ''; document.getElementById('edit-episode').value = ''; // Convert dateadded to datetime-local format if present if (dateadded && dateadded !== '-') { try { const date = new Date(dateadded); document.getElementById('edit-dateadded').value = date.toISOString().slice(0, 16); } catch (e) { document.getElementById('edit-dateadded').value = ''; } } else { document.getElementById('edit-dateadded').value = ''; } document.getElementById('edit-source').value = source || 'manual'; document.getElementById('edit-modal').classList.add('active'); } function editEpisode(imdbId, season, episode, dateadded, source) { document.getElementById('modal-title').textContent = `Edit Episode: ${imdbId} S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`; document.getElementById('edit-imdb-id').value = imdbId; document.getElementById('edit-media-type').value = 'episode'; document.getElementById('edit-season').value = season; document.getElementById('edit-episode').value = episode; // Convert dateadded to datetime-local format if present if (dateadded && dateadded !== '-') { try { const date = new Date(dateadded); document.getElementById('edit-dateadded').value = date.toISOString().slice(0, 16); } catch (e) { document.getElementById('edit-dateadded').value = ''; } } else { document.getElementById('edit-dateadded').value = ''; } document.getElementById('edit-source').value = source || 'manual'; document.getElementById('edit-modal').classList.add('active'); } function closeModal() { document.getElementById('edit-modal').classList.remove('active'); } async function handleEditSubmit(event) { event.preventDefault(); const imdbId = document.getElementById('edit-imdb-id').value; const mediaType = document.getElementById('edit-media-type').value; const season = document.getElementById('edit-season').value; const episode = document.getElementById('edit-episode').value; const dateadded = document.getElementById('edit-dateadded').value; const source = document.getElementById('edit-source').value; // Convert datetime-local to ISO string const isoDateadded = dateadded ? new Date(dateadded).toISOString() : null; try { if (mediaType === 'movie') { await updateMovieDate(imdbId, isoDateadded, source); } else { await updateEpisodeDate(imdbId, parseInt(season), parseInt(episode), isoDateadded, source); } closeModal(); } catch (error) { console.error('Update failed:', error); } } // Update functions async function updateMovieDate(imdbId, dateadded, source) { try { const result = await apiCall(`/api/movies/${imdbId}`, { method: 'PUT', body: JSON.stringify({ imdb_id: imdbId, dateadded: dateadded, source: source }) }); showToast(result.message, 'success'); // Refresh current view if (currentTab === 'movies') loadMovies(currentMoviesPage); if (currentTab === 'reports') loadReport(); if (currentTab === 'dashboard') loadDashboard(); } catch (error) { console.error('Movie update failed:', error); } } async function updateEpisodeDate(imdbId, season, episode, dateadded, source) { try { const result = await apiCall(`/api/episodes/${imdbId}/${season}/${episode}`, { method: 'PUT', body: JSON.stringify({ imdb_id: imdbId, season: season, episode: episode, dateadded: dateadded, source: source }) }); showToast(result.message, 'success'); // Refresh current view if (currentTab === 'tv') loadSeries(currentSeriesPage); if (currentTab === 'reports') loadReport(); if (currentTab === 'dashboard') loadDashboard(); // Refresh episodes modal if open const episodesModal = document.getElementById('episodes-modal'); if (episodesModal) { closeEpisodesModal(); setTimeout(() => viewSeriesEpisodes(imdbId), 100); } } catch (error) { console.error('Episode update failed:', error); } } // Utility functions function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } function formatDateTime(dateString) { try { const date = new Date(dateString); return date.toLocaleString(); } catch (e) { return dateString; } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function showToast(message, type = 'info') { const container = document.getElementById('toast-container'); const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.innerHTML = `
${escapeHtml(message)}
`; container.appendChild(toast); // Auto remove after 5 seconds setTimeout(() => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }, 5000); // Remove on click toast.addEventListener('click', () => { if (toast.parentNode) { toast.parentNode.removeChild(toast); } }); }