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:
2026-08-11 14:13:38 +02:00
co-authored by Claude Sonnet 5
parent f949566f76
commit 27ba48d181
23 changed files with 897 additions and 111 deletions
+12 -2
View File
@@ -9,13 +9,23 @@
"urlLabel": "URL del vídeo",
"format": "Formato",
"quality": "Calidad",
"subtitles": "Subtítulos (fr, en)",
"subtitles": "Subtítulos",
"subtitleLanguages": "Idiomas de los subtítulos",
"advanced": "Opciones avanzadas",
"formats": "mp4 · mp3 · webm · mkv",
"submit": "Descargar",
"submitting": "Enviando...",
"errorRateLimit": "Demasiadas peticiones. Inténtalo de nuevo en una hora.",
"errorGeneric": "Error al enviar."
"errorGeneric": "Error al enviar.",
"analyzing": "Analizando el vídeo...",
"probeError": "No se pudo analizar este vídeo. Comprueba el enlace o inténtalo de nuevo.",
"retry": "Reintentar",
"audioOnlySource": "Fuente solo de audio",
"clipRange": "Fragmento (opcional)",
"clipStart": "Inicio (s)",
"clipEnd": "Fin (s)",
"audioQuality": "Calidad de audio",
"videoLength": "Duración: {duration}"
},
"status": {
"heading": "Estado de la descarga",