diff --git a/app/routers/router_downloads.py b/app/routers/router_downloads.py index c4ecfcb..d0381e3 100644 --- a/app/routers/router_downloads.py +++ b/app/routers/router_downloads.py @@ -4,6 +4,7 @@ Download management routes for Ohm Stream Downloader API. from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response from fastapi.templating import Jinja2Templates +from fastapi.responses import HTMLResponse from app.download_manager import DownloadManager from app.models import DownloadRequest @@ -24,18 +25,20 @@ async def get_downloads( html: bool = Query(False), download_manager: DownloadManager = Depends(get_download_manager), ): - """Get list of all download tasks. Returns HTML for HTMX.""" + """Get list of all download tasks. Returns HTML for HTMX requests.""" tasks = download_manager.get_all_tasks() - # Force HTML if requested or via HTMX - if html or request.headers.get("HX-Request"): - print(f"[DOWNLOADS] HTMX Request detected. Returning HTML for {len(tasks)} tasks.") + # Strictly check for HTMX or explicit HTML flag + is_htmx = request.headers.get("HX-Request") == "true" or request.headers.get("HX-Request") + + if html or is_htmx: + print(f"[DOWNLOADS] HTML Request. Found {len(tasks)} tasks.") return templates.TemplateResponse( "components/downloads_list.html", {"request": request, "tasks": tasks} ) - print(f"[DOWNLOADS] API Request detected. Returning JSON.") + print(f"[DOWNLOADS] API Request. Returning JSON.") return {"downloads": tasks} @@ -92,12 +95,10 @@ async def cancel_download( current_user=Depends(get_current_user_from_token), ): """Cancel and delete a download task""" - # Use delete_task if cancel_download not available or for full removal if hasattr(download_manager, "cancel_download"): if download_manager.cancel_download(task_id): return {"status": "success", "message": "Download cancelled"} - # Fallback to manual removal if task_id in download_manager.tasks: del download_manager.tasks[task_id] return {"status": "success", "message": "Download removed"} @@ -115,7 +116,6 @@ async def cleanup_completed( count = download_manager.cleanup_tasks() return {"status": "success", "message": f"Cleaned up {count} tasks"} - # Manual cleanup fallback to_delete = [tid for tid, t in download_manager.tasks.items() if t.status == "completed"] for tid in to_delete: del download_manager.tasks[tid] diff --git a/static/js/anime.js b/static/js/anime.js index 5bca520..e0d198c 100644 --- a/static/js/anime.js +++ b/static/js/anime.js @@ -1,640 +1,20 @@ /** - * Anime search and episode management + * Anime Search & Releases (Legacy - Partially modernized to HTMX) */ -/** - * Display search results - */ -async function displaySearchResults(data, lang) { - const resultsContainer = document.getElementById('searchResults'); - const providers = await getProvidersInfo(); - - let totalResults = 0; - let htmlPromises = []; - - for (const [providerId, results] of Object.entries(data.results)) { - if (results && results.length > 0) { - totalResults += results.length; - - results.forEach(anime => { - const providerInfo = providers.anime_providers[providerId]; - // Collect promises for async rendering - htmlPromises.push(renderAnimeCard(anime, providerId, providerInfo, lang)); - }); - } - } - - if (totalResults === 0) { - resultsContainer.innerHTML = '
Aucun résultat trouvé
'; - return; - } - - // Wait for all cards to be rendered - const htmlSegments = await Promise.all(htmlPromises); - resultsContainer.innerHTML = htmlSegments.join(''); - - // Auto-load seasons for providers that support them - // Stagger the requests to avoid overwhelming the server - let delayCounter = 0; - for (const [providerId, results] of Object.entries(data.results)) { - if (results && results.length > 0) { - results.forEach((anime, index) => { - // Stagger requests: 500ms delay between each anime - setTimeout(() => { - // Try to load seasons first (if provider supports them) - if (anime.url) { - loadSeasonsForAnime(providerId, encodeURIComponent(anime.url)); - } - }, 500 * index); - delayCounter++; - }); - } - } -} - -/** - * Render anime card HTML - */ -async function renderAnimeCard(anime, providerId, providerInfo, lang) { - const metadataHtml = renderAnimeMetadata(anime.metadata); - - // Check if provider supports seasons using helper function - const supportsSeasons = await providerSupportsSeasons(providerId, anime.url); - - const seasonSelectHtml = supportsSeasons ? ` - - ` : ''; - - return ` -
-
-
${escapeHtml(anime.title)}
-
${providerInfo?.icon || ''} ${providerInfo?.name || providerId}
-
- ${metadataHtml} -
- ${seasonSelectHtml} - -
- -
- `; -} - -/** - * Render anime metadata - */ -function renderAnimeMetadata(metadata) { - if (!metadata) return ''; - - let metaParts = []; - - if (metadata.release_year) metaParts.push(`📅 ${metadata.release_year}`); - if (metadata.rating) metaParts.push(`⭐ ${metadata.rating}`); - if (metadata.genres && metadata.genres.length > 0) metaParts.push(`🏷️ ${metadata.genres.slice(0, 3).join(', ')}`); - if (metadata.total_episodes) metaParts.push(`📺 ${metadata.total_episodes} épisodes`); - if (metadata.status) metaParts.push(`📡 ${metadata.status === 'Ongoing' ? 'En cours' : 'Terminé'}`); - - let html = ''; - - if (metaParts.length > 0) { - html += ` -
- ${metaParts.join(' • ')} -
- `; - } - - if (metadata.synopsis) { - html += ` -
- 📖 Synopsis -

