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>
57 lines
1.8 KiB
TypeScript
57 lines
1.8 KiB
TypeScript
import { afterEach, describe, expect, it } from 'vitest';
|
|
import { YtDlpError, runYtDlp } from '../src/services/ytDlpRunner.js';
|
|
|
|
describe('runYtDlp', () => {
|
|
afterEach(() => {
|
|
// biome-ignore lint/performance/noDelete: une assignation à `undefined` stockerait la chaîne "undefined" dans process.env.
|
|
delete process.env.YTDLP_PATH;
|
|
});
|
|
|
|
it('retourne stdout quand le processus réussit', async () => {
|
|
process.env.YTDLP_PATH = process.execPath;
|
|
|
|
const stdout = await runYtDlp(['-e', "console.log('bonjour')"]);
|
|
|
|
expect(stdout.trim()).toBe('bonjour');
|
|
});
|
|
|
|
it('lève YtDlpError quand le processus se termine en échec', async () => {
|
|
process.env.YTDLP_PATH = process.execPath;
|
|
|
|
await expect(runYtDlp(['-e', "console.error('boom'); process.exit(3)"])).rejects.toBeInstanceOf(
|
|
YtDlpError,
|
|
);
|
|
});
|
|
|
|
it('appelle onStdoutLine pour chaque ligne, sans attendre la fin du processus', async () => {
|
|
process.env.YTDLP_PATH = process.execPath;
|
|
const lines: string[] = [];
|
|
|
|
await runYtDlp(['-e', "console.log('a'); console.log('b'); console.log('c')"], {
|
|
onStdoutLine: (line) => lines.push(line),
|
|
});
|
|
|
|
expect(lines).toEqual(['a', 'b', 'c']);
|
|
});
|
|
|
|
it('émet le fragment final même sans saut de ligne terminal', async () => {
|
|
process.env.YTDLP_PATH = process.execPath;
|
|
const lines: string[] = [];
|
|
|
|
await runYtDlp(['-e', "process.stdout.write('sans-newline')"], {
|
|
onStdoutLine: (line) => lines.push(line),
|
|
});
|
|
|
|
expect(lines).toEqual(['sans-newline']);
|
|
});
|
|
|
|
it("rejette avec une AbortError quand le signal d'annulation se déclenche", async () => {
|
|
process.env.YTDLP_PATH = process.execPath;
|
|
const controller = new AbortController();
|
|
|
|
const promise = runYtDlp(['-e', 'setTimeout(() => {}, 5000)'], { signal: controller.signal });
|
|
controller.abort();
|
|
|
|
await expect(promise).rejects.toMatchObject({ name: 'AbortError' });
|
|
});
|
|
});
|