#!/usr/bin/env bash # Download YouTube video(s) at highest quality as merged MP4 (Linux/macOS). # # ./yt-download.sh "https://www.youtube.com/watch?v=xxxx" # ./yt-download.sh URL1 URL2 URL3 # # Env overrides: # OUTDIR=/path/to/dir output directory (default: ~/Downloads) # FORMAT="..." yt-dlp -f selector (default: bestvideo+bestaudio/best) # COOKIES=/path/cookies.txt Netscape cookies file (default: cookies.txt beside this script) # # Requirements: yt-dlp, ffmpeg, and a JS runtime (bun/deno/node) must be installed. # pipx install yt-dlp (or: brew install yt-dlp / apt install yt-dlp) # brew install ffmpeg (or: apt install ffmpeg) # bun / deno / node any one (needed for YouTube JS challenges) set -euo pipefail if [ "$#" -lt 1 ]; then echo "usage: $0 [more urls...]" >&2 exit 2 fi SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" OUTDIR="${OUTDIR:-$HOME/Downloads}" FORMAT="${FORMAT:-bestvideo+bestaudio/best}" # yt-dlp if ! command -v yt-dlp >/dev/null 2>&1; then echo "error: yt-dlp not found. Install it: pipx install yt-dlp" >&2 exit 1 fi # ffmpeg if ! command -v ffmpeg >/dev/null 2>&1; then echo "warning: ffmpeg not found; merging video+audio will fail. Install ffmpeg." >&2 fi # JS runtime for YouTube JS challenges. Prefer bun, fall back to deno/node. # Override with JS_RUNTIME=bun|deno|node (must be on PATH). # --no-js-runtimes ensures ONLY the chosen runtime is used (bun won't be # silently shadowed by yt-dlp's built-in deno preference). JS_ARGS=() JS_RUNTIME_NAME= JS_RUNTIME_BIN= if [ -n "${JS_RUNTIME:-}" ] && command -v "$JS_RUNTIME" >/dev/null 2>&1; then JS_RUNTIME_NAME="$JS_RUNTIME" JS_RUNTIME_BIN="$(command -v "$JS_RUNTIME")" elif command -v bun >/dev/null 2>&1; then JS_RUNTIME_NAME="bun" JS_RUNTIME_BIN="$(command -v bun)" elif command -v deno >/dev/null 2>&1; then JS_RUNTIME_NAME="deno" JS_RUNTIME_BIN="$(command -v deno)" elif [ -x "$HOME/.deno/bin/deno" ]; then JS_RUNTIME_NAME="deno" JS_RUNTIME_BIN="$HOME/.deno/bin/deno" elif command -v node >/dev/null 2>&1; then JS_RUNTIME_NAME="node" JS_RUNTIME_BIN="$(command -v node)" fi if [ -n "$JS_RUNTIME_NAME" ]; then JS_ARGS=(--no-js-runtimes --js-runtimes "$JS_RUNTIME_NAME:$JS_RUNTIME_BIN") else echo "warning: no JS runtime (bun/deno/node) found. YouTube may reject the download ('confirm you're not a bot')." >&2 fi # cookies COOKIES="${COOKIES:-$SCRIPT_DIR/cookies.txt}" COOKIE_ARGS=() if [ -f "$COOKIES" ]; then COOKIE_ARGS=(--cookies "$COOKIES") echo "[info] using cookies: $COOKIES" else echo "[info] no cookies.txt -- public videos only. See SKILL.md for login-required videos." fi mkdir -p "$OUTDIR" fail=0 for url in "$@"; do echo "" echo "==== downloading: $url ====" if ! yt-dlp "${JS_ARGS[@]}" "${COOKIE_ARGS[@]}" \ -f "$FORMAT" \ --merge-output-format mp4 \ -o "$OUTDIR/%(title)s [%(height)sp].%(ext)s" \ --no-playlist \ "$url"; then fail=$((fail+1)) echo "warning: failed: $url" >&2 fi done if [ "$fail" -gt 0 ]; then echo "warning: $fail download(s) failed." >&2 exit 1 fi echo "" echo "All done. Saved to: $OUTDIR"