diff --git a/CLAUDE.md b/CLAUDE.md
index 42e3d4f..2077e18 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -10,23 +10,22 @@
- Utilise Docker & Docker-Compose pour la base de données (MariaDB)
- On développe tout en NodeJS notamment avec : NextJS & Prisma pour la partie backend et React pour la partie frontend
- Utilise Tailwind pour le CSS
-- L'application est toujours multilangue (EN + FR)
+- Si tu dois utiliser des Workers, gère le avec Passenger (pm2)
+- L'application est toujours multilangue (EN, FR, ES, IT — voir `messages/`)
## Application : Ombrora-YTDLP
-- Interface web publique permettant de soumettre des URLs de videos a telecharger via yt-dlp.
-- Les telechargements sont geres via une file d'attente (queue) executee par Passenger en arriere-plan avec support du multithreading.
+- Interface web publique permettant de soumettre des URLs de videos a telecharger via yt-dlp, avec pages SEO par plateforme (`/[locale]/[slug]`, ex. `/youtube-downloader`, definies dans `src/lib/downloader-platforms.ts`) et une page `supported-sites` listant tous les sites geres par yt-dlp.
+- Avant soumission, l'URL est analysee en temps reel via un probe yt-dlp (`POST /api/probe`, `src/lib/ytdlp-probe.ts`, `yt-dlp -J`) : les options presentees a l'utilisateur (qualite, sous-titres disponibles, decoupe, qualite audio MP3) sont derivees de cette analyse et non d'une liste statique ; la soumission est bloquee tant que le probe n'a pas reussi.
+- Les telechargements sont geres via une file d'attente (queue) executee par un worker Node.js (`worker/`, lance via `tsx`) supervise par pm2, avec support du multithreading (concurrence configurable via `WORKER_CONCURRENCY`).
- Chaque telechargement est stocke en base de donnees (MariaDB via Prisma). **Aucune entree n'est jamais supprimee** : la DB conserve l'historique complet de tous les telechargements (statuts, erreurs, metadata).
-- Le worker Passenger est responsable de dequeuer et d'executer yt-dlp en parallele selon la capacite configuree.
+- `yt-dlp` est un zipapp Python invoque differemment selon la plateforme (`src/lib/ytdlp.ts`) : via `python` sur Windows (le shebang n'est pas executable par `spawn()`), directement sur Linux/o2switch (shebang natif).
## Deployment (o2switch)
-- o2switch is shared hosting: no compiler toolchain, no root access. Any dependency with a native/binary component must ship as a precompiled binary — it cannot be built from source on the server.
+- o2switch is shared hosting: no compiler toolchain, no root access. Any dependency with a native/binary component must ship as a precompiled binary — it cannot be built from source on the server. This is why `yt-dlp`/`ffmpeg`/`ffprobe` ship as precompiled binaries in `bin/` (or `BIN_DIR`) rather than as npm deps.
- Before adding any new dependency with native bindings, confirm it publishes prebuilt binaries for o2switch's platform/arch — otherwise it will fail to install or run there.
-- **The o2switch nodevenv/Passenger setup ("Setup Node.js App" in cPanel) only supports a single `package.json`/`node_modules` for the whole registered app — not one per subfolder.** Verified by directly debugging a failed `frontend/` build: `npm install --prefix frontend --include=dev` (root's own `build` script) and even a plain `npm install` run with `cd frontend` first (confirmed via `pwd` to genuinely be inside `frontend/`) both completed "successfully" (correct, unmodified `frontend/package-lock.json`, real `resolved` entries for every package) yet never created a `frontend/node_modules` directory on the server at all. Meanwhile `vite`/`@vitejs/plugin-react` (already present as root devDependencies) resolved fine during the build — only packages that exist *exclusively* in `frontend/package.json` (`react-router-dom`, `react-i18next`, `i18next`, `@phosphor-icons/react`) failed to resolve, with Vite/Rolldown erroring `Rolldown failed to resolve import "react-router-dom"`.
- - **Fix (applied):** every runtime package `frontend/src/**` imports must also be listed in the **root** `package.json`'s `dependencies` (not just `frontend/package.json`'s) — `react`/`react-dom` already were; `react-router-dom`, `react-i18next`, `i18next`, `@phosphor-icons/react` were added there too. Root's single `node_modules` is an ancestor directory of `frontend/src/`, so Node/Vite's normal upward `node_modules` resolution walk finds them there even with no `frontend/node_modules` on the server.
- - `frontend/package.json` still declares the same packages in its own `dependencies` — that's intentional, not stale duplication. It's what makes local dev (`npm run dev` inside `frontend/`, which gets a real, normal `frontend/node_modules` on a dev machine) work independently of this server-only constraint. When adding a new frontend runtime dependency, add it to **both** `package.json` files (frontend's own, for local dev; root's, for the o2switch build) and run `npm install` in both places to keep both lockfiles in sync.
- - `frontend/package.json`'s `devDependencies` (`vite`, `@vitejs/plugin-react`, `oxlint`, `@types/react*`) do **not** need mirroring to root — only the ones already there (`vite`, `@vitejs/plugin-react`) are actually required for the production build to run at all; `oxlint`/`@types/*` are dev-only tooling never invoked during `npm run build`.
+- Single Next.js app with a single `package.json`/`node_modules` at the repo root — there is no separate frontend build or subfolder to keep in sync. Deployed via cPanel "Setup Node.js App" (Passenger) for the web app; the worker is a separate process that Passenger does not supervise, kept alive by pm2 instead (`ecosystem.config.cjs`, `npm run worker:pm2:*`). Full deployment steps (including `npm run deploy` / `scripts/deploy.sh`) are documented in `README.md`.
diff --git a/README.md b/README.md
index 1de697f..8f27fec 100644
--- a/README.md
+++ b/README.md
@@ -4,12 +4,13 @@ Interface web de téléchargement de vidéos via [yt-dlp](https://github.com/yt-
## Fonctionnement
-1. L'utilisateur soumet une URL depuis l'interface web.
-2. La demande est enregistrée en base (statut `PENDING`).
-3. Le worker (Node.js, arrière-plan, supervisé par pm2) dépile les jobs et exécute `yt-dlp` en parallèle (concurrence configurable).
-4. Le fichier téléchargé est stocké localement ; un token signé (24h) est généré.
-5. L'utilisateur est redirigé vers une page de statut qui se rafraîchit jusqu'à ce que le fichier soit prêt.
-6. Un cron quotidien supprime les fichiers expirés et marque les entrées `FILE_DELETED` — la DB conserve l'historique complet.
+1. L'utilisateur colle une URL dans l'interface web. Elle est immédiatement analysée via un probe `yt-dlp -J` (`POST /api/probe`), qui détermine les qualités, sous-titres et durée réellement disponibles pour cette vidéo.
+2. Les options de téléchargement (format, qualité, sous-titres, découpe, qualité audio MP3) affichées à l'utilisateur sont dérivées de ce probe — la soumission est bloquée tant qu'il n'a pas réussi.
+3. La demande est enregistrée en base (statut `PENDING`).
+4. Le worker (Node.js, arrière-plan, supervisé par pm2) dépile les jobs et exécute `yt-dlp` en parallèle (concurrence configurable).
+5. Le fichier téléchargé est stocké localement ; un token signé (24h) est généré.
+6. L'utilisateur est redirigé vers une page de statut qui se rafraîchit jusqu'à ce que le fichier soit prêt.
+7. Un cron quotidien supprime les fichiers expirés et marque les entrées `FILE_DELETED` — la DB conserve l'historique complet.
## Stack technique
@@ -17,7 +18,10 @@ Interface web de téléchargement de vidéos via [yt-dlp](https://github.com/yt-
|--------|-------------|
| Framework | Next.js 16 (App Router) |
| Base de données | MariaDB 11 via Prisma 7 |
-| Frontend | React 19, TypeScript |
+| Frontend | React 19, TypeScript, Tailwind CSS v4 |
+| i18n | next-intl (EN, FR, ES, IT), routing par `[locale]` |
+| Thème | next-themes (clair/sombre) |
+| Analytics | PostHog (cookieless) |
| Worker | Node.js (tsx), supervisé par pm2 |
| Conteneur (dev) | Docker Compose |
@@ -25,7 +29,7 @@ Interface web de téléchargement de vidéos via [yt-dlp](https://github.com/yt-
- Node.js 20+
- Docker & Docker Compose (pour MariaDB en développement)
-- `yt-dlp` et `ffmpeg` (binaires précompilés dans `./bin/` ou dans le `PATH`)
+- `yt-dlp`, `ffmpeg` et `ffprobe` (binaires précompilés dans `./bin/` ou dans le `PATH`)
## Démarrage rapide
@@ -60,7 +64,8 @@ L'interface est accessible sur [http://localhost:3000](http://localhost:3000).
|----------|--------|---------|-------------|
| `DATABASE_URL` | oui | `mysql://ombrora:ombrora@localhost:3306/ombrora` | Connexion MariaDB |
| `STORAGE_PATH` | oui | `/var/www/ombrora/storage` | Répertoire de stockage des fichiers téléchargés |
-| `BIN_DIR` | non | `./bin` | Répertoire contenant `yt-dlp` et `ffmpeg` (défaut : `./bin`) |
+| `NEXT_PUBLIC_BASE_URL` | oui | `https://example.com` | URL publique du site, utilisée pour le SEO (canonical, OpenGraph, JSON-LD, sitemap) |
+| `BIN_DIR` | non | `./bin` | Répertoire contenant `yt-dlp`, `ffmpeg` et `ffprobe` (défaut : `./bin`) — absent de `.env.example`, à ajouter manuellement si le défaut ne convient pas |
## Scripts npm
@@ -87,18 +92,22 @@ L'interface est accessible sur [http://localhost:3000](http://localhost:3000).
Les paramètres applicatifs sont centralisés dans `config/app.config.ts` :
```typescript
-DOWNLOAD_LINK_TTL_HOURS: 24 // Durée de validité du token de téléchargement
-WORKER_CONCURRENCY: 3 // Téléchargements parallèles maximum
-WORKER_POLL_INTERVAL_MS: 10000 // Intervalle de polling du worker (ms)
-RATE_LIMIT_MAX: 5 // Soumissions max par IP par fenêtre
-RATE_LIMIT_WINDOW_MS: 3600000 // Fenêtre de rate-limit (1h)
+DOWNLOAD_LINK_TTL_HOURS: 24 // Durée de validité du token de téléchargement
+WORKER_CONCURRENCY: 3 // Téléchargements parallèles maximum
+WORKER_POLL_INTERVAL_MS: 10000 // Intervalle de polling du worker (ms)
+RATE_LIMIT_MAX: 5 // Soumissions max par IP par fenêtre
+RATE_LIMIT_WINDOW_MS: 3600000 // Fenêtre de rate-limit des soumissions (1h)
+PROBE_RATE_LIMIT_MAX: 20 // Analyses (probe) max par IP par fenêtre
+PROBE_RATE_LIMIT_WINDOW_MS: 3600000 // Fenêtre de rate-limit du probe (1h)
+PROBE_TIMEOUT_MS: 20000 // Timeout du probe yt-dlp -J (ms)
```
## API
| Méthode | Endpoint | Description |
|---------|----------|-------------|
-| `POST` | `/api/downloads` | Soumet une URL. Corps JSON : `{ url, format?, quality?, subtitles?, extraArgs? }`. Retourne `{ uuid }`. |
+| `POST` | `/api/probe` | Analyse une URL via `yt-dlp -J`. Corps JSON : `{ url }`. Retourne les qualités, sous-titres et durée réellement disponibles pour la vidéo, ou une erreur (`422` si l'analyse échoue, `429` si rate-limit dépassé). |
+| `POST` | `/api/downloads` | Soumet une URL. Corps JSON : `{ url, format, quality, subtitles, subtitleLangs?, clipStart?, clipEnd?, audioQuality?, extraArgs? }`. Retourne `{ uuid }`. |
| `GET` | `/api/downloads/:uuid` | Statut d'un téléchargement. Retourne `{ status, fileName, fileSize, downloadToken?, tokenExpiresAt?, errorMsg? }`. |
| `GET` | `/api/download/:token` | Téléchargement du fichier (token à usage unique, 24h). |
@@ -107,26 +116,45 @@ RATE_LIMIT_WINDOW_MS: 3600000 // Fenêtre de rate-limit (1h)
```
src/
├── app/
-│ ├── page.tsx # Page d'accueil (formulaire de soumission)
-│ ├── status/[uuid]/page.tsx # Page de suivi
+│ ├── [locale]/
+│ │ ├── layout.tsx # Header, thème, next-intl, JSON-LD, PostHog
+│ │ ├── page.tsx # Accueil (hero, formulaire, sections SEO)
+│ │ ├── [slug]/page.tsx # Pages SEO par plateforme (youtube-downloader, etc.)
+│ │ ├── status/[uuid]/page.tsx # Page de suivi
+│ │ └── supported-sites/page.tsx # Liste des sites supportés par yt-dlp
+│ ├── sitemap.ts # Génération du sitemap.xml
│ └── api/
-│ ├── downloads/route.ts # POST — création
+│ ├── downloads/route.ts # POST — création
│ ├── downloads/[uuid]/route.ts # GET — statut
-│ └── download/[token]/route.ts # GET — téléchargement
+│ ├── download/[token]/route.ts # GET — téléchargement
+│ └── probe/route.ts # POST — analyse yt-dlp -J
├── components/
-│ ├── SubmitForm.tsx # Formulaire de soumission
-│ └── StatusView.tsx # Vue de suivi (polling)
+│ ├── SubmitForm.tsx # Formulaire de soumission (options dérivées du probe)
+│ ├── StatusView.tsx # Vue de suivi (polling)
+│ ├── Header.tsx, LanguageSwitcher.tsx, ThemeToggle.tsx
+│ ├── HowItWorksSection.tsx, ReassuranceSection.tsx, SupportedPlatformsSection.tsx
+│ └── FaqSection.tsx, FaqAccordion.tsx, SupportedSitesBrowser.tsx
+├── i18n/
+│ ├── routing.ts # Locales, préfixes d'URL
+│ └── request.ts # Config next-intl côté serveur
+├── proxy.ts # Middleware next-intl (détection/routage locale)
└── lib/
- ├── prisma.ts # Client Prisma (singleton)
- ├── rate-limit.ts # Rate limiting par IP
- ├── token.ts # Génération/validation de tokens
- └── ytdlp.ts # Construction des arguments yt-dlp
+ ├── prisma.ts # Client Prisma (singleton)
+ ├── rate-limit.ts # Factory createRateLimiter (downloads + probe)
+ ├── token.ts # Génération/validation de tokens
+ ├── ytdlp.ts # Construction des arguments/commande yt-dlp
+ ├── ytdlp-options.ts # Formats/qualités/langues proposés
+ ├── ytdlp-probe.ts # Analyse yt-dlp -J d'une URL
+ ├── downloader-platforms.ts # Données des pages SEO par plateforme
+ └── supported-sites.ts # Liste des sites supportés par yt-dlp
worker/
├── index.ts # Boucle de polling principale
├── processor.ts # Exécution de yt-dlp, mise à jour DB
└── cron-cleanup.ts # Nettoyage des fichiers expirés
+messages/ # Traductions (en.json, fr.json, es.json, it.json)
+
ecosystem.config.cjs # Config pm2 du process "ombrora-worker"
prisma/
@@ -137,8 +165,8 @@ prisma/
### Contraintes spécifiques à l'hébergement mutualisé
-- **Pas de compilation sur le serveur** : `yt-dlp` et `ffmpeg` doivent être des binaires précompilés pour la plateforme cible, placés dans `BIN_DIR`.
-- **`node_modules` unique** : Passenger ne crée qu'un seul `node_modules` à la racine. Toute dépendance runtime importée par le frontend doit figurer dans le `dependencies` du `package.json` racine (en plus de `frontend/package.json`).
+- **Pas de compilation sur le serveur** : `yt-dlp`, `ffmpeg` et `ffprobe` doivent être des binaires précompilés pour la plateforme cible, placés dans `BIN_DIR`.
+- **`node_modules` unique** : Passenger ne crée qu'un seul `node_modules` à la racine. Le projet n'a qu'un seul `package.json` (pas de sous-dossier frontend séparé à synchroniser).
- **Worker** : Passenger ne supervise que l'app Next.js. Le worker est un process à part, maintenu vivant par **pm2** (auto-restart en cas de crash), à la manière de Passenger pour l'app web.
### Étapes de déploiement
diff --git a/src/app/page.tsx b/src/app/page.tsx
deleted file mode 100644
index 12a0357..0000000
--- a/src/app/page.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { SubmitForm } from '@/components/SubmitForm'
-
-export default function Home() {
- return (
-
- Ombrora
-
-
- )
-}
diff --git a/src/app/status/[uuid]/page.tsx b/src/app/status/[uuid]/page.tsx
deleted file mode 100644
index 92287cb..0000000
--- a/src/app/status/[uuid]/page.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { StatusView } from '@/components/StatusView'
-
-export default async function StatusPage({
- params,
-}: {
- params: Promise<{ uuid: string }>
-}) {
- const { uuid } = await params
- return (
-
- Statut du telechargement
-
-
- )
-}