$(function () { // ------------------------- CONFIGURATION DES ENDPOINTS (à adapter avec vos vraies routes Symfony) ------------------------- // ------------------------- VARIABLES GLOBALES ------------------------- let videoEntries = []; // liste des vidéos let entryIdCounter = 0; let isProcessing = false; // file d'attente active ? let isLoggedIn = false; // état connexion simulé (pour démo) // Mise à jour affichage connexion function updateAuthUI() { const authDiv = $('#authToggle'); if (isLoggedIn) { authDiv.addClass('connected'); $('#authStatusText').text('Connecté'); } else { authDiv.removeClass('connected'); $('#authStatusText').text('Non connecté'); } } // Récupération des infos vidéo (nom + taille Mo) function fetchVideoInfo(url, format) { return new Promise((resolve, reject) => { // Simulation d'appel AJAX vers app_frontend_size_movie $.post(ENDPOINTS.getInfo, { url: url }, function (data) { console.log(data); resolve({ name: data.filename, sizeMB: data.size }); }).fail(function (response) { reject(new Error("Erreur serveur lors de la récupération des infos")); return; }); }); } // Téléchargement direct avec progression (streamed) // Téléchargement direct via SSE avec progression réelle function startStreamedDownload(entry, onProgress) { return new Promise((resolve, reject) => { const encodedVideoUrl = encodeURIComponent(entry.url); const finalUrl = `${ENDPOINTS.streamDownload}?url=${encodedVideoUrl}&format=${entry.format}`; const eventSource = new EventSource(finalUrl); let isResolved = false; // ✅ Fonction de nettoyage centralisée function cleanup(timeoutId) { clearTimeout(timeoutId); eventSource.close(); } const timeout = setTimeout(() => { if (!isResolved) { isResolved = true; cleanup(null); // timeout déjà déclenché reject(new Error('Délai d\'attente dépassé')); } }, 300000); eventSource.onmessage = function (event) { try { const data = JSON.parse(event.data); if (data.progress !== undefined) { onProgress(data.progress); } // ✅ Gérer l'erreur serveur if (data.error === true) { if (!isResolved) { isResolved = true; cleanup(timeout); reject(new Error(data.message || 'Erreur serveur pendant le téléchargement')); } } if (data.done === true) { if (!isResolved) { isResolved = true; cleanup(timeout); // ✅ nettoyer le timeout avant resolve updateEntry(entry.id, { pathDownload: data.pathDownload }); resolve({ success: true }); } } } catch (err) { console.error('[SSE] Erreur parsing JSON:', err); } }; eventSource.onerror = function () { // ✅ Ne rejeter QUE si pas encore résolu (évite faux reject après done) if (!isResolved) { isResolved = true; cleanup(timeout); reject(new Error('Erreur de connexion SSE')); } }; }); } // Téléchargement en tâche de fond (barre infinie) function startBackgroundDownload(entry) { return new Promise((resolve, reject) => { console.log(`[BACKGROUND] Lancement tâche de fond pour : ${entry.name}`); // Appel POST vers app_background_download $.post(ENDPOINTS.backgroundDownload, { url: entry.url, format: entry.format }, function (data) { resolve({ success: true, jobId: "bg_" + entry.id }); }).fail(function (response) { reject(new Error("Échec du téléchargement en arrière-plan")); }); }); } // Rendu de la liste avec les différents statuts function renderVideoList() { const container = $('#videoListContainer'); container.empty(); videoEntries.forEach(entry => { let progressHtml = ''; let statusHtml = ''; let actionHtml = ''; const status = entry.status; // Gestion de la barre de progression if (status === 'downloading') { const pct = entry.progress || 0; progressHtml = `
`; statusHtml = ` Téléchargement : ${pct}%`; } else if (status === 'background_downloading') { // 🔄 Affiche la barre indéterminée avec la classe 'indeterminate' progressHtml = `
`; statusHtml = ` Tâche de fond a été créer. une fois le traitement terminé, vous recevrez un e-mail.`; } else if (status === 'initializing') { progressHtml = `
`; statusHtml = ` Initialisation...`; } else if (status === 'pending') { progressHtml = `
`; statusHtml = ` En attente`; } else if (status === 'completed') { progressHtml = `
`; statusHtml = ` Terminé`; actionHtml = ` Télécharger`; } else if (status === 'error') { progressHtml = `
`; const errMsg = entry.errorMessage || "Erreur inconnue"; statusHtml = ` ${escapeHtml(errMsg)}`; } else if (status === 'background_requires_auth') { progressHtml = `
`; statusHtml = ` Connexion requise pour tâche de fond`; actionHtml = ``; } else if (status === 'background_ready') { progressHtml = `
`; statusHtml = ` Prêt pour tâche de fond`; actionHtml = ``; } else if (status === 'ready_stream') { // Transition rapide, normalement ne devrait pas rester visible statusHtml = `Prêt pour téléchargement...`; progressHtml = `
`; } const itemHtml = `
${escapeHtml(entry.name)} ${entry.sizeMB ? `${entry.sizeMB} Mo` : ''}
${progressHtml}
${statusHtml} ${actionHtml}
`; container.append(itemHtml); }); } // Mise à jour d'une entrée spécifique et re-rendu function updateEntry(entryId, changes) { const index = videoEntries.findIndex(e => e.id === entryId); if (index !== -1) { Object.assign(videoEntries[index], changes); renderVideoList(); } } // Ajout des URLs en statut "pending" function addUrls(urlsArray, format) { const newEntries = []; urlsArray.forEach(url => { const trimmed = url.trim(); if (trimmed === '') return; const displayName = `Nouvelle vidéo (${trimmed.substring(0, 30)}...)`; newEntries.push({ id: ++entryIdCounter, url: trimmed, name: displayName, format: format, status: 'pending', progress: 0, sizeMB: null, errorMessage: '' }); }); videoEntries.push(...newEntries); renderVideoList(); return newEntries; } // Traitement d'une vidéo (séquentiel) async function processEntry(entry) { // Évite de traiter une vidéo déjà en cours ou terminée if (entry.status !== 'pending' && entry.status !== 'background_ready' && entry.status !== 'background_requires_auth') { return; } // 1. Initialisation : appel à getInfo updateEntry(entry.id, { status: 'initializing', progress: 0, errorMessage: '' }); try { const info = await fetchVideoInfo(entry.url, entry.format); const sizeMo = info.sizeMB; const videoName = info.name; updateEntry(entry.id, { name: videoName, sizeMB: sizeMo }); // 2. Décision selon taille if (sizeMo !== null && sizeMo <= 250) { // Taille <= 250 Mo => téléchargement direct (en cours) updateEntry(entry.id, { status: 'downloading', progress: 0 }); try { await startStreamedDownload(entry, (prog) => { updateEntry(entry.id, { progress: prog }); }); updateEntry(entry.id, { status: 'completed', progress: 100 }); } catch (err) { updateEntry(entry.id, { status: 'error', errorMessage: `Échec direct : ${err.message}` }); } } else { // Taille > 250 Mo ou inconnue => tâche de fond const auth = await checkAuth(); if (auth) { // Connecté => bouton "Lancer" (en tache de fond prêt) updateEntry(entry.id, { status: 'background_ready' }); } else { // Non connecté => bouton "Connexion" updateEntry(entry.id, { status: 'background_requires_auth' }); } } } catch (err) { updateEntry(entry.id, { status: 'error', errorMessage: `Erreur initialisation : ${err.message}` }); } } // Lancement du background après clic sur "Lancer" async function launchBackground(entry) { if (entry.status !== 'background_ready') return; try { // 🟢 Passe la vidéo en mode 'background_downloading' pour déclencher l'affichage de la barre indéterminée updateEntry(entry.id, { status: 'background_downloading', progress: 0 }); await startBackgroundDownload(entry); } catch (err) { // 🔴 En cas d'erreur, affiche le message d'erreur updateEntry(entry.id, { status: 'error', errorMessage: `Tâche de fond échouée : ${err.message}` }); } } // Gestion de la file d'attente séquentielle async function processQueue() { if (isProcessing) return; isProcessing = true; // On traite uniquement les vidéos en attente ou celles en background_ready/requires_auth ? // Pour éviter de bloquer la queue, on ne traite que les "pending" const pendingEntries = videoEntries.filter(e => e.status === 'pending'); for (const entry of pendingEntries) { await processEntry(entry); // Une fois cette vidéo traitée (même si elle est en background_ready ou error), on passe à la suivante // Mais attention : si la vidéo est en background_ready, elle attend une action utilisateur, donc on ne bloque pas la file. } isProcessing = false; } // Déclenche l'ajout des URLs et démarre la queue function startNewDownloads(urls, format) { if (urls.length === 0) { alert("Ajoutez au moins une URL YouTube."); return false; } const newEntries = addUrls(urls, format); processQueue(); return true; } // Gestion des clics sur les boutons (connexion, lancer background, téléchargement final) $(document).on('click', '.action-btn', async function (e) { const entryId = $(this).data('id'); const action = $(this).data('action'); const entry = videoEntries.find(e => e.id === entryId); if (!entry) return; if (action === 'login') { window.open(ENDPOINTS.urlAuth, "_self"); } else if (action === 'startBg') { if (entry.status === 'background_ready') { await launchBackground(entry); // Après fin du background, la queue pourrait reprendre s'il y a d'autres pending, // mais on ne relance pas automatiquement processQueue pour ne pas interférer. } } /*else if ($(this).hasClass('download-btn') && entry.status === 'completed') { // Téléchargement final du fichier (simulé) //window.open(ENDPOINTS.downloadFileAnonyme, "_self"); console.log(); //alert(`Téléchargement du fichier : ${entry.name}\nFormat : ${entry.format}\n(Simulation - fichier prêt)`); }*/ }); // Bascule manuelle de connexion (pour démo) $('#authToggle').on('click', function () { isLoggedIn = !isLoggedIn; updateAuthUI(); // Pour chaque vidéo en background_requires_auth, on la repasse en background_ready videoEntries.forEach(entry => { if (entry.status === 'background_requires_auth' && isLoggedIn) { updateEntry(entry.id, { status: 'background_ready' }); } else if (entry.status === 'background_ready' && !isLoggedIn) { updateEntry(entry.id, { status: 'background_requires_auth' }); } }); renderVideoList(); }); // Bouton principal "Démarrer le traitement" $('#processBtn').on('click', function () { const urlText = $('#youtubeUrls').val(); const urls = urlText.split('\n').filter(line => line.trim() !== ''); const format = $('#formatSelect').val(); if (urls.length === 0) { alert("Collez au moins une URL YouTube."); return; } startNewDownloads(urls, format); $('#youtubeUrls').val(''); // nettoyer }); // Utilitaires function escapeHtml(text) { return String(text).replace(/[&<>]/g, function (m) { if (m === '&') return '&'; if (m === '<') return '<'; if (m === '>') return '>'; return m; }); } // Initialisation updateAuthUI(); renderVideoList(); });