${escapeHtml(metadata.synopsis)}

-
- `; - } - - return html; -} - -/** - * Load seasons for anime (if provider supports it) - */ -async function loadSeasonsForAnime(providerId, encodedUrl) { - const url = decodeURIComponent(encodedUrl); - const seasonSelectId = `seasons-${providerId}-${encodedUrl}`; - - const seasonSelectElement = document.getElementById(seasonSelectId); - if (!seasonSelectElement) { - console.log('Season select element not found:', seasonSelectId); - return; - } - - // Check if provider supports seasons - const supportsSeasons = await providerSupportsSeasons(providerId, url); - if (!supportsSeasons) { - console.log('Provider does not support seasons:', providerId); - seasonSelectElement.style.display = 'none'; - return; - } - - console.log('Loading seasons for:', url, 'Element:', seasonSelectId); - - // Mark as loading to prevent duplicate requests - if (seasonSelectElement.dataset.loading === 'true') { - console.log('Season loading already in progress, skipping...'); - return; - } - seasonSelectElement.dataset.loading = 'true'; - +async function loadAnimeReleases() { + // Keep this for now as it's not yet fully HTMX + console.log('Loading anime releases...'); try { - // Add timeout to the fetch - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 15000); // 15 second timeout - - const response = await fetch(`${API_BASE}/anime/seasons?url=${encodeURIComponent(url)}`, { - signal: controller.signal - }); - clearTimeout(timeoutId); - - if (response.ok) { - const data = await response.json(); - - if (data.seasons && data.seasons.length > 0) { - seasonSelectElement.innerHTML = ''; - - data.seasons.forEach(season => { - const option = document.createElement('option'); - option.value = season.url; - const episodeText = season.episode_count ? - `${season.episode_count} épisodes` : - 'Chargement...'; - option.textContent = `${season.title} (${episodeText})`; - option.dataset.seasonNum = season.season; - seasonSelectElement.appendChild(option); - }); - - console.log(`Loaded ${data.seasons.length} seasons`); - seasonSelectElement.style.display = 'block'; - } else { - // No seasons found, hide season selector and load episodes directly - console.log('No seasons found, hiding selector'); - seasonSelectElement.style.display = 'none'; - loadEpisodesForAnime(providerId, encodedUrl, 'vostfr'); - } - } else { - console.error('Failed to load seasons:', response.status); - seasonSelectElement.style.display = 'none'; - loadEpisodesForAnime(providerId, encodedUrl, 'vostfr'); - } - } catch (error) { - if (error.name === 'AbortError') { - console.error('Season loading timeout'); - seasonSelectElement.innerHTML = ''; - // Add retry functionality - seasonSelectElement.disabled = false; - seasonSelectElement.onclick = () => { - seasonSelectElement.dataset.loading = 'false'; - seasonSelectElement.onclick = null; - loadSeasonsForAnime(providerId, encodedUrl); - }; - } else { - console.error('Error loading seasons:', error); - seasonSelectElement.style.display = 'none'; - loadEpisodesForAnime(providerId, encodedUrl, 'vostfr'); - } - } finally { - seasonSelectElement.dataset.loading = 'false'; - } -} - -/** - * Handle season selection change - */ -async function handleSeasonChange(providerId, encodedUrl, lang) { - const seasonSelectId = `seasons-${providerId}-${encodedUrl}`; - const seasonSelectElement = document.getElementById(seasonSelectId); - - const selectedSeasonUrl = seasonSelectElement.value; - const encodedSeasonUrl = encodeURIComponent(selectedSeasonUrl); - - if (!selectedSeasonUrl) { - // Clear episodes if no season selected - const episodeSelectId = `episodes-${providerId}-${encodedUrl}`; - const episodeSelectElement = document.getElementById(episodeSelectId); - episodeSelectElement.innerHTML = ''; - episodeSelectElement.disabled = true; - return; - } - - // Find the episode select element (it's based on the original anime URL) - const episodeSelectId = `episodes-${providerId}-${encodedUrl}`; - const selectElement = document.getElementById(episodeSelectId); - - if (!selectElement) { - console.error('Episode select element not found:', episodeSelectId); - return; - } - - // Show loading state - selectElement.innerHTML = ''; - selectElement.disabled = false; - - try { - // Load episodes for the selected season - const data = await loadEpisodes(selectedSeasonUrl, lang); - - if (data.episodes && data.episodes.length > 0) { - selectElement.innerHTML = ''; - data.episodes.forEach(ep => { - const option = document.createElement('option'); - option.value = ep.url; - option.textContent = `Épisode ${ep.episode}`; - selectElement.appendChild(option); - }); - - // Show download buttons - const actionsId = `actions-${providerId}-${encodedUrl}`; - const actionsDiv = document.getElementById(actionsId); - actionsDiv.style.display = 'flex'; - } else { - selectElement.innerHTML = ''; - selectElement.disabled = true; - } - } catch (error) { - console.error('Error loading episodes:', error); - selectElement.innerHTML = ''; - } -} - -/** - * Load episodes for an anime - */ -async function loadEpisodesForAnime(providerId, encodedUrl, lang) { - const url = decodeURIComponent(encodedUrl); - const selectId = `episodes-${providerId}-${encodedUrl}`; - const actionsId = `actions-${providerId}-${encodedUrl}`; - - const selectElement = document.getElementById(selectId); - if (!selectElement) return; - - selectElement.innerHTML = ''; - - try { - const data = await loadEpisodes(url, lang); - - if (data.episodes && data.episodes.length > 0) { - selectElement.innerHTML = ''; - data.episodes.forEach(ep => { - const option = document.createElement('option'); - option.value = ep.url; - option.textContent = `Épisode ${ep.episode}`; - selectElement.appendChild(option); - }); - - // Show download buttons - const actionsDiv = document.getElementById(actionsId); - actionsDiv.style.display = 'flex'; - } else { - selectElement.innerHTML = ''; - selectElement.disabled = true; - - // Add warning message - const card = document.getElementById(`anime-${providerId}-${encodedUrl}`); - if (card) { - const warning = document.createElement('div'); - warning.style.cssText = 'margin-top: 10px; padding: 10px; background: rgba(255, 100, 100, 0.2); border-radius: 6px; font-size: 12px; color: #ff6b6b;'; - warning.textContent = '⚠️ Aucun épisode trouvé. Essayez une recherche exacte ou un autre fournisseur.'; - card.appendChild(warning); - } - } - } catch (error) { - console.error('Error loading episodes:', error); - selectElement.innerHTML = ''; - } -} - -/** - * Handle episode download - */ -async function handleDownloadEpisode(encodedUrl, providerId, lang) { - const url = decodeURIComponent(encodedUrl); - const selectId = `episodes-${providerId}-${encodedUrl}`; - const selectElement = document.getElementById(selectId); - - const episodeUrl = selectElement.value; - if (!episodeUrl) { - alert('Veuillez sélectionner un épisode'); - return; - } - - try { - await downloadEpisode(episodeUrl); - loadDownloads(); - alert('Téléchargement démarré!'); - selectElement.value = ''; - } catch (error) { - console.error('Download error:', error); - alert('Erreur lors du démarrage du téléchargement'); - } -} - -/** - * Handle season download - */ -async function handleDownloadSeason(encodedUrl, lang) { - const url = decodeURIComponent(encodedUrl); - - if (!confirm(`⚠️ Attention: Vous allez télécharger toute la saison. Cela peut prendre du temps et utiliser beaucoup d'espace disque.\n\nVoulez-vous continuer ?`)) { - return; - } - - try { - const data = await downloadSeason(url, lang); - loadDownloads(); - alert(`✅ ${data.message}\n\n${data.total_episodes} épisodes ont été ajoutés à la file de téléchargement!`); - } catch (error) { - console.error('Season download error:', error); - alert('Erreur lors du démarrage du téléchargement de la saison'); - } -} - -/** - * Load all seasons and episodes and display them - */ -async function loadAllSeasonsAndEpisodes(providerId, encodedUrl, lang) { - const url = decodeURIComponent(encodedUrl); - const cardId = `anime-${providerId}-${encodedUrl}`; - const card = document.getElementById(cardId); - - if (!card) { - console.error('Card not found:', cardId); - return; - } - - // Remove existing all-seasons container if present - const existingContainer = document.getElementById(`all-seasons-${providerId}-${encodedUrl}`); - if (existingContainer) { - existingContainer.remove(); - return; - } - - // Create container for all seasons - const container = document.createElement('div'); - container.id = `all-seasons-${providerId}-${encodedUrl}`; - container.style.cssText = 'margin-top: 16px;'; - - try { - // Fetch all seasons - const response = await fetch(`${API_BASE}/anime/seasons?url=${encodeURIComponent(url)}`); - - if (!response.ok) { - throw new Error('Failed to fetch seasons'); - } - + const response = await fetch('/api/anime/mal/search?q=2024&limit=12'); const data = await response.json(); - - if (!data.seasons || data.seasons.length === 0) { - container.innerHTML = '
Aucune saison disponible
'; - card.appendChild(container); - return; - } - - // Create HTML for all seasons - let html = '
Toutes les saisons
'; - - for (const season of data.seasons) { - const seasonId = `season-${encodeURIComponent(season.url)}`; - - html += ` -
-
-
${escapeHtml(season.title)}
-
${season.episode_count || '?'} épisodes
-
- - -
- `; - } - - container.innerHTML = html; - card.appendChild(container); - - } catch (error) { - console.error('Error loading all seasons:', error); - container.innerHTML = '
Erreur de chargement des saisons
'; - card.appendChild(container); - } + // Logic to render cards would go here, but for now we expect HTMX to handle core search + } catch (e) { console.error(e); } } -/** - * Toggle season episodes visibility - */ -function toggleSeasonEpisodes(seasonId) { - const episodesDiv = document.getElementById(`${seasonId}-episodes`); - const button = episodesDiv.parentElement.querySelector('button[onclick^="toggleSeasonEpisodes"]'); - - if (episodesDiv.style.display === 'none') { - episodesDiv.style.display = 'block'; - button.textContent = '▲ Masquer les épisodes'; - - // Load episodes if not already loaded - const select = episodesDiv.querySelector('.episode-select'); - if (select && select.options.length <= 1) { - const seasonUrl = select.dataset.seasonUrl; - loadSeasonEpisodes(seasonUrl, select); - } - } else { - episodesDiv.style.display = 'none'; - button.textContent = '▼ Afficher les épisodes'; - } -} - -/** - * Load episodes for a specific season - */ -async function loadSeasonEpisodes(seasonUrl, selectElement) { - try { - selectElement.innerHTML = ''; - selectElement.disabled = true; - - const data = await loadEpisodes(decodeURIComponent(seasonUrl), 'vostfr'); - - if (data.episodes && data.episodes.length > 0) { - selectElement.innerHTML = ''; - data.episodes.forEach(ep => { - const option = document.createElement('option'); - option.value = ep.url; - option.textContent = `Épisode ${ep.episode}`; - selectElement.appendChild(option); - }); - selectElement.disabled = false; - - // Show action buttons - const actionsDiv = selectElement.parentElement.querySelector('.season-actions'); - if (actionsDiv) { - actionsDiv.style.display = 'flex'; - } - } else { - selectElement.innerHTML = ''; - } - } catch (error) { - console.error('Error loading episodes:', error); - selectElement.innerHTML = ''; - } -} - -/** - * Download selected episode from season - */ -async function downloadSeasonEpisode(encodedSeasonUrl, providerId, lang) { - const seasonUrl = decodeURIComponent(encodedSeasonUrl); - const selectElement = document.querySelector(`[data-season-url="${seasonUrl}"]`); - - if (!selectElement) { - console.error('Select element not found'); - return; - } - - const episodeUrl = selectElement.value; - if (!episodeUrl) { - alert('Veuillez sélectionner un épisode'); - return; - } - - try { - await downloadEpisode(episodeUrl); - loadDownloads(); - alert('Téléchargement démarré!'); - selectElement.value = ''; - } catch (error) { - console.error('Download error:', error); - alert('Erreur lors du démarrage du téléchargement'); - } -} - -/** - * Download entire season - */ -async function downloadEntireSeason(encodedSeasonUrl, lang) { - const seasonUrl = decodeURIComponent(encodedSeasonUrl); - - if (!confirm(`⚠️ Attention: Vous allez télécharger toute cette saison. Cela peut prendre du temps et utiliser beaucoup d'espace disque.\n\nVoulez-vous continuer ?`)) { - return; - } - - try { - const data = await downloadSeason(seasonUrl, lang); - loadDownloads(); - alert(`✅ ${data.message}\n\n${data.total_episodes} épisodes ont été ajoutés à la file de téléchargement!`); - } catch (error) { - console.error('Season download error:', error); - alert('Erreur lors du démarrage du téléchargement de la saison'); - } -} - -/** - * Handle search form submission - */ -async function handleSearch() { - const query = document.getElementById('searchInput').value.trim(); - - if (!query) return; - - // Use the new anime details search - await searchAnimeDetails(query); -} - -// Handle anime search (new dedicated function) async function handleAnimeSearch() { - const searchInput = document.getElementById('animeSearchInput') || document.getElementById('searchInput'); - if (!searchInput) return; - - const query = searchInput.value.trim(); - if (!query) return; - - // Use the new anime details search - await searchAnimeDetails(query); + console.log('Legacy handleAnimeSearch - using HTMX form instead'); } -// Ensure global scope -window.handleSearch = handleSearch; +window.loadAnimeReleases = loadAnimeReleases; window.handleAnimeSearch = handleAnimeSearch; - -/** - * Handle direct download form submission - */ -async function handleDirectDownload(e) { - e.preventDefault(); - const url = document.getElementById('urlInput').value; - - try { - await startDownload(url); - document.getElementById('urlInput').value = ''; - loadDownloads(); - } catch (error) { - console.error('Download error:', error); - alert('Erreur lors du démarrage du téléchargement'); - } -} - -// Ensure all functions are globally accessible -window.displaySearchResults = displaySearchResults; -window.renderAnimeCard = renderAnimeCard; -window.renderAnimeMetadata = renderAnimeMetadata; -window.loadSeasonsForAnime = loadSeasonsForAnime; -window.handleSeasonChange = handleSeasonChange; -window.loadEpisodesForAnime = loadEpisodesForAnime; -window.handleDownloadEpisode = handleDownloadEpisode; -window.handleDownloadSeason = handleDownloadSeason; -window.handleSearch = handleSearch; -window.handleDirectDownload = handleDirectDownload; -window.loadAllSeasonsAndEpisodes = loadAllSeasonsAndEpisodes; -window.toggleSeasonEpisodes = toggleSeasonEpisodes; -window.loadSeasonEpisodes = loadSeasonEpisodes; -window.downloadSeasonEpisode = downloadSeasonEpisode; -window.downloadEntireSeason = downloadEntireSeason; diff --git a/static/js/downloads.js b/static/js/downloads.js index 1837d46..1a940c2 100644 --- a/static/js/downloads.js +++ b/static/js/downloads.js @@ -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 = ` -
Total: ${stats.total}
- ${stats.downloading > 0 ? `
En cours: ${stats.downloading}
` : ''} - ${stats.paused > 0 ? `
En pause: ${stats.paused}
` : ''} - ${stats.completed > 0 ? `
Terminés: ${stats.completed}
` : ''} - ${stats.cancelled > 0 ? `
Annulés: ${stats.cancelled}
` : ''} - ${stats.failed > 0 ? `
Échoués: ${stats.failed}
` : ''} - `; - - 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 = ` -
- - - -

