youtubeDownloader/spec/architecture.md
Nicolas 0e78b44a55
Some checks failed
Publier l'image Docker / test (pull_request) Failing after 4s
Publier l'image Docker / publish (pull_request) Has been skipped
feat(server): service de découverte de chaîne (GET /api/channel)
Première brique du pipeline : lister les vidéos/streams d'une chaîne
YouTube (yt-dlp --flat-playlist), filtrables par mots-clés sur le titre.

- ytDlpRunner.ts : exécution de yt-dlp via child_process.spawn (jamais de
  shell, arguments en tableau - exclut l'injection de commande)
- ChannelDiscoveryService.ts : validation stricte du handle/URL de chaîne
  + séparateur `--` avant l'URL (défense en profondeur contre l'injection
  d'options yt-dlp), parsing tolérant des lignes JSON, filtrage mots-clés
- routes/channel.ts : GET /api/channel (validation de schéma Fastify,
  400 si entrée invalide, 502 si yt-dlp échoue)
- 14 tests (unitaires services + intégration route via app.inject) - un
  test de route a révélé une regex de validation trop permissive qu'un
  test de service avait manquée, corrigée avant ce commit

La file d'attente et le téléchargement suivront dans une PR séparée.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-16 08:05:29 +02:00

5.1 KiB

Architecture

Contexte

Remplace une série de scripts .cmd (./downloader, non versionné) par une application web : parcourir une chaîne YouTube, mettre des vidéos/audios en file d'attente, les télécharger en parallèle avec suivi en temps réel, et les livrer au navigateur de l'utilisateur. Pas d'intégration Emby, pas d'authentification (usage LAN).

Structure du dépôt

server/   Backend Fastify (TypeScript, ESM)
client/   Frontend TypeScript vanilla (esbuild + sass)
.forgejo/workflows/   CI : lint + typecheck + tests + build, puis publication Docker

Pas de docker-compose.yml pour l'instant (une seule image, pas encore de dépendances externes type base de données).

Backend (server/)

  • Fastify 5, TypeScript strict, ESM ("type": "module", imports relatifs en .js, résolution NodeNext).
  • src/index.ts : bootstrap (plugins, écoute, arrêt propre SIGINT/SIGTERM).
  • src/config.ts : configuration runtime depuis l'environnement (PORT, HOST, CLIENT_DIST_PATH).
  • src/routes/ : routes Fastify enregistrées comme plugins (health.ts, channel.ts). Chaque domaine métier (découverte de chaîne, file d'attente, téléchargement, notifications temps réel à venir) suit le même principe : un plugin de route fin sous src/routes/, qui délègue à un service sous src/services/.
  • src/services/ :
    • ytDlpRunner.ts : exécution bas niveau de yt-dlp (node:child_process.spawn, jamais de shell - arguments toujours passés en tableau, jamais concaténés dans une chaîne, pour exclure toute injection de commande).
    • ChannelDiscoveryService.ts : liste les vidéos/streams d'une chaîne (yt-dlp --flat-playlist -j), filtre par mots-clés sur le titre. Valide strictement le handle/URL de chaîne en entrée (premier caractère forcément alphanumérique) et ajoute un séparateur -- avant l'URL passée à yt-dlp, en défense en profondeur contre l'injection d'options.
  • Le serveur sert aussi les fichiers statiques de client/dist via @fastify/static.
  • Tests : Vitest (server/test/). Deux niveaux : services testés directement (avec ytDlpRunner mocké pour ChannelDiscoveryService - seul ytDlpRunner lui-même spawn un vrai process, node en test), et routes testées via app.inject() (schéma de validation Fastify, mapping des erreurs en codes HTTP). Les deux niveaux comptent : un test de service avec un exemple d'entrée mal choisi avait laissé passer une regex de validation trop permissive (--exec accepté), repérée seulement par le test de route avec un autre exemple.

Frontend (client/)

  • TypeScript vanilla, pas de framework. Chaque vue = un dossier avec Vue.ts / Vue.html / Vue.scss (gabarit importé comme chaîne brute, style Sass importé et compilé). Voir src/shell/ pour l'exemple actuel (placeholder).
  • Build via build.mjs (esbuild), pas d'outil de build plus lourd (Vite, webpack) : le script compile src/app.ts en bundle ESM, copie src/index.html, et compile/regroupe les .scss importés en un app.css voisin (plugin esbuild maison qui délègue au paquet sass).
  • Pas encore de router : une seule vue (shell) pour l'instant. Un routeur sera introduit avec la première vraie vue (parcours de chaîne).

API

  • GET /health : statut du service (utilisé par le HEALTHCHECK Docker).
  • GET /api/channel?channel=<@handle|URL>&section=<videos|streams>&keywords=<a,b> : liste les vidéos/streams d'une chaîne, filtrés par mots-clés (ET, insensible à la casse) sur le titre. section par défaut videos. Réponses : 200 { videos: [...] }, 400 si channel est absent/invalide, 502 si yt-dlp échoue.

Lint

Biome.js à la racine (biome.json), applique aux deux paquets (server/src, client/src) - pas d'ESLint/Prettier. downloader/ (scripts legacy, non versionné) est explicitement exclu.

Docker / CI-CD

  • server/Dockerfile : image multi-étapes construite avec pour contexte la racine du dépôt (docker build -f server/Dockerfile .), car elle a besoin à la fois de server/ et client/. Étapes : build client (esbuild) → build serveur (tsc) → image runtime (Node 22 Alpine + ffmpeg + binaire autonome yt-dlp, utilisateur non-root node).
  • yt-dlp et ffmpeg sont installés dans l'image (pas vendorisés dans le dépôt, contrairement aux scripts .cmd d'origine qui embarquaient yt-dlp.exe et un build ffmpeg complet).
  • .forgejo/workflows/publish-docker.yml : job test (lint racine, typecheck/tests/build serveur, typecheck/build client) puis job publish (build + push de l'image vers git.dev.kyuno.fr/kyuno/youtubedownloader, seulement si test passe et hors PR). Tags : latest + sha court sur push main, X.Y.Z/X.Y/latest sur tag vX.Y.Z. Nécessite le secret de dépôt REGISTRY_TOKEN (à créer manuellement, voir l'en-tête du workflow - un jeton d'API n'a pas le droit d'écrire dans le registre de paquets).

Statut

  • Fait : infrastructure (serveur qui répond, client placeholder, Docker, CI), découverte de chaîne (GET /api/channel).
  • À venir (PR successives) : file d'attente, téléchargement yt-dlp (vidéo/audio/miniature), notifications temps réel, vues frontend (parcours de chaîne, file d'attente).