Remplace le "tout télécharger" par téléchargements individuels décalés
(PR précédente) par une unique archive zip, streamée sans être bufferisée
en mémoire (paquet `archiver`).
- server/src/routes/queue.ts : GET /api/queue/download-all, construit le
zip à la volée depuis les fichiers des éléments `completed`, noms
dédupliqués en cas de titres identiques. Point d'attention : le flux
est envoyé à la réponse AVANT archive.finalize(), pas l'inverse -
finalize() n'aboutit qu'une fois tout le contenu écrit dans le flux, ce
qui bloquerait indéfiniment sur une grosse archive si rien ne la lit
encore.
- QueueService : buildDelivery() factorise la logique de livraison
(chemin + nettoyage post-envoi) entre consumeCompletedFile (un fichier)
et le nouveau consumeAllCompletedFiles (tous les fichiers prêts).
- QueuePanel : le bouton "Tout télécharger" déclenche maintenant un seul
lien vers /api/queue/download-all. Tous les liens de téléchargement
(individuel et groupé) portent target="_blank" : si jamais rien n'est
disponible côté serveur au moment du clic, l'erreur s'ouvre dans un
nouvel onglet plutôt que de remplacer l'application.
- test/helpers.ts : extractOutputDir et waitFor factorisés (dupliqués
entre deux fichiers de test) ; waitFor accepte maintenant un prédicat
async (la version précédente, sync uniquement, aurait toujours vu un
prédicat async comme "vrai" immédiatement - une Promise est truthy).
4 nouveaux tests, dont un qui vérifie la signature zip réelle (PK\x03\x04)
sur une archive construite à partir de vrais fichiers temporaires (pas
juste des mocks) - 65 tests serveur au total. Vérifié aussi sur le
serveur réel en cours d'exécution (pas seulement les tests automatisés) :
GET /api/queue/download-all répond bien 404 sur une file vide, bouton
correctement cablé et masqué côté client.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- QueueService.remove() remplace cancel() : supprime l'élément de la
liste quel que soit son état (annule d'abord s'il était en attente/en
cours), au lieu de se limiter aux deux cas actifs et renvoyer false
pour un élément déjà terminé.
- consumeCompletedFile() met fileName à null une fois le fichier livré
(et son dossier nettoyé) : évite qu'un second appel tente de relire un
fichier qui n'existe déjà plus.
- QueuePanel : un même bouton retire l'élément quel que soit son état -
"Annuler" en attente/en cours, "Supprimer" une fois terminé. Bouton
"Tout télécharger" dans l'en-tête, qui déclenche avec un léger décalage
chaque téléchargement encore disponible (évite le blocage navigateur
des téléchargements multiples simultanés).
- Liste de résultats de recherche de chaîne agrandie (320px -> 65vh).
11 nouveaux tests côté serveur (63 -> alignés). Vérifié en navigateur :
ajout, échec, suppression effective (l'élément disparaît de la liste).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cœur du pipeline : ajoute une file d'attente exécutant les téléchargements
yt-dlp avec une concurrence bornée, et livre le fichier produit au
navigateur (pas de dépôt durable côté serveur).
- downloadPlan.ts : construction des arguments yt-dlp par mode (vidéo avec
sous-titres/miniature en option, audio dans un format courant, miniature
seule), toujours avec séparateur `--` avant l'URL. Parsing de la
progression depuis stdout (`[download] NN.N%`).
- videoUrl.ts : valide qu'une URL cible bien youtube.com/youtu.be avant de
la passer à yt-dlp - ferme le risque de relais SSRF via l'extracteur
générique de yt-dlp (des centaines de sites supportés) si une URL
arbitraire était acceptée.
- QueueService.ts : file en mémoire, N téléchargements simultanés
(MAX_CONCURRENT_DOWNLOADS), annulation via AbortSignal, nettoyage du
dossier de sortie après échec/annulation ou après livraison du fichier.
- ytDlpRunner.ts : étendu pour le suivi ligne par ligne de stdout
(progression) et l'annulation (AbortSignal passé à child_process.spawn).
- routes/queue.ts : POST/GET /api/queue, DELETE /api/queue/:id,
GET /api/queue/:id/file (streaming + nettoyage après envoi).
45 nouveaux tests (59 au total) : fonctions pures, services (vrai système
de fichiers temporaire, yt-dlp mocké), routes. Vérifié aussi en conditions
réelles (serveur démarré localement) : validation d'URL, ajout en file,
et passage correct à l'état "failed" + nettoyage quand le binaire yt-dlp
est absent (ENOENT).
Empilée sur feature/channel-discovery (réutilise ytDlpRunner.ts).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>