From 7fffaac2479c7c7ddf11fa02dfd65611c864c4c7 Mon Sep 17 00:00:00 2001 From: SBCrumb Date: Wed, 29 Oct 2025 10:06:35 -0400 Subject: [PATCH] bulk delete --- nfoguard-web/static/js/app.js | 465 +++++++--------------------------- requirements.txt | 2 + static/js/app.js | 120 ++++++++- 3 files changed, 214 insertions(+), 373 deletions(-) diff --git a/nfoguard-web/static/js/app.js b/nfoguard-web/static/js/app.js index ba15f72..a1eabc8 100644 --- a/nfoguard-web/static/js/app.js +++ b/nfoguard-web/static/js/app.js @@ -53,9 +53,6 @@ function switchTab(tabName) { case 'reports': loadReport(); break; - case 'scheduled-scans': - loadScheduledScans(); - break; case 'tools': loadDetailedStats(); break; @@ -507,10 +504,21 @@ function showEpisodesModal(data) { +
+ + +
+ @@ -533,7 +541,10 @@ function showEpisodesModal(data) { ``; return ` - + + ${dateCell} @@ -1630,392 +1641,104 @@ async function logout() { } } -// Scheduled Scans Functions - -async function loadScheduledScans() { - try { - // Load schedules - const response = await fetch('/api/admin/scheduled-scans'); - const data = await response.json(); - - if (data.success) { - populateSchedulesTable(data.scans); - } else { - showToast('❌ Failed to load scheduled scans', 'error'); - } - - // Load execution history - const execResponse = await fetch('/api/admin/scheduled-scans/executions'); - const execData = await execResponse.json(); - - if (execData.success) { - populateExecutionsTable(execData.executions); - } - - } catch (error) { - console.error('Failed to load scheduled scans:', error); - showToast('❌ Error loading scheduled scans', '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 populateSchedulesTable(scans) { - const tbody = document.getElementById('schedules-table-body'); - tbody.innerHTML = ''; +function updateBulkDeleteButton() { + const selectedCheckboxes = document.querySelectorAll('.episode-checkbox:checked'); + const selectedCount = selectedCheckboxes.length; + const bulkDeleteButton = document.getElementById('bulk-delete-selected'); + const selectedCountSpan = document.getElementById('selected-count'); - if (scans.length === 0) { - tbody.innerHTML = ''; + if (selectedCountSpan) { + selectedCountSpan.textContent = selectedCount; + } + + if (bulkDeleteButton) { + bulkDeleteButton.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; } - scans.forEach(scan => { - const row = document.createElement('tr'); - - const nextRun = scan.next_run_at ? new Date(scan.next_run_at).toLocaleString() : 'Not scheduled'; - const lastRun = scan.last_run_at ? new Date(scan.last_run_at).toLocaleString() : 'Never'; - const statusBadge = scan.enabled ? - 'Enabled' : - 'Disabled'; - - row.innerHTML = ` - - - - - - - - - `; - - tbody.appendChild(row); - }); -} - -function populateExecutionsTable(executions) { - const tbody = document.getElementById('executions-table-body'); - tbody.innerHTML = ''; - - if (executions.length === 0) { - tbody.innerHTML = ''; + if (!confirm(`Are you sure you want to delete ${selectedCount} episode(s)? This action cannot be undone.`)) { return; } - executions.forEach(exec => { - const row = document.createElement('tr'); - - const started = new Date(exec.started_at).toLocaleString(); - const duration = exec.execution_time_seconds ? - `${exec.execution_time_seconds}s` : 'N/A'; - - let statusBadge; - switch (exec.status) { - case 'completed': - statusBadge = 'Completed'; - break; - case 'failed': - statusBadge = 'Failed'; - break; - case 'running': - statusBadge = 'Running'; - break; - default: - statusBadge = `${exec.status}`; - } - - row.innerHTML = ` - - - - - - - - - `; - - tbody.appendChild(row); - }); -} - -function openScheduleModal(scheduleId = null) { - const modal = document.getElementById('schedule-modal'); - const title = document.getElementById('schedule-modal-title'); - const submitText = document.getElementById('schedule-submit-text'); - const form = document.getElementById('schedule-form'); + const bulkDeleteButton = document.getElementById('bulk-delete-selected'); + const originalText = bulkDeleteButton.innerHTML; + bulkDeleteButton.innerHTML = ' Deleting...'; + bulkDeleteButton.disabled = true; - // Reset form - form.reset(); - document.getElementById('schedule-id').value = ''; - document.getElementById('schedule-enabled').checked = true; + let successCount = 0; + let failCount = 0; - if (scheduleId) { - title.textContent = 'Edit Schedule'; - submitText.textContent = 'Update Schedule'; - loadScheduleForEdit(scheduleId); - } else { - title.textContent = 'Add New Schedule'; - submitText.textContent = 'Create Schedule'; - } - - modal.style.display = 'block'; -} - -function closeScheduleModal() { - document.getElementById('schedule-modal').style.display = 'none'; -} - -async function loadScheduleForEdit(scheduleId) { - try { - const response = await fetch('/api/admin/scheduled-scans'); - const data = await response.json(); + // 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')); - if (data.success) { - const schedule = data.scans.find(s => s.id === scheduleId); - if (schedule) { - document.getElementById('schedule-id').value = schedule.id; - document.getElementById('schedule-name').value = schedule.name; - document.getElementById('schedule-description').value = schedule.description || ''; - document.getElementById('schedule-media-type').value = schedule.media_type; - document.getElementById('schedule-scan-mode').value = schedule.scan_mode; - document.getElementById('schedule-cron').value = schedule.cron_expression; - document.getElementById('schedule-paths').value = schedule.specific_paths || ''; - document.getElementById('schedule-enabled').checked = schedule.enabled; + try { + const response = await apiCall(`/api/tv/${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); } - } catch (error) { - console.error('Failed to load schedule for edit:', error); - showToast('❌ Failed to load schedule details', 'error'); - } -} - -async function toggleSchedule(scheduleId) { - try { - const response = await fetch(`/api/admin/scheduled-scans/${scheduleId}/toggle`, { - method: 'POST' - }); - const data = await response.json(); - - if (data.success) { - showToast(`✅ ${data.message}`, 'success'); - loadScheduledScans(); // Reload table - } else { - showToast(`❌ ${data.error}`, 'error'); - } - } catch (error) { - console.error('Failed to toggle schedule:', error); - showToast('❌ Error toggling schedule', 'error'); - } -} - -async function runSchedule(scheduleId) { - if (!confirm('Are you sure you want to run this scan now?')) { - return; } - try { - const response = await fetch(`/api/admin/scheduled-scans/${scheduleId}/run`, { - method: 'POST' - }); - const data = await response.json(); - - if (data.success) { - showToast(`✅ ${data.message}`, 'success'); - // Reload executions table after a moment - setTimeout(() => { - loadScheduledScans(); - }, 2000); - } else { - showToast(`❌ ${data.error}`, 'error'); - } - } catch (error) { - console.error('Failed to run schedule:', error); - showToast('❌ Error running schedule', 'error'); - } -} - -async function deleteSchedule(scheduleId) { - if (!confirm('Are you sure you want to delete this scheduled scan? This action cannot be undone.')) { - return; - } + // Update UI + updateEpisodeModalCounts(); + updateBulkDeleteButton(); - try { - const response = await fetch(`/api/admin/scheduled-scans/${scheduleId}`, { - method: 'DELETE' - }); - const data = await response.json(); - - if (data.success) { - showToast(`✅ ${data.message}`, 'success'); - loadScheduledScans(); // Reload table - } else { - showToast(`❌ ${data.error}`, 'error'); - } - } catch (error) { - console.error('Failed to delete schedule:', error); - showToast('❌ Error deleting schedule', 'error'); - } -} - -function editSchedule(scheduleId) { - openScheduleModal(scheduleId); -} - -// Cron Builder Functions - -function openCronBuilder() { - document.getElementById('cron-builder-modal').style.display = 'block'; - updateCronPreview(); -} - -function closeCronBuilder() { - document.getElementById('cron-builder-modal').style.display = 'none'; -} - -function setCronPreset(expression) { - const parts = expression.split(' '); - document.getElementById('cron-minute').value = parts[0]; - document.getElementById('cron-hour').value = parts[1]; - document.getElementById('cron-day').value = parts[2]; - document.getElementById('cron-month').value = parts[3]; - document.getElementById('cron-dow').value = parts[4]; - updateCronPreview(); -} - -function updateCronPreview() { - const minute = document.getElementById('cron-minute').value || '*'; - const hour = document.getElementById('cron-hour').value || '*'; - const day = document.getElementById('cron-day').value || '*'; - const month = document.getElementById('cron-month').value || '*'; - const dow = document.getElementById('cron-dow').value || '*'; + // Reset button + bulkDeleteButton.innerHTML = originalText; + bulkDeleteButton.disabled = true; - const expression = `${minute} ${hour} ${day} ${month} ${dow}`; - document.getElementById('cron-preview-text').textContent = expression; - - // Simple description generation - let description = 'Runs '; - if (minute === '0' && hour !== '*' && day === '*' && month === '*' && dow === '*') { - description += `daily at ${hour}:00`; - } else if (minute === '0' && hour !== '*' && day === '*' && month === '*' && dow !== '*') { - const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; - description += `weekly on ${days[dow]} at ${hour}:00`; - } else if (minute === '0' && hour !== '*' && day !== '*' && month === '*' && dow === '*') { - description += `monthly on day ${day} at ${hour}:00`; + // 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 { - description += 'with custom schedule'; + showToast(`❌ Failed to delete ${failCount} episode(s)`, 'error'); } - - document.getElementById('cron-description').textContent = description; } - -function applyCronExpression() { - const expression = document.getElementById('cron-preview-text').textContent; - document.getElementById('schedule-cron').value = expression; - closeCronBuilder(); -} - -// Event listeners for scheduled scans -document.addEventListener('DOMContentLoaded', function() { - // Add Schedule button - const addScheduleBtn = document.getElementById('add-schedule-btn'); - if (addScheduleBtn) { - addScheduleBtn.addEventListener('click', () => openScheduleModal()); - } - - // Cron builder button - const cronBuilderBtn = document.getElementById('cron-builder-btn'); - if (cronBuilderBtn) { - cronBuilderBtn.addEventListener('click', openCronBuilder); - } - - // Cron field change listeners - const cronFields = ['cron-minute', 'cron-hour', 'cron-day', 'cron-month', 'cron-dow']; - cronFields.forEach(fieldId => { - const field = document.getElementById(fieldId); - if (field) { - field.addEventListener('input', updateCronPreview); - } - }); - - // Schedule form submission - const scheduleForm = document.getElementById('schedule-form'); - if (scheduleForm) { - scheduleForm.addEventListener('submit', async function(e) { - e.preventDefault(); - - const formData = new FormData(this); - const scheduleId = document.getElementById('schedule-id').value; - - const scheduleData = { - name: formData.get('schedule-name') || document.getElementById('schedule-name').value, - description: formData.get('schedule-description') || document.getElementById('schedule-description').value, - cron_expression: formData.get('schedule-cron') || document.getElementById('schedule-cron').value, - media_type: formData.get('schedule-media-type') || document.getElementById('schedule-media-type').value, - scan_mode: formData.get('schedule-scan-mode') || document.getElementById('schedule-scan-mode').value, - specific_paths: formData.get('schedule-paths') || document.getElementById('schedule-paths').value, - enabled: document.getElementById('schedule-enabled').checked - }; - - try { - let url, method; - if (scheduleId) { - url = `/api/admin/scheduled-scans/${scheduleId}`; - method = 'PUT'; - } else { - url = '/api/admin/scheduled-scans'; - method = 'POST'; - } - - const response = await fetch(url, { - method: method, - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(scheduleData) - }); - - const data = await response.json(); - - if (data.success) { - showToast(`✅ ${data.message}`, 'success'); - closeScheduleModal(); - loadScheduledScans(); // Reload table - } else { - showToast(`❌ ${data.error}`, 'error'); - } - - } catch (error) { - console.error('Failed to save schedule:', error); - showToast('❌ Error saving schedule', 'error'); - } - }); - } -}); \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index bc54ab1..c198188 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,5 @@ aiofiles==23.2.1 aiohttp==3.8.6 psutil==5.9.6 python-dotenv==1.0.0 +APScheduler==3.10.4 +croniter==1.4.1 diff --git a/static/js/app.js b/static/js/app.js index 4a5f3b5..a1eabc8 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -504,10 +504,21 @@ function showEpisodesModal(data) { +
+ + +
+ + Episode Aired Date Added${dateadded}
+ + S${episode.season.toString().padStart(2, '0')}E${episode.episode.toString().padStart(2, '0')} ${episode.aired || '-'}
No scheduled scans configured
- ${scan.name} - ${scan.description ? `
${scan.description}` : ''} -
- ${scan.media_type} - - ${scan.scan_mode} - - ${scan.cron_expression} - ${lastRun}${nextRun}${statusBadge} -
- - - - -
-
No execution history
${exec.schedule_name}${started}${duration}${statusBadge}${exec.items_processed || 0}${exec.items_skipped || 0}${exec.items_failed || 0} - ${exec.error_message ? - `` : ''} -
+ @@ -530,7 +541,10 @@ function showEpisodesModal(data) { ``; return ` - + + ${dateCell} @@ -1625,4 +1639,106 @@ async function logout() { console.error('Logout failed:', error); showToast('❌ Logout error', 'error'); } -} \ No newline at end of file +} + +// 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 selectedCountSpan = document.getElementById('selected-count'); + + if (selectedCountSpan) { + selectedCountSpan.textContent = selectedCount; + } + + if (bulkDeleteButton) { + bulkDeleteButton.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/tv/${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'); + } +}
+ + Episode Aired Date Added${dateadded}
+ + S${episode.season.toString().padStart(2, '0')}E${episode.episode.toString().padStart(2, '0')} ${episode.aired || '-'}