youtubeDownloader/server/src/index.ts
Nicolas f0dec86b19
Some checks failed
Publier l'image Docker / test (pull_request) Failing after 4s
Publier l'image Docker / publish (pull_request) Has been skipped
feat: playlists YouTube (parcourir, ajouter une par une ou d'un coup)
Coller un lien de playlist (youtube.com/playlist?list=...) dans le champ
URL affiche toutes ses videos dans la meme zone de resultats que la
recherche de chaine ; chaque ligne a son bouton "Ajouter", et un bouton
"Tout ajouter a la file" ajoute le reste d'un coup.

Serveur :
- GET /api/playlist (PlaylistDiscoveryService) : yt-dlp --flat-playlist
  -J. -J plutot que -j : le titre de la playlist n'existe qu'a la racine,
  et une playlist vide reste un resultat valide.
- playlistUrl.ts : n'accepte que les liens youtube.com/playlist?list=<id>
  et reconstruit l'URL canonique depuis le seul identifiant valide (rien
  d'autre de l'entree n'atteint yt-dlp). Un lien watch?v=...&list=...
  reste une video (--no-playlist etait deja passe au telechargement).
- videoSummary.ts : conversion d'une entree --flat-playlist en resume de
  video, extraite de ChannelDiscoveryService pour etre partagee (chaine
  et playlist parsent desormais identiquement). ChannelVideoSummary
  devient VideoSummary (et ChannelVideoDto -> VideoDto cote client).

Client :
- Le champ URL accepte video ou playlist (isPlaylistUrl aiguille) ; zone
  de resultats partagee (createResultsView).
- "Tout ajouter" : POST /api/queue sequentiels (ordre conserve, pas de
  doublon avec les lignes deja ajoutees). Un echec n'interrompt pas le
  lot : nombre d'echecs + premier message affiches, seules les lignes en
  echec redeviennent ajoutables. Le mode est lu une seule fois pour tout
  le lot.
- Miniatures de resultats en loading="lazy" (une playlist peut compter
  des centaines de videos).

Verifie contre le vrai yt-dlp (2026.07.04) : forme reelle de -J relevee
(fixtures de test calquees dessus), playlist de 1607 videos listee en
~11 s (delai fixe a 120 s). Teste dans le navigateur reel : playlist de
13 videos, ajout unitaire, "tout ajouter" (13 elements, aucun doublon),
echec partiel simule + reprise, playlist inexistante, lien video avec
list=, recherche de chaine (non-regression).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 14:55:38 +02:00

85 lines
2.5 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 { playlistRoutes } from './routes/playlist.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(playlistRoutes);
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);
});