feat: pos media yt — unified YouTube tools + subtitles (POS--9)
gates / consistency-and-conventions (push) Successful in 20s
gates / consistency-and-conventions (push) Successful in 20s
This commit is contained in:
+2
-216
@@ -1,219 +1,5 @@
|
||||
#!/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 <<EOF
|
||||
Usage: pos media grab [options] <url>
|
||||
|
||||
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 <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 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
|
||||
# -h|--help forwarded via exec below
|
||||
exec pos media yt grab "$@"
|
||||
|
||||
Reference in New Issue
Block a user