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>
34 lines
1.2 KiB
TypeScript
34 lines
1.2 KiB
TypeScript
/**
|
|
* Extrait le dossier de sortie passé à yt-dlp via `-o` (voir downloadPlan.ts).
|
|
*
|
|
* @param args - Arguments passés au mock de `runYtDlp`.
|
|
* @returns Le dossier de sortie déduit du gabarit `-o`.
|
|
*/
|
|
export function extractOutputDir(args: readonly string[]): string {
|
|
const oIndex = args.indexOf('-o');
|
|
const template = args[oIndex + 1] ?? '';
|
|
const withoutPrefix = template.replace(/^thumbnail:/, '');
|
|
return withoutPrefix.slice(0, withoutPrefix.lastIndexOf('/'));
|
|
}
|
|
|
|
/**
|
|
* Attend qu'une condition devienne vraie, en pollant (pas de timers
|
|
* factices : vraie I/O fichier/réseau en jeu dans les tests qui l'utilisent).
|
|
* Accepte un prédicat synchrone ou asynchrone.
|
|
*
|
|
* @param predicate - Condition à satisfaire.
|
|
* @param timeoutMs - Délai maximal avant d'abandonner.
|
|
* @throws {Error} Si le délai est dépassé sans que la condition ne devienne vraie.
|
|
*/
|
|
export async function waitFor(
|
|
predicate: () => boolean | Promise<boolean>,
|
|
timeoutMs = 2000,
|
|
): Promise<void> {
|
|
const start = Date.now();
|
|
while (!(await predicate())) {
|
|
if (Date.now() - start > timeoutMs) {
|
|
throw new Error('waitFor: timeout');
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
}
|
|
}
|