// 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();
checkAuthStatus(); // Check authentication status on page load
loadDashboard();
loadSeriesSources();
});
// 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('movies-imdb-search').addEventListener('input', debounce(loadMovies, 500));
document.getElementById('series-search').addEventListener('input', debounce(loadSeries, 500));
document.getElementById('series-imdb-search').addEventListener('input', debounce(loadSeries, 500));
// Filter dropdowns
document.getElementById('movies-filter-date').addEventListener('change', loadMovies);
document.getElementById('movies-filter-source').addEventListener('change', loadMovies);
document.getElementById('series-filter-date').addEventListener('change', loadSeries);
document.getElementById('series-filter-source').addEventListener('change', loadSeries);
// Forms
document.getElementById('edit-form').addEventListener('submit', handleEditSubmit);
document.getElementById('bulk-update-form').addEventListener('submit', handleBulkUpdate);
document.getElementById('manual-scan-form').addEventListener('submit', handleManualScan);
document.getElementById('populate-form').addEventListener('submit', handlePopulateDatabase);
}
// 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;
// Debug: Log dashboard data to see what fields are available
console.log('Dashboard data received:', dashboardData);
const moviesTotal = dashboardData.movies_total || 0;
const moviesWithDates = dashboardData.movies_with_dates || 0;
const moviesWithoutDates = dashboardData.movies_without_dates || (moviesTotal - moviesWithDates);
const episodesTotal = dashboardData.episodes_total || 0;
const episodesWithDates = dashboardData.episodes_with_dates || 0;
const episodesWithoutDates = dashboardData.episodes_without_dates || (episodesTotal - episodesWithDates);
document.getElementById('movies-total').textContent = moviesTotal;
document.getElementById('movies-with-dates').textContent = `${moviesWithDates} with dates, ${moviesWithoutDates} without`;
document.getElementById('series-total').textContent = dashboardData.series_count || 0;
document.getElementById('episodes-total').textContent = `${episodesTotal} episodes (${episodesWithDates} with dates, ${episodesWithoutDates} without)`;
const missingTotal = moviesWithoutDates + episodesWithoutDates;
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 = `${moviesWithoutDates} movies, ${episodesWithoutDates} episodes without dates`;
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) {
// Ensure page is a valid number
if (isNaN(page) || page < 1) {
page = 1;
}
const search = document.getElementById('movies-search').value;
const imdbSearch = document.getElementById('movies-imdb-search').value;
const hasDate = document.getElementById('movies-filter-date').value;
const sourceFilter = document.getElementById('movies-filter-source').value;
const skip = (page - 1) * 100;
console.log(`DEBUG: loadMovies called with page=${page}, calculated skip=${skip}`);
const params = new URLSearchParams({
skip: skip,
limit: 100
});
if (search) params.append('search', search);
if (imdbSearch) params.append('imdb_search', imdbSearch);
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 = (isNaN(page) || page < 1) ? 1 : 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 hasVideoBadge = movie.has_video_file ?
'Yes' :
'No';
// Determine date type based on source and dates
let dateType = 'Unknown';
let dateTypeBadge = 'badge-secondary';
if (movie.source === 'digital_release') {
dateType = 'Digital Release';
dateTypeBadge = 'badge-success';
} else if (movie.source && movie.source.includes('radarr') && movie.source.includes('import')) {
dateType = 'Radarr Import';
dateTypeBadge = 'badge-warning';
} else if (movie.source === 'manual') {
dateType = 'Manual';
dateTypeBadge = 'badge-info';
} else if (movie.source === 'nfo_file_existing') {
dateType = 'Existing NFO';
dateTypeBadge = 'badge-secondary';
} else if (movie.source === 'no_valid_date_source') {
dateType = 'No Valid Source';
dateTypeBadge = 'badge-danger';
} else if (movie.source && movie.source.toLowerCase().includes('tmdb:theatrical')) {
dateType = 'TMDB Theatrical';
dateTypeBadge = 'badge-primary';
} else if (movie.source && movie.source.toLowerCase().includes('tmdb:digital')) {
dateType = 'TMDB Digital';
dateTypeBadge = 'badge-primary';
} else if (movie.source && movie.source.toLowerCase().includes('tmdb:physical')) {
dateType = 'TMDB Physical';
dateTypeBadge = 'badge-primary';
} else if (movie.source && movie.source.toLowerCase().includes('tmdb:')) {
dateType = 'TMDB Release';
dateTypeBadge = 'badge-primary';
} else if (movie.source && movie.source.toLowerCase().includes('omdb:')) {
dateType = 'OMDb Release';
dateTypeBadge = 'badge-info';
} else if (movie.source && movie.source.toLowerCase().includes('webhook:')) {
dateType = 'Webhook/API';
dateTypeBadge = 'badge-warning';
}
return `
| ${escapeHtml(movie.title)} |
${movie.imdb_id} |
${movie.released || '-'} |
${dateadded || '-'} |
${movie.source_description || movie.source || 'Unknown'} |
${dateType} |
${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;
}
}
async function loadSeriesSources() {
try {
const data = await apiCall('/api/series/sources');
const select = document.getElementById('series-filter-source');
const currentValue = select.value;
select.innerHTML = '';
data.sources.forEach(source => {
select.innerHTML += ``;
});
select.value = currentValue;
} catch (error) {
console.error('Failed to load series sources:', error);
}
}
function refreshMovies() {
loadMovies(isNaN(currentMoviesPage) ? 1 : currentMoviesPage);
}
// TV Series
async function loadSeries(page = 1) {
// Ensure page is a valid number
if (isNaN(page) || page < 1) {
page = 1;
}
const search = document.getElementById('series-search').value;
const imdbSearch = document.getElementById('series-imdb-search').value;
const dateFilter = document.getElementById('series-filter-date').value;
const sourceFilter = document.getElementById('series-filter-source').value;
const skip = (page - 1) * 50;
console.log(`DEBUG: loadSeries called with page=${page}, calculated skip=${skip}`);
const params = new URLSearchParams({
skip: skip,
limit: 50
});
if (search) params.append('search', search);
if (imdbSearch) params.append('imdb_search', imdbSearch);
if (dateFilter) params.append('date_filter', dateFilter);
if (sourceFilter) params.append('source_filter', sourceFilter);
try {
const data = await apiCall(`/api/series?${params}`);
updateSeriesTable(data);
updateSeriesPagination(data);
currentSeriesPage = (isNaN(page) || page < 1) ? 1 : 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(isNaN(currentSeriesPage) ? 1 : 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) {
// Calculate statistics
const totalEpisodes = data.episodes.length;
const episodesWithDates = data.episodes.filter(ep => ep.dateadded && ep.dateadded.trim() !== '').length;
const episodesWithoutDates = totalEpisodes - episodesWithDates;
const episodesWithVideo = data.episodes.filter(ep => ep.has_video_file).length;
const modalHtml = `
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
}
function filterEpisodes(filterType) {
const rows = document.querySelectorAll('#episodes-table-body tr');
rows.forEach(row => {
const hasDate = row.getAttribute('data-has-date') === 'true';
let shouldShow = true;
switch (filterType) {
case 'missing':
shouldShow = !hasDate;
break;
case 'has-dates':
shouldShow = hasDate;
break;
case 'all':
default:
shouldShow = true;
break;
}
row.style.display = shouldShow ? '' : 'none';
});
}
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_description || 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_description || episode.source || 'Unknown'} |
|
`).join('');
}
}
function refreshReport() {
loadReport();
}
// Smart fix functions
async function smartFixMovie(imdbId) {
try {
console.log('đ SMART FIX: Loading options for movie', imdbId);
const options = await apiCall(`/api/movies/${imdbId}/date-options`);
console.log('đ SMART FIX: Received options:', options);
showSmartFixModal('movie', options);
} catch (error) {
console.error('Failed to load movie options:', error);
showToast('Failed to load movie options', 'error');
}
}
async function smartFixEpisode(imdbId, season, episode) {
// Validate parameters
if (!imdbId || season === undefined || season === null || episode === undefined || episode === null) {
console.error('smartFixEpisode: Invalid parameters:', {imdbId, season, episode});
showToast('Invalid episode parameters', 'error');
return;
}
try {
const options = await apiCall(`/api/episodes/${imdbId}/${season}/${episode}/date-options`);
showSmartFixModal('episode', options);
} catch (error) {
console.error('Failed to load episode options:', error);
showToast('Failed to load episode options', 'error');
}
}
function showSmartFixModal(mediaType, options) {
console.log('đ SMART FIX: Showing modal for', mediaType, 'with options:', options);
const modal = document.getElementById('smart-fix-modal');
const title = document.getElementById('smart-fix-title');
const content = document.getElementById('smart-fix-content');
if (!modal || !title || !content) {
console.error('â SMART FIX: Modal elements not found!', {modal, title, content});
alert('Smart Fix modal not found - check console for details');
return;
}
console.log('â
SMART FIX: Modal elements found, proceeding to show Smart Fix modal');
if (mediaType === 'movie') {
title.textContent = `Fix Date for Movie: ${options.imdb_id}`;
} else {
// Add validation for episode data
const season = options.season || 0;
const episode = options.episode || 0;
title.textContent = `Fix Date for Episode: ${options.imdb_id} S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`;
}
// Build options HTML
let optionsHtml = '';
options.options.forEach((option, index) => {
const isChecked = index === 0 ? 'checked' : '';
const dateInput = option.type === 'manual' ?
`
` : '';
optionsHtml += `
`;
});
optionsHtml += '
';
optionsHtml += `
`;
content.innerHTML = optionsHtml;
modal.classList.add('active');
}
function closeSmartFixModal() {
document.getElementById('smart-fix-modal').classList.remove('active');
}
async function applySmartFix(mediaType, options) {
const selectedRadio = document.querySelector('input[name="date-option"]:checked');
if (!selectedRadio) {
showToast('Please select a date option', 'warning');
return;
}
const selectedIndex = selectedRadio.value;
const selectedOption = options.options[selectedIndex];
let dateadded = selectedOption.date;
let source = selectedOption.source;
// Handle manual date entry
if (selectedOption.type === 'manual') {
const manualDateInput = document.getElementById(`manual-date-${selectedIndex}`);
if (manualDateInput && manualDateInput.value) {
try {
dateadded = new Date(manualDateInput.value).toISOString();
} catch (e) {
showToast('Invalid date format', 'error');
return;
}
} else {
showToast('Please enter a date for manual option', 'warning');
return;
}
} else if (dateadded) {
// Fix date format for non-manual options
try {
let dateValue = dateadded;
// Handle timezone offsets
if (dateValue.includes('+00:00')) {
dateValue = dateValue.replace('+00:00', 'Z');
}
const date = new Date(dateValue);
if (isNaN(date.getTime())) {
showToast('Invalid date from server', 'error');
return;
}
dateadded = date.toISOString();
} catch (e) {
console.error('Date conversion error:', e, dateadded);
showToast('Date conversion error', 'error');
return;
}
}
// Debug logging
console.log('đ SMART FIX DEBUG:', {
mediaType,
imdb_id: options.imdb_id,
selectedOption,
dateadded,
source,
originalDate: selectedOption.date
});
try {
if (mediaType === 'movie') {
await updateMovieDate(options.imdb_id, dateadded, source);
} else {
await updateEpisodeDate(options.imdb_id, options.season, options.episode, dateadded, source);
}
closeSmartFixModal();
} catch (error) {
console.error('Smart fix failed:', error);
showToast('Smart fix failed: ' + error.message, 'error');
}
}
// 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
async function editMovie(imdbId, dateadded, source) {
try {
// Load movie options to populate available dates
const options = await apiCall(`/api/movies/${imdbId}/date-options`);
showEnhancedEditModal('movie', options, dateadded, source);
} catch (error) {
console.error('Failed to load movie options for edit:', error);
// Fallback to basic edit modal
showBasicEditModal('movie', imdbId, dateadded, source);
}
}
function showEnhancedEditModal(mediaType, options, currentDateadded, currentSource) {
const modal = document.getElementById('edit-modal');
const title = document.getElementById('modal-title');
const modalBody = document.querySelector('#edit-modal .modal-body');
if (mediaType === 'movie') {
title.textContent = `Edit Movie: ${options.imdb_id}`;
} else {
// Add validation for episode data
const season = options.season || 0;
const episode = options.episode || 0;
title.textContent = `Edit Episode: ${options.imdb_id} S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`;
}
// Build enhanced edit form with date options
let formHtml = `
${mediaType === 'episode' ? `
` : `
`}
Adjust the date/time as needed
`;
modalBody.innerHTML = formHtml;
// Set current values
if (currentDateadded && currentDateadded !== '-') {
try {
const date = new Date(currentDateadded);
document.getElementById('edit-dateadded').value = date.toISOString().slice(0, 16);
} catch (e) {
document.getElementById('edit-dateadded').value = '';
}
}
document.getElementById('edit-source').value = currentSource || 'manual';
// Store options for later use
modal.dataset.options = JSON.stringify(options);
modal.classList.add('active');
}
function showBasicEditModal(mediaType, imdbId, dateadded, source) {
// Fallback to original basic edit modal
document.getElementById('modal-title').textContent = `Edit ${mediaType}: ${imdbId}`;
document.getElementById('edit-imdb-id').value = imdbId;
document.getElementById('edit-media-type').value = mediaType;
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 updateEditDateFromOption(optionIndex, option) {
const dateInput = document.getElementById('edit-dateadded');
const sourceSelect = document.getElementById('edit-source');
if (option.date) {
// Convert to datetime-local format with better date parsing
try {
let dateValue = option.date;
// Handle timezone offsets by converting to local time
if (dateValue.includes('+00:00') || dateValue.includes('Z')) {
dateValue = dateValue.replace('+00:00', 'Z');
}
const date = new Date(dateValue);
if (isNaN(date.getTime())) {
console.error('Invalid date:', dateValue);
dateInput.value = '';
} else {
// Convert to local datetime-local format
const localDateTime = new Date(date.getTime() - (date.getTimezoneOffset() * 60000));
dateInput.value = localDateTime.toISOString().slice(0, 16);
}
} catch (e) {
console.error('Date parsing error:', e, option.date);
dateInput.value = '';
}
} else {
// Manual option - clear the date for user input
dateInput.value = '';
}
sourceSelect.value = option.source;
}
async function handleEnhancedEditSubmit(event) {
event.preventDefault();
const modal = document.getElementById('edit-modal');
const options = JSON.parse(modal.dataset.options);
const imdbId = options.imdb_id;
const mediaType = document.getElementById('edit-media-type').value;
const dateadded = document.getElementById('edit-dateadded').value;
const source = document.getElementById('edit-source').value;
if (!dateadded) {
showToast('Please enter a date', 'warning');
return;
}
// Convert datetime-local to ISO string with error handling
let isoDateadded = null;
try {
isoDateadded = new Date(dateadded).toISOString();
} catch (e) {
showToast('Invalid date format', 'error');
return;
}
try {
if (mediaType === 'movie') {
await updateMovieDate(imdbId, isoDateadded, source);
} else {
await updateEpisodeDate(imdbId, options.season, options.episode, isoDateadded, source);
}
closeModal();
} catch (error) {
console.error('Enhanced edit failed:', error);
showToast('Update failed: ' + error.message, 'error');
}
}
async function editEpisode(imdbId, season, episode, dateadded, source) {
// Validate parameters
if (!imdbId || season === undefined || season === null || episode === undefined || episode === null) {
console.error('editEpisode: Invalid parameters:', {imdbId, season, episode});
showToast('Invalid episode parameters', 'error');
return;
}
try {
// Load episode options to populate available dates
const options = await apiCall(`/api/episodes/${imdbId}/${season}/${episode}/date-options`);
showEnhancedEditModal('episode', options, dateadded, source);
} catch (error) {
console.error('Failed to load episode options for edit:', error);
// Fallback to basic edit modal
showBasicEditModal('episode', imdbId, dateadded, source, season, episode);
}
}
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({
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({
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);
}
});
}
// Debug function
async function debugMovie(imdbId) {
try {
const data = await apiCall(`/api/debug/movie/${imdbId}/raw`);
const debugInfo = `
DEBUG INFO for ${imdbId}:
Raw Database Data:
- imdb_id: ${data.raw_data.imdb_id}
- path: ${data.raw_data.path}
- released: ${data.raw_data.released}
- dateadded: ${data.raw_data.dateadded}
- source: ${data.raw_data.source}
- has_video_file: ${data.raw_data.has_video_file}
- last_updated: ${data.raw_data.last_updated}
Analysis:
- Movie Released: ${data.raw_data.released || 'Not set'}
- Library Import Date: ${data.raw_data.dateadded || 'Not set'}
- Date Source: ${data.raw_data.source_description || data.raw_data.source || 'Unknown'}
`;
alert(debugInfo);
console.log('đ Debug data for', imdbId, data);
} catch (error) {
console.error('Debug failed:', error);
showToast('Debug failed: ' + error.message, 'error');
}
}
// Episode deletion functionality
async function deleteEpisode(imdbId, season, episode) {
// Validate parameters
if (!imdbId || season === undefined || season === null || episode === undefined || episode === null) {
console.error('deleteEpisode: Invalid parameters:', {imdbId, season, episode});
showToast('Invalid episode parameters', 'error');
return;
}
const episodeStr = `S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}`;
// Confirmation dialog
if (!confirm(`â ī¸ Delete Episode ${episodeStr}?\n\nThis will permanently remove the episode from the database.\n\nAre you sure you want to continue?`)) {
return;
}
try {
const response = await fetch(`/api/episodes/${imdbId}/${season}/${episode}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (response.ok && result.success) {
showToast(`â
Episode ${episodeStr} deleted successfully`, 'success');
// Remove the row from the table
const rows = document.querySelectorAll('#episodes-table-body tr');
rows.forEach(row => {
const episodeCell = row.querySelector('td:first-child');
if (episodeCell && episodeCell.textContent === episodeStr) {
row.remove();
}
});
// Update episode counts in modal header
updateEpisodeModalCounts();
} else {
const errorMsg = result.message || result.error || 'Unknown error';
showToast(`â Failed to delete episode: ${errorMsg}`, 'error');
}
} catch (error) {
console.error('Delete episode failed:', error);
showToast(`â Delete failed: ${error.message}`, 'error');
}
}
// Movie deletion functionality
async function deleteMovie(imdbId) {
// Confirmation dialog
if (!confirm(`â ī¸ Delete Movie?\n\nThis will permanently remove the movie (${imdbId}) from the database.\n\nAre you sure you want to continue?`)) {
return;
}
try {
const response = await fetch(`/api/movies/${imdbId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json();
if (response.ok && result.success) {
showToast(`â
Movie deleted successfully`, 'success');
// Refresh the movies table
loadMovies(currentMoviesPage);
} else {
const errorMsg = result.message || result.error || 'Unknown error';
showToast(`â Failed to delete movie: ${errorMsg}`, 'error');
}
} catch (error) {
console.error('Delete movie failed:', error);
showToast(`â Delete failed: ${error.message}`, 'error');
}
}
// Update episode counts in modal after deletion
function updateEpisodeModalCounts() {
const remainingRows = document.querySelectorAll('#episodes-table-body tr');
const totalEpisodes = remainingRows.length;
const episodesWithDates = Array.from(remainingRows).filter(row =>
row.getAttribute('data-has-date') === 'true'
).length;
const episodesWithoutDates = totalEpisodes - episodesWithDates;
// Update the stats in the modal
const statsDiv = document.querySelector('.episode-stats');
if (statsDiv) {
// Keep the existing "With Video" count by finding it
const videoCountDiv = statsDiv.querySelector('div:nth-child(4)');
const videoCountText = videoCountDiv ? videoCountDiv.innerHTML : 'With Video: -
';
statsDiv.innerHTML = `
Total Episodes: ${totalEpisodes}
With Dates: ${episodesWithDates}
Missing Dates: ${episodesWithoutDates}
${videoCountText}
`;
}
}
// ===========================
// Authentication Functions
// ===========================
async function checkAuthStatus() {
try {
const response = await fetch('/api/auth/status');
const authStatus = await response.json();
const authStatusDiv = document.getElementById('auth-status');
const authUsernameSpan = document.getElementById('auth-username');
if (authStatus.auth_enabled && authStatus.authenticated) {
// Show authentication status with username
authUsernameSpan.textContent = authStatus.username;
authStatusDiv.style.display = 'flex';
} else if (authStatus.auth_enabled && !authStatus.authenticated) {
// This shouldn't happen if middleware is working, but handle it
console.warn('Auth enabled but not authenticated - middleware may be misconfigured');
} else {
// Authentication disabled - hide auth status
authStatusDiv.style.display = 'none';
}
} catch (error) {
console.error('Failed to check authentication status:', error);
// Hide auth status on error
document.getElementById('auth-status').style.display = 'none';
}
}
// Manual Scan Functions
async function handleManualScan(event) {
event.preventDefault();
const scanType = document.getElementById('scan-type').value;
const scanMode = document.getElementById('scan-mode').value;
const scanPath = document.getElementById('scan-path').value.trim();
// Validate inputs
if (!scanType || !scanMode) {
showToast('â Please select scan type and mode', 'error');
return;
}
// Build query parameters
const params = new URLSearchParams({
scan_type: scanType,
scan_mode: scanMode
});
if (scanPath) {
params.append('path', scanPath);
}
try {
// Show scan status
showScanStatus();
// Start the scan
showToast('đ Starting manual scan...', 'info');
const response = await fetch(`/manual/scan?${params}`, {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
if (result.status === 'started') {
showToast('â
Scan started successfully', 'success');
// Start polling for status
startScanPolling();
} else {
showToast(`âšī¸ ${result.message || 'Scan completed'}`, 'info');
hideScanStatus();
}
} catch (error) {
console.error('Manual scan failed:', error);
showToast(`â Scan failed: ${error.message}`, 'error');
hideScanStatus();
}
}
function showScanStatus() {
const scanStatus = document.getElementById('scan-status');
const progressBar = document.getElementById('scan-progress-bar');
const operationText = document.getElementById('scan-current-operation');
const progressText = document.getElementById('scan-progress-text');
// Reset and show
progressBar.style.width = '0%';
operationText.textContent = 'Initializing scan...';
progressText.textContent = '0%';
scanStatus.style.display = 'block';
}
function hideScanStatus() {
document.getElementById('scan-status').style.display = 'none';
if (window.scanPollingInterval) {
clearInterval(window.scanPollingInterval);
window.scanPollingInterval = null;
}
}
function stopScanPolling() {
hideScanStatus();
}
function startScanPolling() {
// Poll every 2 seconds for scan status
window.scanPollingInterval = setInterval(async () => {
try {
const response = await fetch('/api/scan/status');
if (!response.ok) {
throw new Error('Failed to get scan status');
}
const status = await response.json();
updateScanProgress(status);
// Stop polling if scan is complete
if (!status.scanning) {
stopScanPolling();
showToast('â
Scan completed!', 'success');
}
} catch (error) {
console.error('Failed to poll scan status:', error);
// Don't show error toast repeatedly, just stop polling
stopScanPolling();
}
}, 2000);
}
function updateScanProgress(status) {
const progressBar = document.getElementById('scan-progress-bar');
const operationText = document.getElementById('scan-current-operation');
const progressText = document.getElementById('scan-progress-text');
if (!status.scanning) {
progressBar.style.width = '100%';
operationText.textContent = 'Scan completed';
progressText.textContent = '100%';
return;
}
// Calculate overall progress
let totalProgress = 0;
let progressDetails = '';
if (status.scan_type === 'both' || status.scan_type === 'tv') {
const tvProgress = status.tv_series_total > 0 ?
((status.tv_series_processed + status.tv_series_skipped) / status.tv_series_total) * 50 : 0;
totalProgress += tvProgress;
if (status.tv_series_total > 0) {
progressDetails += `TV: ${status.tv_series_processed + status.tv_series_skipped}/${status.tv_series_total} `;
}
}
if (status.scan_type === 'both' || status.scan_type === 'movies') {
const movieProgress = status.movies_total > 0 ?
((status.movies_processed + status.movies_skipped) / status.movies_total) * 50 : 0;
totalProgress += movieProgress;
if (status.movies_total > 0) {
progressDetails += `Movies: ${status.movies_processed + status.movies_skipped}/${status.movies_total}`;
}
}
// For single type scans, use full 100%
if (status.scan_type !== 'both') {
totalProgress *= 2;
}
// Update progress bar
progressBar.style.width = `${Math.min(totalProgress, 100)}%`;
progressText.textContent = `${Math.round(totalProgress)}%`;
// Update operation text
if (status.current_operation) {
operationText.textContent = status.current_operation;
} else if (status.current_item) {
operationText.textContent = `Processing: ${status.current_item}`;
} else {
operationText.textContent = progressDetails || 'Scanning...';
}
}
async function logout() {
if (!confirm('Are you sure you want to logout?')) {
return;
}
try {
const response = await fetch('/api/auth/logout', {
method: 'POST',
credentials: 'same-origin'
});
if (response.ok) {
showToast('â
Logged out successfully', 'success');
// Reload page to trigger authentication prompt
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
showToast('â Logout failed', 'error');
}
} catch (error) {
console.error('Logout failed:', error);
showToast('â Logout error', 'error');
}
}
// Bulk delete functions for TV episodes
function toggleSelectAll() {
const selectAllCheckbox = document.getElementById('select-all-checkbox');
const episodeCheckboxes = document.querySelectorAll('.episode-checkbox');
if (selectAllCheckbox && episodeCheckboxes.length > 0) {
const shouldCheck = selectAllCheckbox.checked;
episodeCheckboxes.forEach(checkbox => {
checkbox.checked = shouldCheck;
});
updateBulkDeleteButton();
}
}
function updateBulkDeleteButton() {
const selectedCheckboxes = document.querySelectorAll('.episode-checkbox:checked');
const selectedCount = selectedCheckboxes.length;
const bulkDeleteButton = document.getElementById('bulk-delete-selected');
const bulkUpdateButton = document.getElementById('bulk-update-dates');
const selectedCountSpan = document.getElementById('selected-count');
const updateCountSpan = document.getElementById('update-count');
if (selectedCountSpan) {
selectedCountSpan.textContent = selectedCount;
}
if (updateCountSpan) {
updateCountSpan.textContent = selectedCount;
}
if (bulkDeleteButton) {
bulkDeleteButton.disabled = selectedCount === 0;
}
if (bulkUpdateButton) {
bulkUpdateButton.disabled = selectedCount === 0;
}
// Update select all checkbox state
const selectAllCheckbox = document.getElementById('select-all-checkbox');
const allCheckboxes = document.querySelectorAll('.episode-checkbox');
if (selectAllCheckbox && allCheckboxes.length > 0) {
selectAllCheckbox.checked = selectedCount === allCheckboxes.length;
selectAllCheckbox.indeterminate = selectedCount > 0 && selectedCount < allCheckboxes.length;
}
}
async function bulkDeleteSelected() {
const selectedCheckboxes = document.querySelectorAll('.episode-checkbox:checked');
const selectedCount = selectedCheckboxes.length;
if (selectedCount === 0) {
showToast('â No episodes selected', 'error');
return;
}
if (!confirm(`Are you sure you want to delete ${selectedCount} episode(s)? This action cannot be undone.`)) {
return;
}
const bulkDeleteButton = document.getElementById('bulk-delete-selected');
const originalText = bulkDeleteButton.innerHTML;
bulkDeleteButton.innerHTML = ' Deleting...';
bulkDeleteButton.disabled = true;
let successCount = 0;
let failCount = 0;
// Process deletions
for (const checkbox of selectedCheckboxes) {
const row = checkbox.closest('tr');
const imdbId = row.getAttribute('data-imdb');
const season = parseInt(row.getAttribute('data-season'));
const episode = parseInt(row.getAttribute('data-episode'));
try {
const response = await apiCall(`/api/episodes/${imdbId}/${season}/${episode}`, {
method: 'DELETE'
});
if (response.success) {
// Remove the row from the table
row.remove();
successCount++;
} else {
failCount++;
console.error(`Failed to delete S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}:`, response.message);
}
} catch (error) {
failCount++;
console.error(`Error deleting S${season.toString().padStart(2, '0')}E${episode.toString().padStart(2, '0')}:`, error);
}
}
// Update UI
updateEpisodeModalCounts();
updateBulkDeleteButton();
// Reset button
bulkDeleteButton.innerHTML = originalText;
bulkDeleteButton.disabled = true;
// Show results
if (successCount > 0 && failCount === 0) {
showToast(`â
Successfully deleted ${successCount} episode(s)`, 'success');
} else if (successCount > 0 && failCount > 0) {
showToast(`â ī¸ Deleted ${successCount} episode(s), ${failCount} failed`, 'warning');
} else {
showToast(`â Failed to delete ${failCount} episode(s)`, 'error');
}
}
// Bulk update dates functions
function showBulkUpdateModal() {
const selectedCheckboxes = document.querySelectorAll('.episode-checkbox:checked');
const selectedCount = selectedCheckboxes.length;
if (selectedCount === 0) {
showToast('â No episodes selected', 'error');
return;
}
const modal = document.getElementById('bulk-update-modal');
const modalSelectedCount = document.getElementById('modal-selected-count');
const dateSourceSelect = document.getElementById('bulk-date-source');
const customDateInput = document.getElementById('custom-date-input');
if (modalSelectedCount) {
modalSelectedCount.textContent = selectedCount;
}
// Show/hide custom date input based on selection
if (dateSourceSelect) {
dateSourceSelect.onchange = function() {
if (customDateInput) {
customDateInput.style.display = this.value === 'custom' ? 'block' : 'none';
}
};
}
if (modal) {
modal.style.display = 'flex';
}
}
function closeBulkUpdateModal() {
const modal = document.getElementById('bulk-update-modal');
if (modal) {
modal.style.display = 'none';
}
}
async function executeBulkUpdate() {
const selectedCheckboxes = document.querySelectorAll('.episode-checkbox:checked');
const selectedCount = selectedCheckboxes.length;
const dateSource = document.getElementById('bulk-date-source').value;
const customDate = document.getElementById('bulk-custom-date').value;
if (selectedCount === 0) {
showToast('â No episodes selected', 'error');
return;
}
if (dateSource === 'custom' && !customDate) {
showToast('â Please enter a custom date', 'error');
return;
}
// Close modal
closeBulkUpdateModal();
// Collect selected episodes
const episodes = [];
selectedCheckboxes.forEach(checkbox => {
const row = checkbox.closest('tr');
episodes.push({
imdb_id: row.getAttribute('data-imdb'),
season: parseInt(row.getAttribute('data-season')),
episode: parseInt(row.getAttribute('data-episode'))
});
});
// Show progress
showToast(`âŗ Updating ${selectedCount} episode(s)...`, 'info');
try {
const response = await apiCall('/api/episodes/bulk-update-dates', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
episodes: episodes,
date_source: dateSource,
custom_date: customDate || null
})
});
if (response.success) {
showToast(`â
Successfully updated ${response.updated || selectedCount} episode(s)`, 'success');
// Reload the current series to show updated dates
const currentImdbId = currentTVSeries;
if (currentImdbId) {
loadEpisodes(currentImdbId);
}
// Clear selections
selectedCheckboxes.forEach(checkbox => checkbox.checked = false);
updateBulkDeleteButton();
} else {
showToast(`â ${response.message || 'Failed to update episodes'}`, 'error');
}
} catch (error) {
console.error('Bulk update error:', error);
showToast('â Error updating episodes', 'error');
}
}
// ==================== Database Population Functions ====================
let populatePollingInterval = null;
async function handlePopulateDatabase(event) {
event.preventDefault();
const mediaType = document.getElementById('populate-media-type').value;
const submitButton = event.target.querySelector('button[type="submit"]');
const originalText = submitButton.innerHTML;
// Confirm action
const confirmMsg = mediaType === 'both'
? 'This will populate the database with ALL movies and TV episodes from Radarr/Sonarr. Continue?'
: mediaType === 'movies'
? 'This will populate the database with ALL movies from Radarr. Continue?'
: 'This will populate the database with ALL TV episodes from Sonarr. Continue?';
if (!confirm(confirmMsg)) {
return;
}
// Disable button and show loading
submitButton.disabled = true;
submitButton.innerHTML = ' Starting...';
try {
const response = await fetch('/admin/populate-database', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ media_type: mediaType })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.status === 'started') {
showPopulateStatus();
startPopulatePolling();
} else {
throw new Error('Failed to start population');
}
} catch (error) {
console.error('Error starting population:', error);
showToast('â Failed to start database population', 'error');
submitButton.disabled = false;
submitButton.innerHTML = originalText;
}
}
function showPopulateStatus() {
const statusDiv = document.getElementById('populate-status');
const resultsDiv = document.getElementById('populate-results');
const progressBar = document.getElementById('populate-progress-bar');
const progressText = document.getElementById('populate-progress-text');
if (statusDiv) {
statusDiv.style.display = 'block';
}
if (progressBar) {
progressBar.style.width = '0%';
}
if (progressText) {
progressText.textContent = 'Starting...';
}
if (resultsDiv) {
resultsDiv.style.display = 'none';
}
}
function hidePopulateStatus() {
const statusDiv = document.getElementById('populate-status');
if (statusDiv) {
statusDiv.style.display = 'none';
}
}
function startPopulatePolling() {
// Clear any existing interval
if (populatePollingInterval) {
clearInterval(populatePollingInterval);
}
// Poll every 2 seconds
populatePollingInterval = setInterval(async () => {
try {
const response = await fetch('/api/populate/status');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const status = await response.json();
updatePopulateProgress(status);
// Stop polling if complete or error (API returns "completed": true/false and "error": string)
if (status.completed || status.error) {
stopPopulatePolling();
// Re-enable submit button
const submitButton = document.querySelector('#populate-form button[type="submit"]');
if (submitButton) {
submitButton.disabled = false;
submitButton.innerHTML = ' Start Population';
}
}
} catch (error) {
console.error('Error polling populate status:', error);
stopPopulatePolling();
// Re-enable submit button
const submitButton = document.querySelector('#populate-form button[type="submit"]');
if (submitButton) {
submitButton.disabled = false;
submitButton.innerHTML = ' Start Population';
}
}
}, 2000);
}
function stopPopulatePolling() {
if (populatePollingInterval) {
clearInterval(populatePollingInterval);
populatePollingInterval = null;
}
}
function updatePopulateProgress(status) {
const progressBar = document.getElementById('populate-progress-bar');
const progressText = document.getElementById('populate-progress-text');
const resultsDiv = document.getElementById('populate-results');
// Handle running status (API returns "running": true/false)
if (status.running && !status.completed) {
if (progressBar) {
// Show indeterminate progress
progressBar.style.width = '50%';
}
if (progressText) {
progressText.textContent = 'Populating database...';
}
} else if (status.completed) {
if (progressBar) {
progressBar.style.width = '100%';
}
if (progressText) {
progressText.textContent = 'Complete!';
}
// Show results (API returns movies/tv directly, not nested in result)
if (resultsDiv && (status.movies || status.tv)) {
const result = status; // Use status directly, not status.result
let html = 'Population Results
';
// Movies
if (result.movies) {
const m = result.movies;
html += '';
html += '
Movies
';
html += '
';
html += `- Total found: ${m.total || 0}
`;
html += `- Added: ${m.added || 0}
`;
html += `- Skipped: ${m.skipped || 0}
`;
html += `- Errors: ${m.errors || 0}
`;
html += `- Duration: ${(m.duration || 0).toFixed(2)}s
`;
html += '
';
html += '
';
}
// TV Episodes
if (result.tv) {
const tv = result.tv;
html += '';
html += '
TV Shows
';
html += '
';
html += `- Series: ${tv.total_series || 0}
`;
html += `- Episodes: ${tv.total_episodes || 0}
`;
html += `- Added: ${tv.added || 0}
`;
html += `- Skipped: ${tv.skipped || 0}
`;
html += `- Errors: ${tv.errors || 0}
`;
html += `- Duration: ${(tv.duration || 0).toFixed(2)}s
`;
html += '
';
html += '
';
}
// Total duration
if (result.total_duration) {
html += `Total Duration: ${result.total_duration.toFixed(2)}s
`;
}
resultsDiv.innerHTML = html;
resultsDiv.style.display = 'block';
}
showToast('â
Database population completed successfully', 'success');
} else if (status.status === 'error') {
if (progressFill && progressText) {
progressFill.style.width = '100%';
progressFill.style.backgroundColor = '#dc3545';
progressText.textContent = 'Error!';
}
const errorMsg = status.error || 'Unknown error occurred';
showToast(`â Database population failed: ${errorMsg}`, 'error');
if (resultsDiv) {
resultsDiv.innerHTML = `Error: ${errorMsg}
`;
resultsDiv.style.display = 'block';
}
}
}
// ==================== Table Sorting Functions ====================
let sortDirections = {}; // Track sort direction for each table column
function sortTable(tableBodyId, columnIndex, dataType) {
const tbody = document.getElementById(tableBodyId);
if (!tbody) return;
const sortKey = `${tableBodyId}-${columnIndex}`;
// Toggle sort direction
if (!sortDirections[sortKey]) {
sortDirections[sortKey] = 'asc';
} else {
sortDirections[sortKey] = sortDirections[sortKey] === 'asc' ? 'desc' : 'asc';
}
const rows = Array.from(tbody.querySelectorAll('tr'));
rows.sort((a, b) => {
const aCell = a.cells[columnIndex];
const bCell = b.cells[columnIndex];
if (!aCell || !bCell) return 0;
let aValue = aCell.textContent.trim();
let bValue = bCell.textContent.trim();
// Handle MISSING values - always sort to bottom
if (aValue === 'MISSING') return 1;
if (bValue === 'MISSING') return -1;
// Handle different data types
if (dataType === 'date') {
// Parse dates for comparison
aValue = aValue === '-' ? '' : aValue;
bValue = bValue === '-' ? '' : bValue;
if (!aValue && !bValue) return 0;
if (!aValue) return 1;
if (!bValue) return -1;
const aDate = new Date(aValue);
const bDate = new Date(bValue);
if (sortDirections[sortKey] === 'asc') {
return aDate - bDate;
} else {
return bDate - aDate;
}
} else if (dataType === 'number') {
const aNum = parseFloat(aValue) || 0;
const bNum = parseFloat(bValue) || 0;
if (sortDirections[sortKey] === 'asc') {
return aNum - bNum;
} else {
return bNum - aNum;
}
} else {
// Text comparison
if (sortDirections[sortKey] === 'asc') {
return aValue.localeCompare(bValue);
} else {
return bValue.localeCompare(aValue);
}
}
});
// Re-append sorted rows
rows.forEach(row => tbody.appendChild(row));
// Update sort icons
updateSortIcons(tableBodyId, columnIndex, sortDirections[sortKey]);
}
function updateSortIcons(tableBodyId, activeColumn, direction) {
// Find the table and update icons
const tbody = document.getElementById(tableBodyId);
if (!tbody) return;
const table = tbody.closest('table');
if (!table) return;
const headers = table.querySelectorAll('th.sortable');
headers.forEach((header, index) => {
const icon = header.querySelector('i');
if (!icon) return;
// +1 to account for checkbox column
if (index + 1 === activeColumn) {
icon.className = direction === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down';
} else {
icon.className = 'fas fa-sort';
}
});
}