File size: 27,107 Bytes
eb8ac69
5700c19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
eb8ac69
 
5700c19
 
 
 
eb8ac69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5700c19
eb8ac69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5700c19
eb8ac69
 
 
 
 
 
 
 
 
 
 
 
 
 
5700c19
eb8ac69
 
 
 
 
 
 
 
 
 
5700c19
 
eb8ac69
 
5700c19
eb8ac69
 
 
 
 
5700c19
eb8ac69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5700c19
 
eb8ac69
 
 
 
5700c19
eb8ac69
 
5700c19
eb8ac69
 
 
 
 
5700c19
eb8ac69
 
 
 
 
 
 
 
 
 
5700c19
eb8ac69
 
 
 
 
5700c19
eb8ac69
 
5700c19
eb8ac69
5700c19
eb8ac69
 
 
5700c19
 
 
eb8ac69
5700c19
 
eb8ac69
5700c19
eb8ac69
5700c19
eb8ac69
5700c19
eb8ac69
 
5700c19
 
 
 
 
 
 
 
 
 
 
 
 
 
eb8ac69
 
 
5700c19
 
 
 
 
 
 
 
 
 
 
 
 
 
eb8ac69
5700c19
 
eb8ac69
 
 
 
 
 
 
 
 
 
5700c19
 
 
eb8ac69
5700c19
eb8ac69
5700c19
eb8ac69
5700c19
 
eb8ac69
 
5700c19
eb8ac69
5700c19
eb8ac69
 
5700c19
 
 
 
 
 
 
eb8ac69
5700c19
 
 
 
 
eb8ac69
 
 
 
 
 
 
 
 
 
 
 
 
5700c19
eb8ac69
5700c19
eb8ac69
 
 
 
 
 
 
 
 
 
 
5700c19
eb8ac69
 
 
 
 
5700c19
eb8ac69
5700c19
eb8ac69
 
 
 
 
 
 
5700c19
 
 
 
eb8ac69
 
5700c19
 
eb8ac69
 
 
 
5700c19
 
eb8ac69
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5700c19
 
 
 
eb8ac69
5700c19
eb8ac69
5700c19
eb8ac69
5700c19
 
eb8ac69
5700c19
eb8ac69
5700c19
 
eb8ac69
5700c19
 
 
eb8ac69
 
5700c19
eb8ac69
5700c19
 
eb8ac69
5700c19
eb8ac69
 
5700c19
 
eb8ac69
5700c19
 
 
 
 
 
 
eb8ac69
5700c19
 
 
 
eb8ac69
5700c19
eb8ac69
5700c19
 
 
 
 
eb8ac69
5700c19
 
eb8ac69
 
 
5700c19
 
eb8ac69
5700c19
 
eb8ac69
5700c19
 
eb8ac69
 
 
 
5700c19
eb8ac69
5700c19
 
 
eb8ac69
 
5700c19
 
 
 
eb8ac69
5700c19
 
 
eb8ac69
5700c19
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
<!-- 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>