youtubeDownloader/server/src/index.ts
Nicolas 63ef42cc20 feat(server): file d'attente + téléchargement (vidéo/audio/miniature)
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>
2026-09-16 09:02:37 +02:00

83 lines
2.4 KiB
TypeScript

import fastifyStatic from '@fastify/static';
import Fastify, { type FastifyInstance } from 'fastify';
import { type ServerConfig, loadConfig } from './config.js';
import { channelRoutes } from './routes/channel.js';
import { healthRoutes } from './routes/health.js';
import { createQueueRoutes } from './routes/queue.js';
/**
* Construit et configure l'instance Fastify de l'application : fichiers
* statiques du client, et routes applicatives.
*
* @param config - Configuration runtime résolue depuis l'environnement.
* @returns L'instance Fastify prête à être démarrée.
*/
async function buildServer(config: ServerConfig): Promise<FastifyInstance> {
const app = Fastify({ logger: true });
try {
await app.register(fastifyStatic, {
root: config.clientDistPath,
index: ['index.html'],
});
await app.register(healthRoutes);
await app.register(channelRoutes);
await app.register(createQueueRoutes(config));
} catch (error) {
app.log.error({ err: error }, "Échec de l'enregistrement des plugins Fastify");
throw error;
}
return app;
}
/**
* Installe les gestionnaires d'arrêt propre du serveur (SIGINT/SIGTERM).
*
* @param app - Instance Fastify à fermer proprement à l'arrêt.
*/
function installShutdownHandlers(app: FastifyInstance): void {
const shutdown = async (signal: string): Promise<void> => {
app.log.info(`Signal ${signal} reçu, arrêt du serveur...`);
try {
await app.close();
process.exit(0);
} catch (error) {
app.log.error({ err: error }, "Échec de l'arrêt propre du serveur");
process.exit(1);
}
};
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
process.on(signal, () => {
shutdown(signal).catch((error: unknown) => {
console.error(`Échec du gestionnaire ${signal}`, error);
process.exit(1);
});
});
}
}
/**
* Point d'entrée du serveur : construit l'application, la démarre, et
* installe les gestionnaires d'arrêt propre.
*/
async function main(): Promise<void> {
const config = loadConfig();
const app = await buildServer(config);
try {
await app.listen({ port: config.port, host: config.host });
app.log.info(`Serveur démarré sur http://${config.host}:${config.port}`);
} catch (error) {
app.log.error({ err: error }, 'Échec du démarrage du serveur');
process.exit(1);
}
installShutdownHandlers(app);
}
main().catch((error: unknown) => {
console.error('Erreur fatale au démarrage', error);
process.exit(1);
});