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>
31 lines
1.1 KiB
TypeScript
31 lines
1.1 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { InvalidVideoUrlError, assertValidVideoUrl } from '../src/services/videoUrl.js';
|
|
|
|
describe('assertValidVideoUrl', () => {
|
|
it.each([
|
|
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
|
'https://youtube.com/watch?v=dQw4w9WgXcQ',
|
|
'https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=30s',
|
|
'https://www.youtube.com/shorts/dQw4w9WgXcQ',
|
|
'https://www.youtube.com/live/dQw4w9WgXcQ',
|
|
'https://youtu.be/dQw4w9WgXcQ',
|
|
'https://youtu.be/dQw4w9WgXcQ?t=30',
|
|
])('accepte "%s"', (url) => {
|
|
expect(assertValidVideoUrl(url)).toBe(url);
|
|
});
|
|
|
|
it.each([
|
|
'',
|
|
'not a url',
|
|
'ftp://youtube.com/watch?v=dQw4w9WgXcQ',
|
|
'https://evil.com/watch?v=dQw4w9WgXcQ',
|
|
'https://youtube.com.evil.com/watch?v=dQw4w9WgXcQ',
|
|
'https://www.youtube.com/watch?v=short',
|
|
'https://www.youtube.com/@somechannel',
|
|
'https://www.youtube.com/watch?v=dQw4w9WgXcQ; rm -rf /',
|
|
'--exec=touch /tmp/pwned https://www.youtube.com/watch?v=dQw4w9WgXcQ',
|
|
'file:///etc/passwd',
|
|
])('rejette "%s"', (url) => {
|
|
expect(() => assertValidVideoUrl(url)).toThrow(InvalidVideoUrlError);
|
|
});
|
|
});
|