# 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) ```prisma 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) ```typescript 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.