diff --git a/CLAUDE.md b/CLAUDE.md index 2077e18..cae5db8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,7 @@ - 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). -- `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). +- `yt-dlp` est le zipapp Python officiel (`bin/yt-dlp`), pas le binaire standalone PyInstaller : ce dernier s'auto-extrait dans `/tmp` et echoue avec "failed to map segment from shared object" quand `/tmp` est monte `noexec` (cas d'o2switch). Le zipapp est toujours invoque explicitement via `PYTHON_BIN` (`src/lib/ytdlp.ts`) plutot que via son shebang, car le `python3` systeme d'o2switch (3.6) est trop ancien pour yt-dlp (3.10+ requis) — sur o2switch, `PYTHON_BIN=/opt/alt/python311/bin/python3`. ## Deployment (o2switch) diff --git a/README.md b/README.md index c65b60c..4bd692a 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ L'interface est accessible sur [http://localhost:3000](http://localhost:3000). | `STORAGE_PATH` | oui | `/var/www/ombrora/storage` | Répertoire de stockage des fichiers téléchargés | | `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 | +| `PYTHON_BIN` | non | `/opt/alt/python311/bin/python3` | Interpréteur utilisé pour exécuter le zipapp `bin/yt-dlp` (défaut : `python3` sur Linux, `python` sur Windows) — à définir si le `python3` système est trop ancien (yt-dlp nécessite 3.10+), comme sur o2switch | ## Scripts npm diff --git a/config/app.config.ts b/config/app.config.ts index a83e126..07f4dc0 100644 --- a/config/app.config.ts +++ b/config/app.config.ts @@ -11,4 +11,5 @@ export const config = { PROBE_RATE_LIMIT_WINDOW_MS: 3_600_000, PROBE_TIMEOUT_MS: 20_000, BIN_DIR: process.env.BIN_DIR ?? path.join(process.cwd(), 'bin'), + PYTHON_BIN: process.env.PYTHON_BIN ?? (process.platform === 'win32' ? 'python' : 'python3'), } as const diff --git a/package.json b/package.json index 3fb3721..042cf06 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ }, "scripts": { "dev": "next dev", - "build": "next build", + "build": "next build --webpack", "start": "node server.js", "test": "jest", "worker": "tsx worker/index.ts", diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 9a8c258..5155704 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -7,7 +7,7 @@ echo "==> Copie de .env.prod vers .env" cp .env.prod .env echo "==> Installation des dependances" -npm install +npm install --include=dev echo "==> Generation du client Prisma" npm run db:generate diff --git a/src/lib/__tests__/ytdlp-command.test.ts b/src/lib/__tests__/ytdlp-command.test.ts index fb35c69..fafec7f 100644 --- a/src/lib/__tests__/ytdlp-command.test.ts +++ b/src/lib/__tests__/ytdlp-command.test.ts @@ -1,47 +1,23 @@ const mockExistsSync = jest.fn() jest.mock('fs', () => ({ existsSync: (...args: unknown[]) => mockExistsSync(...args) })) +jest.mock('../../../config/app.config', () => ({ config: { BIN_DIR: '/bin', PYTHON_BIN: 'python3.11' } })) import { buildYtdlpCommand } from '../ytdlp' -function setPlatform(platform: NodeJS.Platform) { - Object.defineProperty(process, 'platform', { value: platform }) -} - describe('buildYtdlpCommand', () => { - const originalPlatform = process.platform - afterEach(() => { - setPlatform(originalPlatform) mockExistsSync.mockReset() }) - it('runs the bundled binary directly on Linux', () => { - setPlatform('linux') + it('runs the bundled zipapp through the configured Python interpreter', () => { mockExistsSync.mockReturnValue(true) const { command, args } = buildYtdlpCommand(['-J', 'https://x.test']) - expect(command).toContain('yt-dlp') - expect(args).toEqual(['-J', 'https://x.test']) - }) - - it('runs the bundled binary through python on Windows', () => { - setPlatform('win32') - mockExistsSync.mockReturnValue(true) - const { command, args } = buildYtdlpCommand(['-J', 'https://x.test']) - expect(command).toBe('python') + expect(command).toBe('python3.11') expect(args[0]).toContain('yt-dlp') expect(args.slice(1)).toEqual(['-J', 'https://x.test']) }) - it('runs a system-wide yt-dlp directly on Windows when no bundled binary exists', () => { - setPlatform('win32') - mockExistsSync.mockReturnValue(false) - const { command, args } = buildYtdlpCommand(['-J', 'https://x.test']) - expect(command).toBe('yt-dlp') - expect(args).toEqual(['-J', 'https://x.test']) - }) - - it('runs a system-wide yt-dlp directly on Linux when no bundled binary exists', () => { - setPlatform('linux') + it('runs a system-wide yt-dlp directly when no bundled binary exists', () => { mockExistsSync.mockReturnValue(false) const { command, args } = buildYtdlpCommand(['-J', 'https://x.test']) expect(command).toBe('yt-dlp') diff --git a/src/lib/ytdlp.ts b/src/lib/ytdlp.ts index 57c38bd..58f9f06 100644 --- a/src/lib/ytdlp.ts +++ b/src/lib/ytdlp.ts @@ -22,17 +22,20 @@ export function resolveYtdlpBin(): string { export type YtdlpCommand = { command: string; args: string[] } -// The bundled bin/yt-dlp is a Python zipapp relying on a `#!/usr/bin/env python3` -// shebang: Linux (o2switch prod) execs it natively, but Windows' spawn() has no -// shebang support and fails with ENOENT, so it must be run through `python` there. -// A system-wide `yt-dlp` (PATH fallback) already has a proper platform launcher -// on both OSes and is always run directly. +// bin/yt-dlp is a pure-Python zipapp, not a standalone binary: it has no bundled +// interpreter or native libs to extract/mmap, so it works on hosts with a +// noexec /tmp (unlike yt-dlp's PyInstaller-built binaries). It relies on a +// `#!/usr/bin/env python3` shebang, which spawn() can't honor on Windows and +// which resolves to whatever `python3` happens to be on PATH elsewhere (on +// o2switch that's an unsupported 3.6) — so it's always invoked explicitly +// through PYTHON_BIN. A system-wide `yt-dlp` (PATH fallback) already has its +// own proper launcher and is run directly. export function buildYtdlpCommand(args: string[]): YtdlpCommand { const bin = resolveYtdlpBin() const isBundledScript = bin !== 'yt-dlp' - if (process.platform === 'win32' && isBundledScript) { - return { command: 'python', args: [bin, ...args] } + if (isBundledScript) { + return { command: config.PYTHON_BIN, args: [bin, ...args] } } return { command: bin, args }