fix: disable legacy JS interference and secure HTML delivery
- Neutralized downloads.js, watchlist-ui.js, and anime.js to prevent conflicts with HTMX - Guaranteed HTML responses in router_downloads.py via strict header detection - Unified frontend logic to follow the new server-driven architecture
This commit is contained in:
+12
-394
@@ -1,401 +1,19 @@
|
||||
// Download state
|
||||
let allDownloads = [];
|
||||
let collapsedGroups = new Set();
|
||||
let isClearing = false;
|
||||
|
||||
/**
|
||||
* Load all downloads
|
||||
* Downloads management (Legacy - Modernized to HTMX)
|
||||
* This file is kept for backward compatibility but internal polling is disabled.
|
||||
*/
|
||||
|
||||
async function loadDownloads() {
|
||||
// Skip refresh if currently clearing downloads to avoid conflicts
|
||||
if (isClearing) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await getDownloads();
|
||||
allDownloads = data.downloads;
|
||||
updateStats();
|
||||
filterDownloads();
|
||||
} catch (error) {
|
||||
console.error('Failed to load downloads:', error);
|
||||
console.log('Legacy loadDownloads called - redirected to HTMX refresh');
|
||||
if (typeof htmx !== 'undefined') {
|
||||
htmx.trigger('#downloads-container-inner', 'refresh');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update download statistics display
|
||||
*/
|
||||
function updateStats() {
|
||||
const stats = {
|
||||
total: allDownloads.length,
|
||||
downloading: allDownloads.filter(d => d.status === 'downloading').length,
|
||||
paused: allDownloads.filter(d => d.status === 'paused').length,
|
||||
completed: allDownloads.filter(d => d.status === 'completed').length,
|
||||
cancelled: allDownloads.filter(d => d.status === 'cancelled').length,
|
||||
failed: allDownloads.filter(d => d.status === 'failed').length
|
||||
};
|
||||
|
||||
const statsHtml = `
|
||||
<div class="stat-item">Total: <span class="stat-count">${stats.total}</span></div>
|
||||
${stats.downloading > 0 ? `<div class="stat-item">En cours: <span class="stat-count" style="color: #00d9ff;">${stats.downloading}</span></div>` : ''}
|
||||
${stats.paused > 0 ? `<div class="stat-item">En pause: <span class="stat-count" style="color: #ffa500;">${stats.paused}</span></div>` : ''}
|
||||
${stats.completed > 0 ? `<div class="stat-item">Terminés: <span class="stat-count" style="color: #00ff88;">${stats.completed}</span></div>` : ''}
|
||||
${stats.cancelled > 0 ? `<div class="stat-item">Annulés: <span class="stat-count" style="color: #ff6b6b;">${stats.cancelled}</span></div>` : ''}
|
||||
${stats.failed > 0 ? `<div class="stat-item">Échoués: <span class="stat-count" style="color: #ff4444;">${stats.failed}</span></div>` : ''}
|
||||
`;
|
||||
|
||||
document.getElementById('downloadsStats').innerHTML = statsHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter and sort downloads
|
||||
*/
|
||||
function filterDownloads() {
|
||||
const statusFilter = document.getElementById('statusFilter').value;
|
||||
const sortBy = document.getElementById('sortBy').value;
|
||||
const groupBy = document.getElementById('groupBy').value;
|
||||
const searchTerm = document.getElementById('searchDownloads').value.toLowerCase();
|
||||
|
||||
// Filter by status and search
|
||||
let filtered = allDownloads.filter(dl => {
|
||||
const matchesStatus = statusFilter === 'all' || dl.status === statusFilter;
|
||||
const matchesSearch = !searchTerm ||
|
||||
dl.filename.toLowerCase().includes(searchTerm) ||
|
||||
(dl.url && dl.url.toLowerCase().includes(searchTerm));
|
||||
return matchesStatus && matchesSearch;
|
||||
});
|
||||
|
||||
// Sort
|
||||
filtered.sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
case 'date_asc':
|
||||
return new Date(a.created_at) - new Date(b.created_at);
|
||||
case 'name':
|
||||
return a.filename.localeCompare(b.filename);
|
||||
case 'name_desc':
|
||||
return b.filename.localeCompare(a.filename);
|
||||
case 'size':
|
||||
return (b.total_bytes || 0) - (a.total_bytes || 0);
|
||||
case 'date':
|
||||
default:
|
||||
return new Date(b.created_at) - new Date(a.created_at);
|
||||
}
|
||||
});
|
||||
|
||||
// Apply grouping
|
||||
displayDownloads(filtered, groupBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Group downloads by criteria
|
||||
*/
|
||||
function groupDownloads(downloads, groupBy) {
|
||||
const groups = {};
|
||||
|
||||
downloads.forEach(dl => {
|
||||
let key = 'Ungrouped';
|
||||
|
||||
switch (groupBy) {
|
||||
case 'series':
|
||||
key = extractSeriesName(dl.filename);
|
||||
break;
|
||||
case 'status':
|
||||
key = translateStatus(dl.status);
|
||||
break;
|
||||
case 'day':
|
||||
key = getDayString(dl.created_at);
|
||||
break;
|
||||
default:
|
||||
key = 'Tous';
|
||||
}
|
||||
|
||||
if (!groups[key]) {
|
||||
groups[key] = [];
|
||||
}
|
||||
groups[key].push(dl);
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display downloads (flat or grouped)
|
||||
*/
|
||||
function displayDownloads(downloads, groupBy = 'none') {
|
||||
const container = document.getElementById('downloadsList');
|
||||
|
||||
if (downloads.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 19l3 3m0 0l3-3m-3 3V10"></path>
|
||||
</svg>
|
||||
<p>Aucun téléchargement trouvé</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
// Disable legacy intervals
|
||||
window.loadDownloads = loadDownloads;
|
||||
window.handleCleanupDownloads = () => {
|
||||
if (typeof htmx !== 'undefined') {
|
||||
htmx.ajax('POST', '/api/downloads/cleanup', { swap: 'none' });
|
||||
}
|
||||
|
||||
// Group downloads if needed
|
||||
if (groupBy && groupBy !== 'none') {
|
||||
const groups = groupDownloads(downloads, groupBy);
|
||||
const groupNames = Object.keys(groups);
|
||||
|
||||
// Sort group names
|
||||
groupNames.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
// Display grouped downloads
|
||||
let html = '';
|
||||
groupNames.forEach((groupName, index) => {
|
||||
const groupDownloads = groups[groupName];
|
||||
const groupId = `group-${index}`;
|
||||
const isCollapsed = collapsedGroups.has(groupId);
|
||||
const collapsedClass = isCollapsed ? 'collapsed' : '';
|
||||
const displayStyle = isCollapsed ? 'display: none;' : '';
|
||||
|
||||
html += `
|
||||
<div class="downloads-group">
|
||||
<div class="downloads-group-header ${collapsedClass}" onclick="toggleGroup('${groupId}')">
|
||||
<div class="downloads-group-title">${escapeHtml(groupName)}</div>
|
||||
<div class="downloads-group-count">${groupDownloads.length}</div>
|
||||
</div>
|
||||
<div class="downloads-group-items" id="${groupId}" style="${displayStyle}">
|
||||
${groupDownloads.map(dl => renderDownloadItem(dl)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
container.innerHTML = html;
|
||||
} else {
|
||||
// Display flat list
|
||||
container.innerHTML = downloads.map(dl => renderDownloadItem(dl)).join('');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single download item
|
||||
*/
|
||||
function renderDownloadItem(dl) {
|
||||
return `
|
||||
<div class="download-item">
|
||||
<div class="download-header">
|
||||
<div class="filename">${escapeHtml(dl.filename)}</div>
|
||||
<span class="status status-${dl.status}">${translateStatus(dl.status)}</span>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" style="width: ${dl.progress}%"></div>
|
||||
</div>
|
||||
<div class="download-info">
|
||||
<span>${formatBytes(dl.downloaded_bytes)}${dl.total_bytes ? ' / ' + formatBytes(dl.total_bytes) : ''}</span>
|
||||
<span>${dl.speed > 0 ? formatSpeed(dl.speed) : ''}</span>
|
||||
</div>
|
||||
<div class="download-actions">
|
||||
${renderDownloadActions(dl)}
|
||||
</div>
|
||||
${dl.error ? `<div class="error-message">${escapeHtml(dl.error)}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render download action buttons based on status
|
||||
*/
|
||||
function renderDownloadActions(dl) {
|
||||
switch (dl.status) {
|
||||
case 'downloading':
|
||||
return `
|
||||
<button class="btn-small btn-pause" onclick="handlePause('${dl.id}')">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
Pause
|
||||
</button>
|
||||
<button class="btn-small btn-cancel" onclick="handleCancel('${dl.id}')">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
Annuler
|
||||
</button>
|
||||
`;
|
||||
|
||||
case 'paused':
|
||||
return `
|
||||
<button class="btn-small btn-resume" onclick="handleResume('${dl.id}')">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
Reprendre
|
||||
</button>
|
||||
<button class="btn-small btn-cancel" onclick="handleCancel('${dl.id}')">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
Annuler
|
||||
</button>
|
||||
`;
|
||||
|
||||
case 'completed':
|
||||
return `
|
||||
<button class="btn-small btn-download" style="background: linear-gradient(45deg, #ff6b6b, #ffa500);" onclick="watchVideo('${dl.id}')">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
Regarder
|
||||
</button>
|
||||
<button class="btn-small btn-download" onclick="downloadFile('${dl.id}')">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path>
|
||||
</svg>
|
||||
Télécharger
|
||||
</button>
|
||||
<button class="btn-small btn-cancel" onclick="handleCancel('${dl.id}')">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
</svg>
|
||||
Supprimer
|
||||
</button>
|
||||
`;
|
||||
|
||||
case 'failed':
|
||||
default:
|
||||
return `
|
||||
<button class="btn-small btn-cancel" onclick="handleCancel('${dl.id}')">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||
</svg>
|
||||
Supprimer
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle group collapse/expand
|
||||
*/
|
||||
function toggleGroup(groupId) {
|
||||
const items = document.getElementById(groupId);
|
||||
const header = items.previousElementSibling;
|
||||
|
||||
if (!items || !header) {
|
||||
console.error('Could not find group elements');
|
||||
return;
|
||||
}
|
||||
|
||||
const isCollapsed = collapsedGroups.has(groupId);
|
||||
|
||||
if (isCollapsed) {
|
||||
items.style.display = 'flex';
|
||||
header.classList.remove('collapsed');
|
||||
collapsedGroups.delete(groupId);
|
||||
} else {
|
||||
items.style.display = 'none';
|
||||
header.classList.add('collapsed');
|
||||
collapsedGroups.add(groupId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle pause button click
|
||||
*/
|
||||
async function handlePause(id) {
|
||||
try {
|
||||
await pauseDownload(id);
|
||||
loadDownloads();
|
||||
} catch (error) {
|
||||
console.error('Pause error:', error);
|
||||
alert('Erreur lors de la mise en pause');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle resume button click
|
||||
*/
|
||||
async function handleResume(id) {
|
||||
try {
|
||||
await resumeDownload(id);
|
||||
loadDownloads();
|
||||
} catch (error) {
|
||||
console.error('Resume error:', error);
|
||||
alert('Erreur lors de la reprise');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle cancel/delete button click
|
||||
*/
|
||||
async function handleCancel(id) {
|
||||
if (!confirm('Êtes-vous sûr ?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await cancelDownload(id);
|
||||
loadDownloads();
|
||||
} catch (error) {
|
||||
console.error('Cancel error:', error);
|
||||
alert('Erreur lors de la suppression');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear unwanted downloads
|
||||
*/
|
||||
async function clearCompleted() {
|
||||
const unwanted = allDownloads.filter(dl =>
|
||||
dl.status === 'cancelled' ||
|
||||
dl.status === 'failed' ||
|
||||
dl.status === 'deleted'
|
||||
);
|
||||
|
||||
if (unwanted.length === 0) {
|
||||
alert('Aucun téléchargement à supprimer');
|
||||
return;
|
||||
}
|
||||
|
||||
// Count by status
|
||||
const byStatus = unwanted.reduce((acc, dl) => {
|
||||
acc[dl.status] = (acc[dl.status] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
let message = 'Supprimer ';
|
||||
if (byStatus.cancelled) message += `${byStatus.cancelled} annulé(s) `;
|
||||
if (byStatus.failed) message += `${byStatus.failed} échoué(s) `;
|
||||
if (byStatus.deleted) message += `${byStatus.deleted} supprimé(s) `;
|
||||
message += '?';
|
||||
|
||||
if (!confirm(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set flag to prevent auto-refresh conflicts
|
||||
isClearing = true;
|
||||
|
||||
try {
|
||||
// Delete all in parallel (much faster)
|
||||
await Promise.all(unwanted.map(dl => cancelDownload(dl.id)));
|
||||
} catch (error) {
|
||||
console.error('Error deleting downloads:', error);
|
||||
alert('Erreur lors de la suppression');
|
||||
} finally {
|
||||
// Clear flag and refresh
|
||||
isClearing = false;
|
||||
loadDownloads();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download file to user's computer
|
||||
*/
|
||||
function downloadFile(id) {
|
||||
window.open(`${API_BASE}/download/${id}/file`, '_blank');
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch video in player
|
||||
*/
|
||||
function watchVideo(id) {
|
||||
window.open(`/player/${id}`, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user