This commit is contained in:
@@ -1574,6 +1574,78 @@ def register_web_routes(app, dependencies):
|
||||
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():
|
||||
|
||||
@@ -711,6 +711,6 @@
|
||||
<!-- Toast Notifications -->
|
||||
<div class="toast-container" id="toast-container"></div>
|
||||
|
||||
<script src="/static/js/app.js?v=2.9.7-status-format-fix"></script>
|
||||
<script src="/static/js/app.js?v=2.9.8-bulk-date-update"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -509,10 +509,44 @@ function showEpisodesModal(data) {
|
||||
<button id="bulk-select-all" class="btn btn-sm btn-secondary" onclick="toggleSelectAll()">
|
||||
<i class="fas fa-check-square"></i> Select All
|
||||
</button>
|
||||
<button id="bulk-update-dates" class="btn btn-sm btn-primary" onclick="showBulkUpdateModal()" style="margin-left: 10px;" disabled>
|
||||
<i class="fas fa-calendar"></i> Update Dates (<span id="update-count">0</span>)
|
||||
</button>
|
||||
<button id="bulk-delete-selected" class="btn btn-sm btn-danger" onclick="bulkDeleteSelected()" style="margin-left: 10px;" disabled>
|
||||
<i class="fas fa-trash"></i> Delete Selected (<span id="selected-count">0</span>)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Bulk Update Dates Modal -->
|
||||
<div id="bulk-update-modal" class="modal" style="display: none;">
|
||||
<div class="modal-content" style="max-width: 500px;">
|
||||
<div class="modal-header">
|
||||
<h3>Bulk Update Episode Dates</h3>
|
||||
<button class="close-button" onclick="closeBulkUpdateModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Update <strong><span id="modal-selected-count">0</span></strong> selected episode(s) to:</p>
|
||||
<div class="form-group">
|
||||
<label>Date Source:</label>
|
||||
<select id="bulk-date-source" class="form-control">
|
||||
<option value="airdate">Air Date (from Sonarr)</option>
|
||||
<option value="import">Import Date (from Sonarr history)</option>
|
||||
<option value="custom">Custom Date</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="custom-date-input" style="display: none; margin-top: 10px;">
|
||||
<label>Custom Date:</label>
|
||||
<input type="datetime-local" id="bulk-custom-date" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeBulkUpdateModal()">Cancel</button>
|
||||
<button class="btn btn-primary" onclick="executeBulkUpdate()">
|
||||
<i class="fas fa-save"></i> Update Dates
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="data-table sortable-table">
|
||||
<thead>
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user