#!/usr/bin/env bash set -euo pipefail # POS: media 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" # Load grab.env config (env-seam: GRAB_DEFAULT) load_grab_config() { load_env_file "$CONFIG_DIR/grab.env" } load_grab_config # ── URL classification ───────────────────────────────────────── classify_url() { local url="$1" mode="${GRAB_DEFAULT:-video}" case "$url" in *music.youtube.com*) echo "audio" ;; *soundcloud.com*) echo "audio" ;; *bandcamp.com*) echo "audio" ;; *youtube.com*|*youtu.be*) echo "video" ;; *vimeo.com*) echo "video" ;; *twitch.tv*) echo "video" ;; *) echo "$mode" ;; esac } usage() { cat < Auto-download a URL as audio or video. Classifies the domain and delegates to 'pos media mp3' (audio) or 'pos media 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 Output directory (passed to mp3/mp4) --no-playlist Download only the single video --cookies 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 grab https://music.youtube.com/watch?v=abc pos media grab https://youtube.com/watch?v=xyz pos media grab --audio https://vimeo.com/123 pos media grab --worst https://youtu.be/abc pos media grab --dry-run https://soundcloud.com/artist/track 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 "pos media grab: --output needs a value" EXTRA_ARGS+=(--output "$2"); shift 2 ;; --no-playlist) EXTRA_ARGS+=(--no-playlist); shift ;; --cookies) [ $# -ge 2 ] || err "pos media grab: --cookies needs a value" EXTRA_ARGS+=(--cookies "$2"); shift 2 ;; --dry-run) DRY_RUN=1; shift ;; -*) err "pos media grab: Unknown option: $1 (see --help)" ;; *) [ -z "$URL" ] && URL="$1" && shift || err "pos media grab: Unexpected argument: $1" ;; esac done [ -n "$URL" ] || usage # Validate URL scheme case "$URL" in http://*|https://*) ;; *) err "pos media grab: not a valid URL: $URL (must start with http:// or https://)" ;; esac # Validate mutually exclusive overrides [ "$FORCE_AUDIO" -eq 1 ] && [ "$FORCE_VIDEO" -eq 1 ] && \ err "pos media grab: --audio and --video are mutually exclusive" [ "$BEST" -eq 1 ] && [ "$WORST" -eq 1 ] && \ err "pos media 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 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 mp4 --worst "${EXTRA_ARGS[@]}") else DELEGATE_ARGS=(pos media 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 "pos media 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