fix: fix o2switch deployment build and yt-dlp Python compatibility
Turbopack requires native SWC bindings unavailable on o2switch's old glibc, so force webpack for production builds. NODE_ENV=production on o2switch's shell also caused npm to skip devDependencies needed at build time (Tailwind, PostCSS), so deploy now installs with --include=dev. yt-dlp's standalone PyInstaller binary self-extracts to noexec /tmp and fails to mmap its bundled shared libs there, and o2switch's system python3 (3.6) is too old for the plain zipapp. Revert to the zipapp and invoke it explicitly through a configurable PYTHON_BIN, set to o2switch's newer Python 3.11 in .env.prod. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
- 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`).
|
- 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).
|
- 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)
|
## Deployment (o2switch)
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
|
| `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) |
|
| `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 |
|
| `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
|
## Scripts npm
|
||||||
|
|
||||||
|
|||||||
@@ -11,4 +11,5 @@ export const config = {
|
|||||||
PROBE_RATE_LIMIT_WINDOW_MS: 3_600_000,
|
PROBE_RATE_LIMIT_WINDOW_MS: 3_600_000,
|
||||||
PROBE_TIMEOUT_MS: 20_000,
|
PROBE_TIMEOUT_MS: 20_000,
|
||||||
BIN_DIR: process.env.BIN_DIR ?? path.join(process.cwd(), 'bin'),
|
BIN_DIR: process.env.BIN_DIR ?? path.join(process.cwd(), 'bin'),
|
||||||
|
PYTHON_BIN: process.env.PYTHON_BIN ?? (process.platform === 'win32' ? 'python' : 'python3'),
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build --webpack",
|
||||||
"start": "node server.js",
|
"start": "node server.js",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"worker": "tsx worker/index.ts",
|
"worker": "tsx worker/index.ts",
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ echo "==> Copie de .env.prod vers .env"
|
|||||||
cp .env.prod .env
|
cp .env.prod .env
|
||||||
|
|
||||||
echo "==> Installation des dependances"
|
echo "==> Installation des dependances"
|
||||||
npm install
|
npm install --include=dev
|
||||||
|
|
||||||
echo "==> Generation du client Prisma"
|
echo "==> Generation du client Prisma"
|
||||||
npm run db:generate
|
npm run db:generate
|
||||||
|
|||||||
@@ -1,47 +1,23 @@
|
|||||||
const mockExistsSync = jest.fn()
|
const mockExistsSync = jest.fn()
|
||||||
jest.mock('fs', () => ({ existsSync: (...args: unknown[]) => mockExistsSync(...args) }))
|
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'
|
import { buildYtdlpCommand } from '../ytdlp'
|
||||||
|
|
||||||
function setPlatform(platform: NodeJS.Platform) {
|
|
||||||
Object.defineProperty(process, 'platform', { value: platform })
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('buildYtdlpCommand', () => {
|
describe('buildYtdlpCommand', () => {
|
||||||
const originalPlatform = process.platform
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
setPlatform(originalPlatform)
|
|
||||||
mockExistsSync.mockReset()
|
mockExistsSync.mockReset()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('runs the bundled binary directly on Linux', () => {
|
it('runs the bundled zipapp through the configured Python interpreter', () => {
|
||||||
setPlatform('linux')
|
|
||||||
mockExistsSync.mockReturnValue(true)
|
mockExistsSync.mockReturnValue(true)
|
||||||
const { command, args } = buildYtdlpCommand(['-J', 'https://x.test'])
|
const { command, args } = buildYtdlpCommand(['-J', 'https://x.test'])
|
||||||
expect(command).toContain('yt-dlp')
|
expect(command).toBe('python3.11')
|
||||||
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(args[0]).toContain('yt-dlp')
|
expect(args[0]).toContain('yt-dlp')
|
||||||
expect(args.slice(1)).toEqual(['-J', 'https://x.test'])
|
expect(args.slice(1)).toEqual(['-J', 'https://x.test'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('runs a system-wide yt-dlp directly on Windows when no bundled binary exists', () => {
|
it('runs a system-wide yt-dlp directly 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')
|
|
||||||
mockExistsSync.mockReturnValue(false)
|
mockExistsSync.mockReturnValue(false)
|
||||||
const { command, args } = buildYtdlpCommand(['-J', 'https://x.test'])
|
const { command, args } = buildYtdlpCommand(['-J', 'https://x.test'])
|
||||||
expect(command).toBe('yt-dlp')
|
expect(command).toBe('yt-dlp')
|
||||||
|
|||||||
+10
-7
@@ -22,17 +22,20 @@ export function resolveYtdlpBin(): string {
|
|||||||
|
|
||||||
export type YtdlpCommand = { command: string; args: string[] }
|
export type YtdlpCommand = { command: string; args: string[] }
|
||||||
|
|
||||||
// The bundled bin/yt-dlp is a Python zipapp relying on a `#!/usr/bin/env python3`
|
// bin/yt-dlp is a pure-Python zipapp, not a standalone binary: it has no bundled
|
||||||
// shebang: Linux (o2switch prod) execs it natively, but Windows' spawn() has no
|
// interpreter or native libs to extract/mmap, so it works on hosts with a
|
||||||
// shebang support and fails with ENOENT, so it must be run through `python` there.
|
// noexec /tmp (unlike yt-dlp's PyInstaller-built binaries). It relies on a
|
||||||
// A system-wide `yt-dlp` (PATH fallback) already has a proper platform launcher
|
// `#!/usr/bin/env python3` shebang, which spawn() can't honor on Windows and
|
||||||
// on both OSes and is always run directly.
|
// 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 {
|
export function buildYtdlpCommand(args: string[]): YtdlpCommand {
|
||||||
const bin = resolveYtdlpBin()
|
const bin = resolveYtdlpBin()
|
||||||
const isBundledScript = bin !== 'yt-dlp'
|
const isBundledScript = bin !== 'yt-dlp'
|
||||||
|
|
||||||
if (process.platform === 'win32' && isBundledScript) {
|
if (isBundledScript) {
|
||||||
return { command: 'python', args: [bin, ...args] }
|
return { command: config.PYTHON_BIN, args: [bin, ...args] }
|
||||||
}
|
}
|
||||||
|
|
||||||
return { command: bin, args }
|
return { command: bin, args }
|
||||||
|
|||||||
Reference in New Issue
Block a user