209 lines
7.2 KiB
Bash
Executable File
209 lines
7.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
# POS: media yt-grab — Auto-download URL as audio or video (classify + route)
|
|
# POS_FLAGS: --audio --video --best --worst --output --no-playlist --cookies --dry-run
|
|
# POS_CONFIG: grab | grab.env | GRAB_DEFAULT=:Default mode for unknown domains (video or audio, default video)
|
|
|
|
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
|
|
|
# Shared config loader (canonical KEY=VALUE parser, env-wins precedence)
|
|
source "$(dirname "$0")/../lib/config-ui.sh" 2>/dev/null || source "$(dirname "$0")/config-ui.sh"
|
|
|
|
# Shared yt helpers (classify_url, yt_validate_url, yt_check_deps, yt_echo_cmd)
|
|
source "$(dirname "$0")/../lib/yt-lib.sh" 2>/dev/null || source "$(dirname "$0")/yt-lib.sh"
|
|
|
|
# Load grab.env config (env-seam: GRAB_DEFAULT)
|
|
load_grab_config() {
|
|
load_env_file "$CONFIG_DIR/grab.env"
|
|
}
|
|
|
|
load_grab_config
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: pos media yt grab [options] <url>
|
|
|
|
Auto-download a URL as audio or video. Classifies the domain and delegates
|
|
to 'pos media yt mp3' (audio) or 'pos media yt mp4' (video).
|
|
|
|
Options:
|
|
--audio Force audio (mp3) download
|
|
--video Force video (mp4) download
|
|
--best Best quality for video (default for non-interactive)
|
|
--worst Lowest quality for video
|
|
-o, --output <dir> Output directory (passed to mp3/mp4)
|
|
--no-playlist Download only the single video
|
|
--cookies <file> Netscape cookies.txt for age-gated content
|
|
--dry-run Print the command that would run, don't execute
|
|
-h, --help This help
|
|
|
|
Examples:
|
|
pos media yt grab https://music.youtube.com/watch?v=abc
|
|
pos media yt grab https://youtube.com/watch?v=xyz
|
|
pos media yt grab --audio https://vimeo.com/123
|
|
pos media yt grab --worst https://youtu.be/abc
|
|
pos media yt grab --dry-run https://soundcloud.com/artist/track
|
|
|
|
The legacy spelling 'pos media grab' still works (forwarder) and is equivalent.
|
|
EOF
|
|
exit 0
|
|
}
|
|
|
|
# ── Arg parsing ────────────────────────────────────────────────
|
|
URL=""
|
|
FORCE_AUDIO=0
|
|
FORCE_VIDEO=0
|
|
BEST=0
|
|
WORST=0
|
|
DRY_RUN=0
|
|
EXTRA_ARGS=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-h|--help) usage ;;
|
|
--audio) FORCE_AUDIO=1; shift ;;
|
|
--video) FORCE_VIDEO=1; shift ;;
|
|
--best) BEST=1; shift ;;
|
|
--worst) WORST=1; shift ;;
|
|
-o|--output)
|
|
[ $# -ge 2 ] || err "yt-grab: --output needs a value"
|
|
EXTRA_ARGS+=(--output "$2"); shift 2 ;;
|
|
--no-playlist) EXTRA_ARGS+=(--no-playlist); shift ;;
|
|
--cookies)
|
|
[ $# -ge 2 ] || err "yt-grab: --cookies needs a value"
|
|
EXTRA_ARGS+=(--cookies "$2"); shift 2 ;;
|
|
--dry-run) DRY_RUN=1; shift ;;
|
|
-*) err "yt-grab: Unknown option: $1 (see --help)" ;;
|
|
*)
|
|
[ -z "$URL" ] && URL="$1" && shift || err "yt-grab: Unexpected argument: $1" ;;
|
|
esac
|
|
done
|
|
|
|
[ -n "$URL" ] || usage
|
|
|
|
# Validate URL scheme
|
|
yt_validate_url "$URL" 2>/dev/null || \
|
|
err "yt-grab: not a valid URL: $URL (must start with http:// or https://)"
|
|
|
|
# Validate mutually exclusive overrides
|
|
[ "$FORCE_AUDIO" -eq 1 ] && [ "$FORCE_VIDEO" -eq 1 ] && \
|
|
err "yt-grab: --audio and --video are mutually exclusive"
|
|
[ "$BEST" -eq 1 ] && [ "$WORST" -eq 1 ] && \
|
|
err "yt-grab: --best and --worst are mutually exclusive"
|
|
|
|
# ── Classification ─────────────────────────────────────────────
|
|
mode=""
|
|
if [ "$FORCE_AUDIO" -eq 1 ]; then
|
|
mode="audio"
|
|
elif [ "$FORCE_VIDEO" -eq 1 ]; then
|
|
mode="video"
|
|
else
|
|
mode="$(classify_url "$URL")"
|
|
fi
|
|
|
|
# ── Build delegated command ────────────────────────────────────
|
|
DELEGATE_ARGS=()
|
|
|
|
if [ "$mode" = "audio" ]; then
|
|
DELEGATE_ARGS=(pos media yt mp3 "${EXTRA_ARGS[@]}")
|
|
else
|
|
# mp4 route: --best by default (non-interactive), --worst if user passes it
|
|
if [ "$WORST" -eq 1 ]; then
|
|
DELEGATE_ARGS=(pos media yt mp4 --worst "${EXTRA_ARGS[@]}")
|
|
else
|
|
DELEGATE_ARGS=(pos media yt mp4 --best "${EXTRA_ARGS[@]}")
|
|
fi
|
|
fi
|
|
|
|
# ── Dry run ────────────────────────────────────────────────────
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
echo "${DELEGATE_ARGS[*]} $URL"
|
|
exit 0
|
|
fi
|
|
|
|
# ── Execute ────────────────────────────────────────────────────
|
|
output=""
|
|
rc=0
|
|
if output=$("${DELEGATE_ARGS[@]}" "$URL" 2>&1); then
|
|
rc=0
|
|
else
|
|
rc=$?
|
|
fi
|
|
|
|
if [ "$rc" -ne 0 ]; then
|
|
# Summarize stderr for the user
|
|
summary="$(printf '%s' "$output" | grep -i 'error\|fail' | head -1 || true)"
|
|
[ -z "$summary" ] && summary="exit code $rc"
|
|
err "yt-grab: ❌ Download failed: $summary"
|
|
fi
|
|
|
|
# ── Metadata + summary ────────────────────────────────────────
|
|
title=""
|
|
duration=""
|
|
file_path=""
|
|
|
|
# Determine expected output directory
|
|
if [ "$mode" = "audio" ]; then
|
|
out_dir="$HOME/Music"
|
|
for (( i=0; i<${#EXTRA_ARGS[@]}; i++ )); do
|
|
if [ "${EXTRA_ARGS[$i]}" = "--output" ] && [ $(( i + 1 )) -lt ${#EXTRA_ARGS[@]} ]; then
|
|
out_dir="${EXTRA_ARGS[$(( i + 1 ))]}"
|
|
break
|
|
fi
|
|
done
|
|
file_ext="mp3"
|
|
else
|
|
out_dir="$HOME/Videos"
|
|
for (( i=0; i<${#EXTRA_ARGS[@]}; i++ )); do
|
|
if [ "${EXTRA_ARGS[$i]}" = "--output" ] && [ $(( i + 1 )) -lt ${#EXTRA_ARGS[@]} ]; then
|
|
out_dir="${EXTRA_ARGS[$(( i + 1 ))]}"
|
|
break
|
|
fi
|
|
done
|
|
file_ext="mp4"
|
|
fi
|
|
|
|
# Fetch metadata (fast, no download)
|
|
if command -v yt-dlp &>/dev/null; then
|
|
meta="$(yt-dlp --print title --print duration_string --no-warnings "$URL" 2>/dev/null || true)"
|
|
title="$(printf '%s' "$meta" | sed -n '1p')"
|
|
duration="$(printf '%s' "$meta" | sed -n '2p')"
|
|
fi
|
|
|
|
# Find the downloaded file (most recent matching extension in out_dir)
|
|
if [ -d "$out_dir" ]; then
|
|
file_path="$(find "$out_dir" -maxdepth 1 -name "*.$file_ext" -printf '%T@ %p\n' 2>/dev/null \
|
|
| sort -rn | head -1 | cut -d' ' -f2- || true)"
|
|
fi
|
|
|
|
# Build summary
|
|
[ -z "$title" ] && title="$(basename "$URL" | sed 's/[?#].*//')"
|
|
[ -z "$duration" ] && duration="?"
|
|
|
|
if [ "$mode" = "audio" ]; then
|
|
emoji="🎵"
|
|
else
|
|
emoji="🎬"
|
|
fi
|
|
|
|
echo "$emoji Downloaded: $title ($duration)"
|
|
|
|
if [ -n "$file_path" ] && [ -f "$file_path" ]; then
|
|
file_size="$(stat --printf='%s' "$file_path" 2>/dev/null || echo "0")"
|
|
# Format size in human-readable form
|
|
if [ "$file_size" -ge 1073741824 ]; then
|
|
size_human="$(awk "BEGIN { printf \"%.1f GB\", $file_size / 1073741824 }")"
|
|
elif [ "$file_size" -ge 1048576 ]; then
|
|
size_human="$(awk "BEGIN { printf \"%.1f MB\", $file_size / 1048576 }")"
|
|
elif [ "$file_size" -ge 1024 ]; then
|
|
size_human="$(awk "BEGIN { printf \"%.1f KB\", $file_size / 1024 }")"
|
|
else
|
|
size_human="${file_size} B"
|
|
fi
|
|
# Show path relative to HOME
|
|
rel_path="${file_path/#$HOME/\~}"
|
|
echo "📁 $rel_path ($size_human)"
|
|
else
|
|
echo "📁 $out_dir/ ($file_ext)"
|
|
fi
|