Aucun téléchargement trouvé

-
- `; - 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 += ` -
-
-
${escapeHtml(groupName)}
-
${groupDownloads.length}
-
-
- ${groupDownloads.map(dl => renderDownloadItem(dl)).join('')} -
-
- `; - }); - container.innerHTML = html; - } else { - // Display flat list - container.innerHTML = downloads.map(dl => renderDownloadItem(dl)).join(''); - } -} - -/** - * Render a single download item - */ -function renderDownloadItem(dl) { - return ` -
-
-
${escapeHtml(dl.filename)}
- ${translateStatus(dl.status)} -
-
-
-
-
- ${formatBytes(dl.downloaded_bytes)}${dl.total_bytes ? ' / ' + formatBytes(dl.total_bytes) : ''} - ${dl.speed > 0 ? formatSpeed(dl.speed) : ''} -
-
- ${renderDownloadActions(dl)} -
- ${dl.error ? `
${escapeHtml(dl.error)}
` : ''} -
- `; -} - -/** - * Render download action buttons based on status - */ -function renderDownloadActions(dl) { - switch (dl.status) { - case 'downloading': - return ` - - - `; - - case 'paused': - return ` - - - `; - - case 'completed': - return ` - - - - `; - - case 'failed': - default: - return ` - - `; - } -} - -/** - * 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'); -} +}; diff --git a/static/js/watchlist-ui.js b/static/js/watchlist-ui.js index e881a37..07cc8a9 100644 --- a/static/js/watchlist-ui.js +++ b/static/js/watchlist-ui.js @@ -1,572 +1,18 @@ /** - * Watchlist UI functions + * Watchlist UI (Legacy - Modernized to HTMX) */ -/** - * Escape HTML to prevent XSS - */ -function escapeHtml(text) { - if (!text) return ''; - const div = document.createElement('div'); - div.textContent = text; - return div.innerHTML; -} - -/** - * Display watchlist items - */ -async function displayWatchlist(status = null) { - const container = document.getElementById('watchlistContainer'); - if (!container) return; - - try { - container.innerHTML = '
Chargement de la watchlist...
'; - - const items = await getWatchlist(status); - const stats = await getWatchlistStats(); - - if (items.length === 0) { - container.innerHTML = ` -
-
- - - -

