d817c37652
gates / consistency-and-conventions (push) Successful in 26s
17-point code-level audit executed via Explorer->Architect->Builder->Tester->Reviewer;
Reviewer accepted (APPROVE_WITH_NOTES; 3 block-list items resolved):
- security: telegram sender-owner AND-gate + TELEGRAM_OWNER_ID, matrix
MATRIX_ROOM_ID fail-closed, gpg --passphrase-fd 3 (no argv secret),
/dev/tcp positional-arg form (checkport/smb-client/share-lib/NET_PROBE),
eval deny-by-default + --no-command-execution carried by both chat bridges,
tty-gated --trust; config/{telegram,matrix}.env reference templates
- ai: all ExecStart flags validated against installed llama.cpp
(requested->error, default->omit+warn, CONFIG_REQUESTED_FLAGS); single-file
hf download failure rc=1 + no .hf-meta; LLAMACPP_HOST coherent;
POS_SUBCMDS + metadata gaps closed
- tooling: lint-conventions Bash-native rewrite (~24-30x faster, rules and
output byte-identical, :num restored); pos system uninstall covers all 12
libs + scale-tail + flags dir + systemd user units (|| true) + plugin
markers; anchored .bash_completion/.bashrc removal replaces sed -i '/pos/d'
- config: canonical load_env_file in lib/config-ui.sh (CRLF strip, env-wins,
XDG, LOADED_ENV_KEYS); 9 tools migrated; entertainment-lib collapsed to
wrappers; docker-compose deliberately unmigrated (source semantics)
- tests: first committed regression suite — tests/run-tests.sh zero-dep
runner + make test; 12 files / 179 checks / 0 skip / ~52s; hard skip
contract; systemd-analyze verify on generated unit PASS
Verified: make gen idempotent; make check green; make lint 0 FAIL, 0 WARN;
make test green; bash -n clean; git diff --check clean. Audit deliverables +
agent reports + AGENT_TODO Done entry included.
220 lines
7.6 KiB
Bash
Executable File
220 lines
7.6 KiB
Bash
Executable File
#!/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
|