Files
Linux_post_install/bin/pos-media-yt-grab
T
Your Name f14d24950a
gates / consistency-and-conventions (push) Successful in 20s
feat: pos media yt — unified YouTube tools + subtitles (POS--9)
2026-09-09 07:04:22 -04:00

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