Files
downloader_video/public/assets/js/frontend.js
T
2026-06-06 16:07:01 +02:00

373 lines
16 KiB
JavaScript

$(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 = `<div class="progress-container"><div class="progress-fill" style="width:${pct}%;"></div></div>`;
statusHtml = `<span class="status-text"><i class="fas fa-download"></i> Téléchargement : ${pct}%</span>`;
}
else if (status === 'background_downloading') {
// 🔄 Affiche la barre indéterminée avec la classe 'indeterminate'
progressHtml = `<div class="progress-container"><div class="progress-fill indeterminate"></div></div>`;
statusHtml = `<span class="status-text"><i class="fas fa-spinner spinner-icon"></i> Tâche de fond a été créer. une fois le traitement terminé, vous recevrez un e-mail.</span>`;
}
else if (status === 'initializing') {
progressHtml = `<div class="progress-container"><div class="progress-fill" style="width:0%;"></div></div>`;
statusHtml = `<span class="status-text pending-text"><i class="fas fa-spinner spinner-icon"></i> Initialisation...</span>`;
}
else if (status === 'pending') {
progressHtml = `<div class="progress-container"><div class="progress-fill" style="width:0%;"></div></div>`;
statusHtml = `<span class="status-text pending-text"><i class="fas fa-clock"></i> En attente</span>`;
}
else if (status === 'completed') {
progressHtml = `<div class="progress-container"><div class="progress-fill" style="width:100%; background:#22c55e;"></div></div>`;
statusHtml = `<span class="status-text success-text"><i class="fas fa-check-circle"></i> Terminé</span>`;
actionHtml = `<a class="action-btn download-btn" data-id="${entry.id}" href="${entry.pathDownload}"><i class="fas fa-save"></i> Télécharger</a>`;
}
else if (status === 'error') {
progressHtml = `<div class="progress-container"><div class="progress-fill error" style="width:100%;"></div></div>`;
const errMsg = entry.errorMessage || "Erreur inconnue";
statusHtml = `<span class="status-text error-text"><i class="fas fa-exclamation-triangle"></i> ${escapeHtml(errMsg)}</span>`;
}
else if (status === 'background_requires_auth') {
progressHtml = `<div class="progress-container"><div class="progress-fill" style="width:0%;"></div></div>`;
statusHtml = `<span class="status-text pending-text"><i class="fas fa-lock"></i> Connexion requise pour tâche de fond</span>`;
actionHtml = `<button class="action-btn outline" data-id="${entry.id}" data-action="login"><i class="fas fa-sign-in-alt"></i> Connexion</button>`;
}
else if (status === 'background_ready') {
progressHtml = `<div class="progress-container"><div class="progress-fill" style="width:0%;"></div></div>`;
statusHtml = `<span class="status-text pending-text"><i class="fas fa-cloud-upload-alt"></i> Prêt pour tâche de fond</span>`;
actionHtml = `<button class="btn btn-primary action-btn" data-id="${entry.id}" data-action="startBg"><i class="fas fa-play"></i> Lancer</button>`;
}
else if (status === 'ready_stream') {
// Transition rapide, normalement ne devrait pas rester visible
statusHtml = `<span class="status-text">Prêt pour téléchargement...</span>`;
progressHtml = `<div class="progress-container"><div class="progress-fill" style="width:0%;"></div></div>`;
}
const itemHtml = `
<div class="video-item" data-entry-id="${entry.id}">
<div class="video-header">
<span class="video-name"><i class="fab fa-youtube" style="color:#ef4444; margin-right:6px;"></i>${escapeHtml(entry.name)}</span>
${entry.sizeMB ? `<span style="font-size:0.7rem; background:#eef2ff; padding:0.2rem 0.6rem; border-radius:1rem;">${entry.sizeMB} Mo</span>` : ''}
</div>
${progressHtml}
<div class="status-area">
${statusHtml}
${actionHtml}
</div>
</div>
`;
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 '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
return m;
});
}
// Initialisation
updateAuthUI();
renderVideoList();
});