Video-Downloader / app /static /index.html
NathMen12's picture
Update app/static/index.html
eb8ac69 verified
Raw
History Blame Contribute Delete
27.1 kB
<!-- app/static/index.html -->
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pro Video Downloader</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; }
.step-indicator { @apply relative flex items-center px-4 py-2 text-sm font-medium text-gray-500 rounded-full bg-gray-100 dark:bg-gray-800 dark:text-gray-400; }
.step-indicator.active { @apply bg-blue-600 text-white; }
.step-indicator.done { @apply bg-green-600 text-white; }
.step-connector { @apply absolute left-full w-8 h-0.5 bg-gray-200 dark:bg-gray-700; }
.progress-bar { transition: width 0.3s ease-out; }
.format-option input:checked + div { @apply border-blue-600 bg-blue-50 dark:bg-blue-900/20; }
.log-entry { @apply text-xs font-mono px-2 py-1 rounded; }
</style>
</head>
<body class="bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100 min-h-screen flex flex-col">
<header class="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 sticky top-0 z-10">
<div class="max-w-4xl mx-auto px-4 py-4 flex items-center justify-between">
<h1 class="text-2xl font-bold text-gray-800 dark:text-white flex items-center gap-2">
<svg class="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
Pro Downloader
</h1>
<span class="text-xs text-gray-500 px-2 py-1 bg-gray-100 dark:bg-gray-800 rounded">yt-dlp + FastAPI</span>
</div>
</header>
<main class="flex-1 max-w-4xl w-full mx-auto px-4 py-8 flex flex-col items-center">
<!-- Stepper -->
<div class="w-full mb-8 flex items-center justify-center relative" id="stepper">
<div class="step-indicator active" data-step="1"><span>1. Lien</span></div>
<div class="step-connector"></div>
<div class="step-indicator" data-step="2"><span>2. Options</span></div>
<div class="step-connector"></div>
<div class="step-indicator" data-step="3"><span>3. Téléchargement</span></div>
</div>
<!-- ÉTAPE 1 -->
<section id="step-1-view" class="w-full max-w-2xl transition-opacity duration-300">
<div class="bg-white dark:bg-gray-900 rounded-2xl shadow-sm border border-gray-200 dark:border-gray-800 p-6 md:p-8">
<div class="text-center mb-6">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white">Étape 1 : Analyse du lien</h2>
<p class="text-gray-500 dark:text-gray-400 mt-1">Collez l'URL (YouTube, X/Twitter, Vimeo, Facebook, TikTok, Generic m3u8...)</p>
</div>
<div class="flex gap-2">
<input type="url" id="urlInput" placeholder="https://youtube.com/watch?v=... ou https://x.com/.../status/..."
class="flex-1 px-4 py-3 border border-gray-300 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition"
autocomplete="off">
<button id="analyzeBtn" class="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-xl transition flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed">
<svg id="analyzeSpinner" class="w-5 h-5 animate-spin hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
<span id="analyzeText">Analyser</span>
</button>
</div>
<p id="urlError" class="text-red-500 text-sm mt-3 min-h-[20px] hidden"></p>
</div>
</section>
<!-- ÉTAPE 2 -->
<section id="step-2-view" class="w-full max-w-2xl hidden transition-opacity duration-300">
<div class="bg-white dark:bg-gray-900 rounded-2xl shadow-sm border border-gray-200 dark:border-gray-800 p-6 md:p-8">
<!-- Meta -->
<div class="flex gap-4 mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700">
<img id="thumbImg" src="" alt="Thumbnail" class="w-24 h-24 object-cover rounded-lg flex-shrink-0 border border-gray-200 dark:border-gray-700">
<div class="flex-1 min-w-0">
<h3 id="videoTitle" class="font-semibold text-gray-800 dark:text-white truncate">Titre de la vidéo</h3>
<div class="flex flex-wrap gap-4 mt-2 text-sm text-gray-500 dark:text-gray-400">
<span id="videoDuration" class="flex items-center gap-1"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> --:--</span>
<span id="videoUploader" class="flex items-center gap-1"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg> Inconnu</span>
</div>
</div>
</div>
<!-- Type Choice -->
<fieldset class="mb-6">
<legend class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Type de téléchargement</legend>
<div class="flex gap-4">
<label class="flex-1 flex items-center justify-center gap-2 p-4 border-2 rounded-xl cursor-pointer transition bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 hover:border-blue-400 has-[:checked]:border-blue-600 has-[:checked]:bg-blue-50 dark:has-[:checked]:bg-blue-900/20">
<input type="radio" name="dlType" value="video" class="sr-only peer" checked>
<svg class="w-6 h-6 text-gray-400 peer-checked:text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<span class="font-medium text-gray-700 dark:text-gray-200">Vidéo + Son</span>
</label>
<label class="flex-1 flex items-center justify-center gap-2 p-4 border-2 rounded-xl cursor-pointer transition bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700 hover:border-blue-400 has-[:checked]:border-blue-600 has-[:checked]:bg-blue-50 dark:has-[:checked]:bg-blue-900/20">
<input type="radio" name="dlType" value="audio" class="sr-only peer">
<svg class="w-6 h-6 text-gray-400 peer-checked:text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1a1 1 0 011 1v3.586a1 1 0 01-.293.707l-5.586 5.586a1 1 0 01-1.414 0z"></path></svg>
<span class="font-medium text-gray-700 dark:text-gray-200">Audio Only (MP3)</span>
</label>
</div>
</fieldset>
<!-- Format Selection -->
<fieldset class="mb-6">
<div class="flex justify-between items-center mb-3">
<legend class="text-sm font-medium text-gray-700 dark:text-gray-300">Qualité / Format <span id="formatCount" class="text-gray-400 font-normal"></span></legend>
<!-- BOUTON AUTO GLOBAL -->
<button id="autoSelectBtn" class="hidden px-3 py-1.5 text-xs font-semibold text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
Auto (Recommandé)
</button>
</div>
<div id="formatList" class="space-y-2 max-h-72 overflow-y-auto pr-2 border border-gray-200 dark:border-gray-700 rounded-xl p-2 bg-gray-50 dark:bg-gray-800">
<!-- Injecté par JS -->
</div>
<!-- Message si vide -->
<div id="noFormatMsg" class="hidden text-center py-6 text-gray-500 dark:text-gray-400 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-xl">
<svg class="w-12 h-12 mx-auto mb-2 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<p class="mb-2">Aucun format standard détecté pour ce lien.</p>
<button id="forceAutoBtn" class="px-4 py-2 bg-amber-600 hover:bg-amber-700 text-white font-medium rounded-lg transition">Forcer "Best Quality" (Auto)</button>
</div>
</fieldset>
<!-- Actions -->
<div class="flex justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
<button id="backBtn" class="px-4 py-2.5 text-gray-700 dark:text-gray-200 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl font-medium transition">Retour</button>
<button id="startDownloadBtn" disabled class="px-6 py-2.5 bg-green-600 hover:bg-green-700 text-white font-medium rounded-xl transition flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
Lancer le téléchargement
</button>
</div>
</div>
</section>
<!-- ÉTAPE 3 -->
<section id="step-3-view" class="w-full max-w-2xl hidden transition-opacity duration-300">
<div class="bg-white dark:bg-gray-900 rounded-2xl shadow-sm border border-gray-200 dark:border-gray-800 p-6 md:p-8">
<div class="text-center mb-6">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white">Téléchargement en cours</h2>
<p class="text-gray-500 dark:text-gray-400 mt-1 truncate" id="dlFileName" title="">Préparation...</p>
</div>
<div class="mb-6">
<div class="flex justify-between text-sm mb-1">
<span id="dlPercent" class="font-mono font-bold text-blue-600">0%</span>
<span id="dlSpeed" class="text-gray-500 dark:text-gray-400">0 Mo/s</span>
</div>
<div class="h-3 bg-gray-200 dark:bg-gray-800 rounded-full overflow-hidden">
<div id="dlBar" class="progress-bar h-full bg-blue-600 rounded-full" style="width: 0%"></div>
</div>
<div class="flex justify-between text-xs text-gray-500 dark:text-gray-400 mt-1">
<span id="dlSize">0 / 0 Mo</span>
<span id="dlEta">ETA: --:--</span>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-800 rounded-xl p-4 h-48 overflow-y-auto font-mono text-sm text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-gray-700" id="dlLog">
<div class="text-gray-400 log-entry">En attente de connexion...</div>
</div>
<!-- Zone Final -->
<div id="dlCompleteActions" class="hidden mt-6 space-y-4">
<div class="p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-xl text-center">
<svg class="w-12 h-12 mx-auto text-green-600 dark:text-green-400 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<h3 class="font-semibold text-green-800 dark:text-green-200">Prêt à être enregistré</h3>
<p class="text-sm text-green-700 dark:text-green-300 mt-1 truncate" id="finalFileNameDisplay">video.mp4</p>
</div>
<a id="finalDownloadLink" href="#" class="block w-full text-center px-6 py-3 bg-green-600 hover:bg-green-700 text-white font-bold rounded-xl transition text-lg flex items-center justify-center gap-2">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
Télécharger sur l'appareil
</a>
<button id="newDownloadBtn" class="w-full px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white text-sm">Nouveau téléchargement</button>
</div>
</div>
</section>
</main>
<footer class="py-6 px-4 text-center text-xs text-gray-500 dark:text-gray-400 border-t border-gray-200 dark:border-gray-800">
Propulsé par <a href="https://github.com/yt-dlp/yt-dlp" target="_blank" class="underline hover:text-blue-600">yt-dlp</a> & <a href="https://fastapi.tiangolo.com" target="_blank" class="underline hover:text-blue-600">FastAPI</a> — Fichiers auto-supprimés après 30 min
</footer>
<script>
/* ============================================================
FRONTEND LOGIC
============================================================ */
// State
let ws = null;
let currentFormats = [];
let selectedFormat = null;
let isAudioOnly = false;
// DOM
const views = { 1: document.getElementById('step-1-view'), 2: document.getElementById('step-2-view'), 3: document.getElementById('step-3-view') };
const steppers = document.querySelectorAll('#stepper .step-indicator');
const urlInput = document.getElementById('urlInput');
const analyzeBtn = document.getElementById('analyzeBtn');
const analyzeSpinner = document.getElementById('analyzeSpinner');
const analyzeText = document.getElementById('analyzeText');
const urlError = document.getElementById('urlError');
const videoTitle = document.getElementById('videoTitle');
const videoDuration = document.getElementById('videoDuration');
const videoUploader = document.getElementById('videoUploader');
const thumbImg = document.getElementById('thumbImg');
const formatList = document.getElementById('formatList');
const formatCount = document.getElementById('formatCount');
const autoSelectBtn = document.getElementById('autoSelectBtn');
const noFormatMsg = document.getElementById('noFormatMsg');
const forceAutoBtn = document.getElementById('forceAutoBtn');
const backBtn = document.getElementById('backBtn');
const startDownloadBtn = document.getElementById('startDownloadBtn');
const dlTypeRadios = document.querySelectorAll('input[name="dlType"]');
// Step 3
const dlBar = document.getElementById('dlBar');
const dlPercent = document.getElementById('dlPercent');
const dlSpeed = document.getElementById('dlSpeed');
const dlSize = document.getElementById('dlSize');
const dlEta = document.getElementById('dlEta');
const dlLog = document.getElementById('dlLog');
const dlFileName = document.getElementById('dlFileName');
const dlCompleteActions = document.getElementById('dlCompleteActions');
const finalDownloadLink = document.getElementById('finalDownloadLink');
const finalFileNameDisplay = document.getElementById('finalFileNameDisplay');
const newDownloadBtn = document.getElementById('newDownloadBtn');
// Utils
const formatTime = (sec) => { if (!sec || sec < 0) return '--:--'; const h = Math.floor(sec / 3600); const m = Math.floor((sec % 3600) / 60); const s = Math.floor(sec % 60); return (h > 0 ? `${h}:` : '') + `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`; };
const formatBytes = (bytes) => { if (!bytes) return '?'; const k=1024, sizes=['o','Ko','Mo','Go']; const i=Math.floor(Math.log(bytes)/Math.log(k)); return parseFloat((bytes/Math.pow(k,i)).toFixed(1))+' '+sizes[i]; };
const showView = (step) => { Object.values(views).forEach(v=>v.classList.add('hidden')); views[step].classList.remove('hidden'); steppers.forEach((s,i)=>{s.classList.remove('active','done'); if(i+1<step)s.classList.add('done'); else if(i+1===step)s.classList.add('active');}); window.scrollTo({top:0, behavior:'smooth'}); };
const setAnalyzing = (b) => { analyzeBtn.disabled=b; analyzeSpinner.classList.toggle('hidden',!b); analyzeText.textContent=b?'Analyse...':'Analyser'; };
const showError = (msg) => { urlError.textContent=msg; urlError.classList.remove('hidden'); };
const hideError = () => urlError.classList.add('hidden');
const log = (msg, type='info') => { const colors={info:'text-gray-600 dark:text-gray-300',success:'text-green-600 dark:text-green-400',error:'text-red-600 dark:text-red-400',warn:'text-yellow-600 dark:text-yellow-400'}; const time=new Date().toLocaleTimeString(); const el=document.createElement('div'); el.className=`log-entry ${colors[type]}`; el.innerHTML=`<span class="text-gray-400">[${time}]</span> ${msg}`; dlLog.appendChild(el); dlLog.scrollTop=dlLog.scrollHeight; };
// 1. Analyze
analyzeBtn.addEventListener('click', async () => {
const url = urlInput.value.trim();
if (!url) return showError("Veuillez entrer une URL.");
try { new URL(url); } catch { return showError("URL invalide."); }
setAnalyzing(true); hideError();
try {
const res = await fetch('/api/analyze', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({url}) });
const data = await res.json();
if (!res.ok) throw new Error(data.detail || "Erreur analyse");
currentFormats = data.formats || [];
renderMeta(data);
renderFormats(currentFormats);
showView(2);
} catch (e) { showError(e.message); }
finally { setAnalyzing(false); }
});
// 2. Render Meta
function renderMeta(meta) {
videoTitle.textContent = meta.title;
videoDuration.innerHTML = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> ${formatTime(meta.duration)}`;
videoUploader.innerHTML = `<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg> ${meta.uploader || 'Inconnu'}`;
thumbImg.src = meta.thumbnail; thumbImg.alt = meta.title;
}
// 3. Render Formats
function renderFormats(formats) {
formatList.innerHTML = '';
noFormatMsg.classList.add('hidden');
autoSelectBtn.classList.add('hidden');
if (!formats || formats.length === 0) {
noFormatMsg.classList.remove('hidden');
formatCount.textContent = `(0)`;
startDownloadBtn.disabled = true;
return;
}
// Filtrer selon Type (Video vs Audio)
const videoFormats = formats.filter(f => f.vcodec !== 'none' || f.is_auto); // Auto inclut vidéo
const audioFormats = formats.filter(f => f.acodec !== 'none' && f.vcodec === 'none' || (f.is_auto && f.resolution.includes('Audio')));
const displayFormats = isAudioOnly ? audioFormats : videoFormats;
formatCount.textContent = `(${displayFormats.length} dispo)`;
if (displayFormats.length === 0) {
noFormatMsg.classList.remove('hidden');
startDownloadBtn.disabled = true;
return;
}
// Afficher Bouton Auto si il y a des formats "is_auto"
const hasAuto = displayFormats.some(f => f.is_auto);
if (hasAuto) autoSelectBtn.classList.remove('hidden');
displayFormats.forEach((f, i) => {
const isAuto = f.is_auto;
const sizeStr = f.filesize_mb > 0 ? `~${f.filesize_mb} Mo` : `<span class="text-gray-400">Taille estimée...</span>`;
const badge = isAuto ? `<span class="ml-2 px-1.5 py-0.5 text-xs font-bold text-blue-700 bg-blue-100 dark:bg-blue-900 dark:text-blue-300 rounded">Auto</span>` : '';
const el = document.createElement('label');
el.className = 'format-option block';
el.innerHTML = `
<input type="radio" name="format" value="${f.format_id}" class="sr-only peer" data-format='${JSON.stringify(f)}' ${i === 0 ? 'checked' : ''}>
<div class="p-3 border-2 rounded-xl cursor-pointer transition border-gray-200 dark:border-gray-700 hover:border-blue-400 peer-checked:border-blue-600 peer-checked:bg-blue-50 dark:peer-checked:bg-blue-900/20 flex items-center justify-between gap-3">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 font-medium text-gray-800 dark:text-white truncate">${f.resolution}${badge}</div>
<div class="text-xs text-gray-500 dark:text-gray-400 truncate">${f.note}${f.ext.toUpperCase()}</div>
</div>
<div class="text-right text-sm font-mono font-semibold text-gray-700 dark:text-gray-300 whitespace-nowrap">${sizeStr}</div>
</div>
`;
formatList.appendChild(el);
});
updateSelection();
}
// Type Change (Video/Audio)
dlTypeRadios.forEach(r => r.addEventListener('change', (e) => {
isAudioOnly = e.target.value === 'audio';
renderFormats(currentFormats);
}));
// Auto Select Button
autoSelectBtn.addEventListener('click', () => {
const autoFmt = currentFormats.find(f => f.is_auto && (isAudioOnly ? f.resolution.includes('Audio') : f.resolution.includes('p')));
if (autoFmt) {
document.querySelector(`input[name="format"][value="${autoFmt.format_id}"]`)?.click();
}
});
// Force Auto Button (Fallback)
forceAutoBtn.addEventListener('click', () => {
selectedFormat = { format_id: "bestvideo+bestaudio/best", ext: "mp4", resolution: "Forcé Best", is_auto: true };
startDownloadBtn.disabled = false;
startDownloadBtn.click(); // Lance direct
});
// Selection Logic
function updateSelection() {
const checked = document.querySelector('input[name="format"]:checked');
if (checked) {
selectedFormat = JSON.parse(checked.dataset.format);
startDownloadBtn.disabled = false;
} else { selectedFormat = null; startDownloadBtn.disabled = true; }
}
formatList.addEventListener('change', (e) => { if(e.target.name==='format') updateSelection(); });
// Back
backBtn.addEventListener('click', () => showView(1));
// 4. Start Download
startDownloadBtn.addEventListener('click', async () => {
if (!selectedFormat && !forceAutoBtn.classList.contains('hidden')) return; // Safety
const url = urlInput.value.trim();
const audioOnly = isAudioOnly; // Bool
showView(3);
dlCompleteActions.classList.add('hidden');
dlBar.style.width = '0%'; dlPercent.textContent = '0%';
dlLog.innerHTML = ''; dlFileName.textContent = `Connexion...`;
connectWebSocket(url, selectedFormat?.format_id || "bestvideo+bestaudio/best", audioOnly);
});
// WebSocket
function connectWebSocket(url, formatId, audioOnly) {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${proto}//${location.host}/api/download`);
ws.onopen = () => {
log('Connexion établie. Envoi config...', 'success');
ws.send(JSON.stringify({ url, format_id: formatId, audio_only: audioOnly }));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
handleWsMessage(msg);
};
ws.onerror = (err) => log('Erreur WS: ' + err.message, 'error');
ws.onclose = () => log('Connexion fermée.', 'warn');
}
function handleWsMessage(msg) {
switch(msg.stage) {
case 'downloading':
const pct = parseFloat(msg.percent.replace('%','')) || 0;
dlBar.style.width = `${pct}%`;
dlPercent.textContent = msg.percent;
dlSpeed.textContent = `${msg.speed_mbps.toFixed(2)} Mo/s`;
dlSize.textContent = `${msg.downloaded_mb.toFixed(1)} / ${msg.total_mb.toFixed(1)} Mo`;
dlEta.textContent = `ETA: ${formatTime(msg.eta_sec)}`;
if (pct % 10 < 0.5) log(`DL: ${msg.percent} (${msg.speed_mbps.toFixed(1)} Mo/s)`, 'info');
break;
case 'post_processing':
dlBar.style.width = '99%'; dlPercent.textContent = '99%';
dlFileName.textContent = 'Fusion / Conversion (FFmpeg)...';
log('Post-traitement (FFmpeg)...', 'warn');
break;
case 'complete':
dlBar.style.width = '100%'; dlPercent.textContent = '100%';
dlSpeed.textContent = 'Terminé';
dlFileName.textContent = msg.filename;
log(`Fichier prêt: ${msg.filename}`, 'success');
finalDownloadLink.href = msg.download_url;
finalDownloadLink.download = msg.filename; // Force nom fichier
finalFileNameDisplay.textContent = msg.filename;
finalFileNameDisplay.title = msg.filename;
dlCompleteActions.classList.remove('hidden');
if(ws) { ws.close(); ws=null; }
break;
case 'error':
log(`ERREUR: ${msg.info}`, 'error');
dlFileName.textContent = 'Échec';
if(ws) { ws.close(); ws=null; }
break;
}
}
// New Download
newDownloadBtn.addEventListener('click', () => {
urlInput.value = '';
selectedFormat = null;
currentFormats = [];
showView(1);
});
// Init
document.addEventListener('DOMContentLoaded', () => urlInput.focus());
</script>
</body>
</html>