Files
video-downloader/src/lib/ytdlp.ts
T
anthonyandClaude Sonnet 5 fd0bc092bd 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>
2026-08-11 16:13:12 +02:00

98 lines
3.0 KiB
TypeScript

import { existsSync } from 'fs'
import path from 'path'
import { config } from '../../config/app.config'
export type YtdlpParams = {
url: string
uuid: string
format: string
quality: string
subtitles: boolean
subtitleLangs: string[] | null
clipStart: number | null
clipEnd: number | null
audioQuality: string | null
extraArgs: string | null
}
export function resolveYtdlpBin(): string {
const local = path.join(config.BIN_DIR, 'yt-dlp')
return existsSync(local) ? local : 'yt-dlp'
}
export type YtdlpCommand = { command: string; args: string[] }
// 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 (isBundledScript) {
return { command: config.PYTHON_BIN, args: [bin, ...args] }
}
return { command: bin, args }
}
export function buildYtdlpArgs(params: YtdlpParams): string[] {
const {
url,
uuid,
format,
quality,
subtitles,
subtitleLangs,
clipStart,
clipEnd,
audioQuality,
extraArgs,
} = params
const outputTemplate = path.join(config.STORAGE_PATH, `${uuid}.%(ext)s`)
const args: string[] = ['--no-playlist', '-o', outputTemplate]
// bin/ffmpeg is a Linux ELF binary bundled for o2switch prod deploy; it can't
// run on Windows, so local dev instead looks for a bin/ffmpeg.exe.
const ffmpegBin = path.join(config.BIN_DIR, process.platform === 'win32' ? 'ffmpeg.exe' : 'ffmpeg')
if (existsSync(ffmpegBin)) {
args.push('--ffmpeg-location', ffmpegBin)
}
if (format === 'mp3') {
args.push('-x', '--audio-format', 'mp3')
args.push('--audio-quality', audioQuality && audioQuality !== 'best' ? `${audioQuality}K` : '0')
} else {
const heightFilter = quality !== 'best' ? `[height<=?${quality.replace('p', '')}]` : ''
args.push('-f', `bestvideo${heightFilter}+bestaudio/best${heightFilter}`)
args.push('--merge-output-format', format)
}
if (subtitles) {
const langs = subtitleLangs?.length ? subtitleLangs.join(',') : 'fr,en'
args.push('--write-subs', '--write-auto-subs', '--sub-lang', langs)
}
if (clipStart != null || clipEnd != null) {
args.push('--download-sections', `*${clipStart ?? 0}-${clipEnd ?? ''}`)
}
if (extraArgs) {
try {
const extra = JSON.parse(extraArgs) as string[]
args.push(...extra)
} catch {
// malformed extraArgs — skip silently
}
}
args.push(url)
return args
}