Files
video-downloader/docs/superpowers/specs/2026-08-10-ombrora-ytdlp-design.md

7.2 KiB

Ombrora-YTDLP — Design Spec

Date: 2026-08-10

Vue d'ensemble

Interface web publique permettant a n'importe quel utilisateur de soumettre une URL de video a telecharger via yt-dlp. Les telechargements sont traites en arriere-plan par un worker Node.js gere par Passenger sur o2switch. Les fichiers sont stockes localement dans un dossier prive et accessibles via un lien temporaire a duree de vie configurable.


Architecture generale

Approche retenue : Monorepo Next.js + Worker script Node.js separe.

  • src/ : app Next.js (UI React + API routes)
  • worker/ : script Node.js long-running qui poll la DB et execute yt-dlp
  • config/ : configuration centralisee (TTL, concurrence, chemins)
  • prisma/ : schema et migrations
  • docker-compose.yml : MariaDB pour le developpement local
  • Un seul package.json a la racine (contrainte o2switch/Passenger)

Structure du projet

Ombrora-YTDLP/
├── src/
│   ├── app/
│   │   ├── page.tsx                  # Formulaire de soumission
│   │   ├── status/[uuid]/page.tsx    # Page de statut
│   │   └── api/
│   │       ├── downloads/route.ts    # POST soumission, GET statut
│   │       ├── downloads/[uuid]/route.ts
│   │       └── download/[token]/route.ts  # Stream fichier via token
│   ├── components/
│   └── lib/
│       ├── prisma.ts
│       ├── rate-limit.ts
│       └── token.ts
├── worker/
│   ├── index.ts          # Worker long-running principal
│   ├── cron-check.ts     # Verifie que le worker tourne (cron 5 min)
│   └── cron-cleanup.ts   # Supprime les fichiers expires (cron 1x/jour)
├── config/
│   └── app.config.ts
├── prisma/
│   └── schema.prisma
├── docker-compose.yml
└── package.json

Schema base de donnees (Prisma / MariaDB)

model Download {
  id          String   @id @default(cuid())
  uuid        String   @unique @default(uuid())  // expose sur les interfaces
  url         String
  status      Status   @default(PENDING)
  format      String                              // ex: "mp4", "mp3"
  quality     String                              // ex: "best", "1080p"
  subtitles   Boolean  @default(false)
  extraArgs   String?                             // options yt-dlp (JSON array de strings, ex: ["--sponsorblock-remove","all"])

  filePath    String?                             // chemin absolu sur disque
  fileName    String?                             // nom original
  fileSize    BigInt?                             // taille en octets
  errorMsg    String?

  ipAddress   String
  submittedAt DateTime @default(now())
  startedAt   DateTime?
  completedAt DateTime?
  deletedAt   DateTime?

  tokens      DownloadToken[]
}

model DownloadToken {
  id         String   @id @default(cuid())
  download   Download @relation(fields: [downloadId], references: [id])
  downloadId String
  token      String   @unique @default(uuid())
  expiresAt  DateTime
  usedAt     DateTime?
  createdAt  DateTime @default(now())
}

enum Status {
  PENDING       // en attente de traitement
  PROCESSING    // worker en cours
  DONE          // fichier disponible
  FAILED        // echec yt-dlp
  FILE_DELETED  // metadonnees conservees, fichier supprime du disque
}

Regles immuables :

  • Aucune entree n'est jamais supprimee de la DB (historique complet).
  • id (cuid) usage interne uniquement ; uuid expose sur toutes les interfaces.
  • Le fichier est stocke sous {STORAGE_PATH}/{uuid}.{extension}.

Worker (worker/index.ts)

Processus Node.js long-running sous Passenger. Cycle a chaque iteration :

  1. Requete DB : SELECT les PENDING ordonnes par submittedAt, limite a WORKER_CONCURRENCY.
  2. Passe les entrees selectionnees en PROCESSING (atomique : UPDATE WHERE status=PENDING).
  3. Spawne un worker_thread par telechargement.
  4. Chaque thread construit la commande yt-dlp :
    • --no-playlist toujours present
    • -f {format} pour le format
    • --write-sub --sub-lang fr,en si subtitles=true
    • -o {STORAGE_PATH}/{uuid}.%(ext)s pour le chemin de sortie
    • extraArgs injectes depuis le champ JSON
  5. Fin du thread :
    • Succes : status=DONE, remplit filePath, fileName, fileSize, completedAt, cree un DownloadToken avec expiresAt = now + DOWNLOAD_LINK_TTL_HOURS.
    • Echec : status=FAILED, remplit errorMsg (stderr yt-dlp).
  6. Attend WORKER_POLL_INTERVAL_MS puis recommence.

Crons o2switch (cPanel)

Script Frequence Role
worker/cron-check.ts Toutes les 5 min Relance le worker s'il n'est pas actif
worker/cron-cleanup.ts 1x par jour Supprime les fichiers dont completedAt + TTL < now, passe status=FILE_DELETED, remplit deletedAt

API Routes (Next.js)

Methode Route Description
POST /api/downloads Valide IP (rate limit), cree l'entree DB, retourne { uuid }
GET /api/downloads/[uuid] Retourne statut et metadonnees du telechargement
GET /api/download/[token] Verifie token (existence, expiration), stream le fichier, marque usedAt

Rate limiting (lib/rate-limit.ts) :

  • Map en memoire : IP -> { count, resetAt }
  • Limite et fenetre configurables dans app.config.ts
  • Pas de Redis requis
  • Hypothese : Passenger tourne avec 1 seul processus Next.js sur o2switch (standard). Si plusieurs processus sont configures, la Map n'est pas partagee et le rate limit serait par-processus — a surveiller.

Configuration (config/app.config.ts)

export const config = {
  DOWNLOAD_LINK_TTL_HOURS: 24,       // duree de validite du lien temporaire
  WORKER_CONCURRENCY: 3,             // telechargements en parallele
  WORKER_POLL_INTERVAL_MS: 10_000,   // intervalle de poll de la DB
  STORAGE_PATH: '/home/.../storage', // chemin absolu du dossier de stockage
  RATE_LIMIT_MAX: 5,                 // soumissions max par IP
  RATE_LIMIT_WINDOW_MS: 3_600_000,   // fenetre de rate limit (1h)
}

Frontend (React / Next.js)

/ — Formulaire de soumission

  • Champ URL
  • Select format (mp4, mp3, webm, ...)
  • Select qualite (best, 1080p, 720p, 480p, ...)
  • Checkbox sous-titres
  • Champ options avancees (optionnel, libre)
  • Soumission -> POST /api/downloads -> redirect vers /status/{uuid}

/status/[uuid] — Page de statut

  • Polling GET /api/downloads/[uuid] toutes les 5 secondes
  • Affichage selon statut :
    • PENDING / PROCESSING : indicateur de progression
    • DONE : bouton de telechargement (lien /api/download/{token})
    • FAILED : message d'erreur yt-dlp
    • FILE_DELETED : message "fichier expire, le telechargement n'est plus disponible"

Contraintes de deploiement o2switch

  • Un seul package.json / node_modules a la racine (pas de frontend/node_modules).
  • Toute dependance runtime du frontend doit etre dans les dependencies de la racine ET dans frontend/package.json (si sous-dossier).
  • yt-dlp doit etre installe comme binaire precompile (pas de build depuis les sources).
  • Les dependances avec bindings natifs doivent publier des binaires precompiles pour la plateforme o2switch.