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 `
-
-
- ${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 += `
-
-
-
- ${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 `
-
-
-
-
- ${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' });
+ }
+};