Aucun anime dans votre watchlist

-

Ajoutez des animes depuis la recherche pour commencer le suivi automatique

-
-
- `; - return; - } - - // Render stats - let statsHtml = ''; - if (stats && stats.total > 0) { - statsHtml = ` -
-
-
${stats.total}
-
Total
-
-
-
${stats.active}
-
Actifs
-
-
-
${stats.paused}
-
En pause
-
-
-
${stats.completed}
-
Terminés
-
-
- `; - } - - // Render items - let itemsHtml = ''; - items.forEach(item => { - const statusIcon = getStatusIcon(item.status); - const statusBadge = getStatusBadge(item.status); - const lastEpInfo = item.last_episode_downloaded > 0 - ? `Dernier épisode: ${item.last_episode_downloaded}` - : ''; - - itemsHtml += ` -
-
-
-
-

${escapeHtml(item.anime_title)}

- ${statusBadge} -
- -
- ${statusIcon} ${item.provider_id} • ${item.lang.toUpperCase()} -
- - ${lastEpInfo ? ` -
- ${lastEpInfo} -
- ` : ''} - - ${item.last_checked ? ` -
- Dernière vérification: ${new Date(item.last_checked).toLocaleString('fr-FR')} -
- ` : '
Jamais vérifié
'} -
- -
- ${item.status === 'active' && item.auto_download ? ` - - ` : item.status === 'paused' ? ` - - ` : ''} - - - - -
-
- - ${item.synopsis ? ` -
- 📖 Synopsis -

${escapeHtml(item.synopsis)}

-
- ` : ''} -
- `; - }); - - container.innerHTML = statsHtml + itemsHtml; - - } catch (error) { - console.error('Error loading watchlist:', error); - container.innerHTML = ` -
- ❌ Erreur lors du chargement: ${error.message} -
- `; +async function displayWatchlist() { + console.log('Legacy displayWatchlist called - redirected to HTMX'); + if (typeof htmx !== 'undefined') { + htmx.trigger('#watchlist-items-container', 'load'); } } -/** - * Get status icon - */ -function getStatusIcon(status) { - const icons = { - 'active': '✅', - 'paused': '⏸️', - 'completed': '✨', - 'archived': '📦' - }; - return icons[status] || '📌'; -} - -/** - * Get status badge - */ -function getStatusBadge(status) { - const badges = { - 'active': 'Actif', - 'paused': 'En pause', - 'completed': 'Terminé', - 'archived': 'Archivé' - }; - return badges[status] || ''; -} - -/** - * Add anime to watchlist from search results - */ -async function handleAddToWatchlist(animeUrl, providerId) { - try { - // Decode URL if it's encoded - always work with decoded URL - let decodedUrl = animeUrl; - try { - decodedUrl = decodeURIComponent(animeUrl); - } catch (e) { - // URL might already be decoded - } - - // Get anime details from the DOM or API - const response = await fetch(`${API_BASE}/anime/metadata?url=${encodeURIComponent(decodedUrl)}`); - - if (!response.ok) { - throw new Error('Failed to fetch anime details'); - } - - const data = await response.json(); - const metadata = data.metadata || {}; - - // Extract anime title from URL if not in metadata - let animeTitle = metadata.title || 'Unknown Anime'; - if (animeTitle === 'Unknown Anime' || !animeTitle) { - // Try to extract title from URL - try { - const urlParts = decodedUrl.split('/'); - // Find the anime name (usually between /catalogue/ and /saison/ or /vostfr/) - const catalogueIndex = urlParts.indexOf('catalogue'); - if (catalogueIndex >= 0 && urlParts[catalogueIndex + 1]) { - animeTitle = urlParts[catalogueIndex + 1]; - } else { - // Fallback: use last part - animeTitle = urlParts[urlParts.length - 2] || urlParts[urlParts.length - 1]; - } - animeTitle = animeTitle.replace(/-/g, ' ').replace(/\+/g, ' ').replace(/\s+/g, ' ').trim(); - // Capitalize words - animeTitle = animeTitle.replace(/\b\w/g, l => l.toUpperCase()); - } catch (e) { - console.warn('Could not extract title from URL:', e); - } - } - - // Normalize provider_id to use dash format (anime-sama not animesama) - let normalizedProviderId = providerId; - if (providerId === 'animesama') { - normalizedProviderId = 'anime-sama'; - } - - const itemData = { - anime_title: animeTitle, - anime_url: decodedUrl, // Always use decoded URL - provider_id: normalizedProviderId, - lang: 'vostfr', - auto_download: true, - quality_preference: 'auto', - poster_image: metadata.poster_image || null, - cover_image: metadata.cover_image || null, - synopsis: metadata.synopsis || null, - genres: metadata.genres || [] - }; - - const result = await addToWatchlist(itemData); - - // Trigger download of all episodes immediately - try { - const token = getToken(); - const downloadResponse = await fetch(`${API_BASE}/watchlist/${result.id}/download-all`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}` - } - }); - - if (downloadResponse.ok) { - const downloadResult = await downloadResponse.json(); - const episodeCount = downloadResult.total_episodes || 'tous les'; - alert(`✅ "${result.anime_title}" a été ajouté!\n\n📥 Téléchargement de la dernière saison lancé (${episodeCount} épisodes).\n\nVous recevrez automatiquement les nouveaux épisodes.`); - } else { - // Still show success even if download failed - alert(`✅ "${result.anime_title}" a été ajouté à votre watchlist!\n\nLe téléchargement automatique des nouveaux épisodes est activé.`); - } - } catch (downloadError) { - console.warn('Auto-download trigger failed:', downloadError); - alert(`✅ "${result.anime_title}" a été ajouté à votre watchlist!\n\nLe téléchargement automatique des nouveaux épisodes est activé.`); - } - - // Update button to show it's already in watchlist - updateAddButton(animeUrl, true); - - - } catch (error) { - console.error('Error adding to watchlist:', error); - alert(`❌ Erreur: ${error.message}`); - } -} - -/** - * Update add button state - */ -function updateAddButton(animeUrl, isInWatchlist) { - // Decode URL for matching - let decodedUrl = animeUrl; - try { - decodedUrl = decodeURIComponent(animeUrl); - } catch (e) {} - - // Find all buttons for this anime (try both encoded and decoded) - const buttons = document.querySelectorAll(`[data-watchlist-url="${encodeURIComponent(decodedUrl)}"], [data-watchlist-url="${decodedUrl}"]`); - - buttons.forEach(button => { - if (isInWatchlist) { - button.innerHTML = '✓ Suivi'; - button.disabled = true; - button.style.opacity = '0.6'; - } else { - button.innerHTML = '+ Suivre'; - button.disabled = false; - button.style.opacity = '1'; - } - }); -} - -/** - * Pause watchlist item - */ -async function handlePauseWatchlist(itemId) { - try { - await pauseWatchlistItem(itemId); - await displayWatchlist(); - alert('✅ Anime mis en pause'); - } catch (error) { - console.error('Error pausing item:', error); - alert(`❌ Erreur: ${error.message}`); - } -} - -/** - * Resume watchlist item - */ -async function handleResumeWatchlist(itemId) { - try { - await resumeWatchlistItem(itemId); - await displayWatchlist(); - alert('✅ Anime réactivé'); - } catch (error) { - console.error('Error resuming item:', error); - alert(`❌ Erreur: ${error.message}`); - } -} - -/** - * Check specific item - */ -async function handleCheckItem(itemId) { - const button = this; - const originalText = button.innerHTML; - - try { - button.disabled = true; - button.innerHTML = '⏳...'; - - const result = await checkWatchlistItem(itemId); - - if (result.new_episodes_found > 0) { - alert(`🎉 ${result.new_episodes_found} nouveau(x) épisode(s) trouvé(s)!\n\n${result.episodes_downloaded.length} téléchargé(s)`); - } else { - alert('ℹ️ Aucun nouvel épisode trouvé'); - } - - await displayWatchlist(); - - } catch (error) { - console.error('Error checking item:', error); - alert(`❌ Erreur: ${error.message}`); - } finally { - button.disabled = false; - button.innerHTML = originalText; - } -} - -/** - * Delete watchlist item - */ -async function handleDeleteWatchlist(itemId) { - if (!confirm('⚠️ Êtes-vous sûr de vouloir supprimer cet anime de votre watchlist ?')) { - return; - } - - try { - await deleteFromWatchlist(itemId); - await displayWatchlist(); - alert('✅ Anime supprimé de la watchlist'); - } catch (error) { - console.error('Error deleting item:', error); - alert(`❌ Erreur: ${error.message}`); - } -} - -/** - * Check all items - */ -async function handleCheckAll() { - const button = this; - const originalText = button.innerHTML; - - try { - button.disabled = true; - button.innerHTML = '⏳ Vérification...'; - - const result = await checkAllWatchlistItems(); - - alert(`✅ Vérification terminée!\n\n${result.checked} animes vérifiés\n${result.total_new_episodes} nouveaux épisodes trouvés\n${result.total_downloaded} téléchargés`); - - await displayWatchlist(); - - } catch (error) { - console.error('Error checking all:', error); - alert(`❌ Erreur: ${error.message}`); - } finally { - button.disabled = false; - button.innerHTML = originalText; - } -} - - - -/** - * Create settings modal HTML - */ -function createSettingsModal(settings) { - const modalHtml = ` -
-
-
-

⚙️ Paramètres Watchlist

- -
- -
- -
- - -

Entre 1 et 168 heures (1 semaine)

-
- - -
-
-
📥 Téléchargement automatique
-

Télécharger automatiquement les nouveaux épisodes

-
- -
- - -
- - -

Maximum 5 téléchargements en parallèle

-
- - -
-
-
🔔 Notifications
-

Être notifié des nouveaux épisodes

-
- -
-
- -
- - -
-
-
- - - `; - - return modalHtml; -} - -/** - * Close settings modal - */ -function closeSettingsModal() { - const modal = document.getElementById('settingsModal'); - if (modal) { - modal.remove(); - } -} - -/** - * Save settings - */ -async function saveSettings() { - try { - const checkInterval = parseInt(document.getElementById('checkInterval').value); - const autoDownloadEnabled = document.getElementById('autoDownloadEnabled').checked; - const maxConcurrent = parseInt(document.getElementById('maxConcurrent').value); - const notifyEnabled = document.getElementById('notifyEnabled').checked; - - const settings = { - check_interval_hours: checkInterval, - auto_download_enabled: autoDownloadEnabled, - max_concurrent_auto_downloads: maxConcurrent, - notify_on_new_episodes: notifyEnabled - }; - - await updateWatchlistSettings(settings); - - // Restart scheduler if it's running to apply new interval - const status = await getSchedulerStatus(); - if (status.running) { - await stopScheduler(); - await startScheduler(); - } - - closeSettingsModal(); - alert('✅ Paramètres enregistrés avec succès!'); - await loadSchedulerStatus(); - - } catch (error) { - console.error('Error saving settings:', error); - alert(`❌ Erreur: ${error.message}`); - } -} - -// Make functions available globally +// Global exposure for legacy calls window.displayWatchlist = displayWatchlist; -window.handleAddToWatchlist = handleAddToWatchlist; -window.handlePauseWatchlist = handlePauseWatchlist; -window.handleResumeWatchlist = handleResumeWatchlist; -window.handleCheckItem = handleCheckItem; -window.handleDeleteWatchlist = handleDeleteWatchlist; -window.handleCheckAll = handleCheckAll; -window.createSettingsModal = createSettingsModal; -window.closeSettingsModal = closeSettingsModal; -window.saveSettings = saveSettings; \ No newline at end of file +window.handleDeleteFromWatchlist = (id) => { + if (confirm('Retirer de la watchlist ?')) { + htmx.ajax('DELETE', `/api/watchlist/${id}`, { target: `#watchlist-${id}`, swap: 'outerHTML' }); + } +};