feat: gate download options on real-time yt-dlp video analysis
Options (quality, subtitles) are now derived from a yt-dlp -J probe of the submitted URL instead of a static list, so users can't pick combinations the source video doesn't actually support. Submission is blocked until the probe succeeds. - New /api/probe endpoint + src/lib/ytdlp-probe.ts, with its own rate limiter (rate-limit.ts refactored into a createRateLimiter factory). - New options: subtitle language selection, clip start/end trim, MP3 audio quality. - Fixed buildYtdlpArgs: format=mp3 never triggered audio extraction (-x/--audio-format), and quality=best never applied --merge-output-format, so the chosen container had no real effect. - Cross-platform yt-dlp invocation: the bundled bin/yt-dlp is a Python zipapp relying on a shebang, which Windows' spawn() can't run directly. buildYtdlpCommand() runs it through `python` on Windows and execs it directly on Linux/o2switch, where the shebang works natively. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+52
-5
@@ -8,11 +8,49 @@ export type YtdlpParams = {
|
||||
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[] }
|
||||
|
||||
// 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.
|
||||
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] }
|
||||
}
|
||||
|
||||
return { command: bin, args }
|
||||
}
|
||||
|
||||
export function buildYtdlpArgs(params: YtdlpParams): string[] {
|
||||
const { url, uuid, format, quality, subtitles, extraArgs } = params
|
||||
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]
|
||||
@@ -22,13 +60,22 @@ export function buildYtdlpArgs(params: YtdlpParams): string[] {
|
||||
args.push('--ffmpeg-location', ffmpegBin)
|
||||
}
|
||||
|
||||
if (quality !== 'best') {
|
||||
const height = quality.replace('p', '')
|
||||
args.push('-f', `${format}[height<=?${height}]+bestaudio/best[height<=?${height}]`)
|
||||
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) {
|
||||
args.push('--write-sub', '--sub-lang', 'fr,en')
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user