100 lines
3.1 KiB
Bash
Executable File
100 lines
3.1 KiB
Bash
Executable File
#!/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 <youtube-url> [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"
|