This commit is contained in:
+73
-1
@@ -1573,7 +1573,79 @@ def register_web_routes(app, dependencies):
|
|||||||
@app.post("/api/bulk/update-source")
|
@app.post("/api/bulk/update-source")
|
||||||
async def api_bulk_update_source(media_type: str, old_source: str, new_source: str):
|
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."}
|
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
|
# Reports
|
||||||
@app.get("/api/reports/missing-dates")
|
@app.get("/api/reports/missing-dates")
|
||||||
async def api_missing_dates_report():
|
async def api_missing_dates_report():
|
||||||
|
|||||||
@@ -711,6 +711,6 @@
|
|||||||
<!-- Toast Notifications -->
|
<!-- Toast Notifications -->
|
||||||
<div class="toast-container" id="toast-container"></div>
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -509,10 +509,44 @@ function showEpisodesModal(data) {
|
|||||||
<button id="bulk-select-all" class="btn btn-sm btn-secondary" onclick="toggleSelectAll()">
|
<button id="bulk-select-all" class="btn btn-sm btn-secondary" onclick="toggleSelectAll()">
|
||||||
<i class="fas fa-check-square"></i> Select All
|
<i class="fas fa-check-square"></i> Select All
|
||||||
</button>
|
</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>
|
<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>)
|
<i class="fas fa-trash"></i> Delete Selected (<span id="selected-count">0</span>)
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<div class="table-container">
|
||||||
<table class="data-table sortable-table">
|
<table class="data-table sortable-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -1670,16 +1704,26 @@ function updateBulkDeleteButton() {
|
|||||||
const selectedCheckboxes = document.querySelectorAll('.episode-checkbox:checked');
|
const selectedCheckboxes = document.querySelectorAll('.episode-checkbox:checked');
|
||||||
const selectedCount = selectedCheckboxes.length;
|
const selectedCount = selectedCheckboxes.length;
|
||||||
const bulkDeleteButton = document.getElementById('bulk-delete-selected');
|
const bulkDeleteButton = document.getElementById('bulk-delete-selected');
|
||||||
|
const bulkUpdateButton = document.getElementById('bulk-update-dates');
|
||||||
const selectedCountSpan = document.getElementById('selected-count');
|
const selectedCountSpan = document.getElementById('selected-count');
|
||||||
|
const updateCountSpan = document.getElementById('update-count');
|
||||||
|
|
||||||
if (selectedCountSpan) {
|
if (selectedCountSpan) {
|
||||||
selectedCountSpan.textContent = selectedCount;
|
selectedCountSpan.textContent = selectedCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (updateCountSpan) {
|
||||||
|
updateCountSpan.textContent = selectedCount;
|
||||||
|
}
|
||||||
|
|
||||||
if (bulkDeleteButton) {
|
if (bulkDeleteButton) {
|
||||||
bulkDeleteButton.disabled = selectedCount === 0;
|
bulkDeleteButton.disabled = selectedCount === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bulkUpdateButton) {
|
||||||
|
bulkUpdateButton.disabled = selectedCount === 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Update select all checkbox state
|
// Update select all checkbox state
|
||||||
const selectAllCheckbox = document.getElementById('select-all-checkbox');
|
const selectAllCheckbox = document.getElementById('select-all-checkbox');
|
||||||
const allCheckboxes = document.querySelectorAll('.episode-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 ====================
|
// ==================== Database Population Functions ====================
|
||||||
|
|
||||||
let populatePollingInterval = null;
|
let populatePollingInterval = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user