From 3e3957234d7e169bf814278f0333ad419deb9d18 Mon Sep 17 00:00:00 2001 From: SBCrumb Date: Mon, 3 Nov 2025 15:52:42 -0500 Subject: [PATCH] udpate add bulk update of TV eps --- VERSION | 2 +- api/web_routes.py | 74 +++++++++++++++- nfoguard-web/static/index.html | 2 +- nfoguard-web/static/js/app.js | 157 ++++++++++++++++++++++++++++++++- 4 files changed, 229 insertions(+), 6 deletions(-) diff --git a/VERSION b/VERSION index 79efce5..8abba37 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.9.7-status-format-fix +2.9.8-bulk-date-update diff --git a/api/web_routes.py b/api/web_routes.py index 0963758..c378e1a 100644 --- a/api/web_routes.py +++ b/api/web_routes.py @@ -1573,7 +1573,79 @@ def register_web_routes(app, dependencies): @app.post("/api/bulk/update-source") async def api_bulk_update_source(media_type: str, old_source: str, new_source: str): return {"error": "Bulk operations not available in web container. Use core container on port 8085."} - + + @app.post("/api/episodes/bulk-update-dates") + async def api_bulk_update_episode_dates(request: Request): + """Bulk update episode dates""" + try: + data = await request.json() + episodes = data.get('episodes', []) + date_source = data.get('date_source', 'airdate') + custom_date = data.get('custom_date') + + if not episodes: + return {"success": False, "message": "No episodes provided"} + + db = dependencies["db"] + sonarr = dependencies.get("sonarr_client") + + updated_count = 0 + failed_count = 0 + + for ep in episodes: + imdb_id = ep.get('imdb_id') + season = ep.get('season') + episode = ep.get('episode') + + if not all([imdb_id, season is not None, episode is not None]): + failed_count += 1 + continue + + try: + dateadded = None + source = "manual" + + if date_source == 'airdate': + # Get airdate from Sonarr + if sonarr: + episode_data = sonarr.get_episode(imdb_id, season, episode) + if episode_data and episode_data.get('airDateUtc'): + dateadded = episode_data['airDateUtc'] + source = "sonarr:airdate" + elif date_source == 'import': + # Get import date from Sonarr history + if sonarr: + import_date = sonarr.get_episode_import_date(imdb_id, season, episode) + if import_date: + dateadded = import_date + source = "sonarr:import" + elif date_source == 'custom': + # Use custom date + if custom_date: + dateadded = custom_date + source = "manual:custom" + + if dateadded: + db.upsert_episode_dates(imdb_id, season, episode, dateadded, source) + updated_count += 1 + else: + failed_count += 1 + + except Exception as e: + print(f"❌ Error updating episode {imdb_id} S{season:02d}E{episode:02d}: {e}") + failed_count += 1 + + return { + "success": True, + "updated": updated_count, + "failed": failed_count, + "message": f"Updated {updated_count} episode(s), {failed_count} failed" + } + + except Exception as e: + print(f"❌ Bulk update error: {e}") + return {"success": False, "message": str(e)} + # Reports @app.get("/api/reports/missing-dates") async def api_missing_dates_report(): diff --git a/nfoguard-web/static/index.html b/nfoguard-web/static/index.html index 265748c..e0b9db2 100644 --- a/nfoguard-web/static/index.html +++ b/nfoguard-web/static/index.html @@ -711,6 +711,6 @@
- + \ No newline at end of file diff --git a/nfoguard-web/static/js/app.js b/nfoguard-web/static/js/app.js index ef39dfb..b8b8cf7 100644 --- a/nfoguard-web/static/js/app.js +++ b/nfoguard-web/static/js/app.js @@ -509,10 +509,44 @@ function showEpisodesModal(data) { + + + +
@@ -1670,16 +1704,26 @@ 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'); @@ -1754,6 +1798,113 @@ async function bulkDeleteSelected() { } } +// 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;