373 lines
16 KiB
JavaScript
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 '&';
|
|
if (m === '<') return '<';
|
|
if (m === '>') return '>';
|
|
return m;
|
|
});
|
|
}
|
|
|
|
// Initialisation
|
|
updateAuthUI();
|
|
renderVideoList();
|
|
}); |