diff --git a/AGENT_TODO.md b/AGENT_TODO.md index efd8b8a..589908f 100644 --- a/AGENT_TODO.md +++ b/AGENT_TODO.md @@ -16,6 +16,8 @@ summary (newest last). ## Done +- **2026-08-09** — `pos media mp3`/`mp4` hardened + smart format selection. Both tools: yt-dlp calls go through `spawn` (honor `$DRY_RUN`; `--dry-run` prints the exact command and skips dep checks), `-o/--output`, `--no-playlist`, `--cookies` (file existence check), clean ffmpeg/yt-dlp guards, `# POS_FLAGS:` for completion, full metadata (`--embed-metadata --embed-chapters --embed-thumbnail --no-overwrites`, mp3 also `--convert-thumbnails jpg` + `--parse-metadata "%(artist,uploader)s:%(artist)s"` so the uploader fills the artist tag). mp3 gains `--by-artist` (`~/Music//.mp3`). mp4 gains `-f <id>` / `--best` / `--worst` (no prompt), conflict validation, and an interactive picker that shows a **curated** `-F` table (`[audio]`/`[video]`/`[combo]` grouping, raw clutter dropped) on stderr — stdout carries only the chosen id (ui_pick lesson) — with id validation against the real table and empty/best default. Docs: howto/media.md rewritten (flags, metadata, by-artist, troubleshooting); `make gen && make check` green. + - **2026-08-09** — Telegram `ai …` now answers about a message you reply to: the listener extracts `reply_to_message.text` (falls back to `caption`) from each update and passes it to `handle_message`; the AI bridge prefixes the prompt with `[Reply context — the message you are replying to]`. So replying to a `/status` output and asking `ai check this details` gives the model the actual output. Applies only to the AI bridge (mapped `/commands` untouched); reply context rides in the user turn so the session records what was analyzed. Docs: howto/ai.md bridge section. - **2026-08-09** — `pos ai gemini` sessions + Telegram-friendly replies. `--session <name>` gives `ask`/`chat` persistent memory (`~/.local/share/linux_post_install/ai/<name>.json`, capped at 40 turns, pruning keeps the first user turn as scene); new `sessions` subcommand (list / `reset <name>`). Telegram listener now keeps one session per chat (`telegram-<chat_id>`) with `ai /reset` to clear. New `--system "<text>"` flag injects a Gemini `systemInstruction` (via `jq` merge) sent every turn but never stored in the session file; the listener passes a Telegram-voice prompt ("reply like a friendly Telegram chat, use emojis") and strips markdown (`**`, `*`, backticks, `#`, links, lists, blockquotes) from replies before `sendMessage`, since messages go out as plain text. Docs: howto/ai.md (flags, sessions, bridge memory/formatting), `make gen && make check` green. diff --git a/DOC/AGENT_Context_Project.md b/DOC/AGENT_Context_Project.md index 1bb0623..aea6d60 100644 --- a/DOC/AGENT_Context_Project.md +++ b/DOC/AGENT_Context_Project.md @@ -72,7 +72,7 @@ Linux_post_install/ │ ├── pos-entertainment-send # Run a public-API plugin and send its output via Telegram (default sender) │ ├── pos-entertainment-status # Show enabled plugins and scheduler state │ ├── pos-media-mp3 # Download audio as MP3 (yt-dlp) -│ ├── pos-media-mp4 # Download video as MP4 (interactive format select) +│ ├── pos-media-mp4 # Download video as MP4 (smart/interactive format select) │ ├── pos-network-checkport # Check TCP port connectivity │ ├── pos-network-hotspot # Wi-Fi hotspot via create_ap + wihotspot-gui │ ├── pos-network-ip # Show interfaces, routes, public IP + location @@ -267,7 +267,7 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst | entertainment | send | `pos-entertainment-send` | Run a public-API plugin and send its output via Telegram (default sender) | | entertainment | status | `pos-entertainment-status` | Show enabled plugins and scheduler state | | media | mp3 | `pos-media-mp3` | Download audio as MP3 (yt-dlp) | -| media | mp4 | `pos-media-mp4` | Download video as MP4 (interactive format select) | +| media | mp4 | `pos-media-mp4` | Download video as MP4 (smart/interactive format select) | | network | checkport | `pos-network-checkport` | Check TCP port connectivity | | network | hotspot | `pos-network-hotspot` | Wi-Fi hotspot via create_ap + wihotspot-gui | | network | ip | `pos-network-ip` | Show interfaces, routes, public IP + location | @@ -573,8 +573,8 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `bin/pos-entertainment-enable` | 49 | Enable an auto-trigger for a plugin on a schedule | | `bin/pos-entertainment-send` | 93 | Run a public-API plugin and send its output via Telegram (default sender) | | `bin/pos-entertainment-status` | 49 | Show enabled plugins and scheduler state | -| `bin/pos-media-mp3` | 35 | Download audio as MP3 (yt-dlp) | -| `bin/pos-media-mp4` | 38 | Download video as MP4 (interactive format select) | +| `bin/pos-media-mp3` | 80 | Download audio as MP3 (yt-dlp) | +| `bin/pos-media-mp4` | 126 | Download video as MP4 (smart/interactive format select) | | `bin/pos-network-checkport` | 45 | Check TCP port connectivity | | `bin/pos-network-hotspot` | 93 | Wi-Fi hotspot via create_ap + wihotspot-gui | | `bin/pos-network-ip` | 69 | Show interfaces, routes, public IP + location | @@ -587,7 +587,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:` | `bin/pos-system-nfs-server` | 134 | Manage the NFS kernel server (status, share/unshare exports, enable/disable) | | `bin/pos-tree` | 110 | Show the pos CLI command tree: categories, commands, and subcommands | | `bin/pos-usb-server` | 218 | USB Redirector server control (--ls, --share; prompts when args omitted) | -| `completions/pos.bash` | 282 | Dynamic bash completion | +| `completions/pos.bash` | 284 | Dynamic bash completion | <!-- GEN:END filetable --> | `apps/install.sh` | 171 | App install/uninstall picker/orchestrator | diff --git a/DOC/howto/media.md b/DOC/howto/media.md index 881f847..dda2b9d 100644 --- a/DOC/howto/media.md +++ b/DOC/howto/media.md @@ -5,10 +5,11 @@ Download audio and video from the web via `yt-dlp`. Tools: `mp3`, `mp4`. | Tool | What it does | |------|--------------| | `pos media mp3` | Download audio, convert to MP3 | -| `pos media mp4` | Download video with interactive format selection | +| `pos media mp4` | Download video with smart/interactive format selection | -Requires `yt-dlp` (`sudo apt install yt-dlp`); the tools fail with a clean -error message instead of a raw `command not found` if it's missing. +Requires `yt-dlp` and `ffmpeg` (`sudo apt install yt-dlp ffmpeg`); the tools +fail with a clean error message instead of a raw `command not found` if either +is missing. --- @@ -18,43 +19,81 @@ error message instead of a raw `command not found` if it's missing. pos media mp3 <url> ``` -Extracts and converts the audio track to MP3 in the current directory. +Extracts and converts the audio track to MP3 in `~/Music/`. With `--by-artist` +files land in `~/Music/<artist>/<title>.mp3` (falls back to the uploader name +when there's no artist tag), so a library stays organized. ```bash pos media mp3 https://youtube.com/watch?v=dQw4w9WgXcQ +pos media mp3 --by-artist https://youtu.be/dQw4w9WgXcQ ``` +MP3s are encoded at best quality with title/artist/album/date/chapters embedded +(`--embed-metadata --embed-chapters`, artist falls back to uploader) plus the +cover art as a JPEG thumbnail. Existing files are never overwritten. + +| Flag | Meaning | +|------|---------| +| `-o, --output <dir>` | Output directory (default `~/Music`) | +| `--by-artist` | Organize as `<dir>/<artist>/<title>.mp3` | +| `--no-playlist` | Download only the single video, not the playlist | +| `--cookies <file>` | Netscape cookies.txt for age-gated content | +| `--dry-run` | Print the yt-dlp command without running it | + **Recipe:** batch — loop over a list of URLs: ```bash -while read -r url; do pos media mp3 "$url"; done < urls.txt +while read -r url; do pos media mp3 --by-artist "$url"; done < urls.txt ``` -**Troubleshooting:** MP3 conversion needs `ffmpeg`; if the tool errors about a -missing postprocessor, `sudo apt install ffmpeg`. Single-song playlists: use the -video URL directly, or a playlist entry. - --- -## `pos media mp4` — video with format choice +## `pos media mp4` — video with smart format choice ```bash pos media mp4 <url> ``` -Lists the available formats (`yt-dlp -F`) and lets you pick interactively, then -downloads to the current directory. +Without a format flag, the available formats are shown as a **short curated +list** (grouped `[audio]` / `[video]` / `[combo]`, with the raw dump's clutter +dropped) and you pick one — the id is validated before downloading. Entering +nothing (or `best`) picks the best video+audio automatically. + +Non-interactive (scripting-friendly): + +| Flag | Meaning | +|------|---------| +| `-f, --format <id>` | Download that format id directly (no prompt) | +| `--best` | Best video + audio, no prompt | +| `--worst` | Lowest quality, no prompt | +| `-o, --output <dir>` | Output directory (default `~/Videos`) | +| `--no-playlist` | Download only the single video | +| `--cookies <file>` | Netscape cookies.txt for age-gated content | +| `--dry-run` | Print the yt-dlp command without running it | ```bash -pos media mp4 https://youtube.com/watch?v=dQw4w9WgXcQ +pos media mp4 --best https://youtube.com/watch?v=dQw4w9WgXcQ +pos media mp4 -f 22 https://youtube.com/watch?v=dQw4w9WgXcQ ``` -**Recipe:** grab a 4K stream for later — pick the highest `video only` format + -best audio; yt-dlp merges them (again needs `ffmpeg`). +Videos merge to MP4 with metadata, chapters, subtitles (all languages) and the +thumbnail embedded (`--embed-metadata --embed-chapters --embed-subs +--sub-langs all --embed-thumbnail`); existing files are never overwritten. -**Troubleshooting:** -- Format list is empty → the site/age-gate requires cookies/auth; yt-dlp can't - access it — use a URL yt-dlp supports, or update yt-dlp (`sudo apt upgrade yt-dlp`). -- Very large downloads: ensure free space; files land in the current directory. +**Recipe:** grab a 4K stream for later — `--best` already picks the best +video+audio and merges them. + +--- + +## Troubleshooting + +- Format list is empty / download fails → the site or age-gate requires + cookies; pass `--cookies ~/cookies.txt` (export it from your browser), or + update yt-dlp (`sudo apt upgrade yt-dlp`). +- Error about a missing postprocessor → `sudo apt install ffmpeg`. +- `--by-artist` leaves files loose → the source has no artist/uploader tag; + it falls back to the uploader name in the artist slot. +- Very large downloads: ensure free space; files land in `~/Music`/`~/Videos` + (or your `-o` directory). --- diff --git a/bin/pos-media-mp3 b/bin/pos-media-mp3 index 6017355..509441b 100755 --- a/bin/pos-media-mp3 +++ b/bin/pos-media-mp3 @@ -1,35 +1,80 @@ #!/usr/bin/env bash set -euo pipefail # POS: media mp3 — Download audio as MP3 (yt-dlp) +# POS_FLAGS: --output --no-playlist --cookies --by-artist --dry-run source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh" +OUT_DIR="$HOME/Music" +BY_ARTIST=0 +EXTRA_ARGS=() + usage() { cat <<EOF -Usage: pos media mp3 <url> +Usage: pos media mp3 [options] <url> Download audio from a URL and convert to MP3 via yt-dlp. +Options: + -o, --output <dir> Output directory (default: $OUT_DIR) + --by-artist Organize as <dir>/<artist>/<title>.mp3 + --no-playlist Download only the single video + --cookies <file> Netscape cookies.txt for age-gated content + --dry-run Print the yt-dlp command without running it + -h, --help This help + Examples: pos media mp3 https://youtube.com/watch?v=dQw4w9WgXcQ + pos media mp3 --by-artist --no-playlist https://youtu.be/dQw4w9WgXcQ + pos media mp3 --cookies ~/cookies.txt https://vimeo.com/123 EOF exit 0 } -case "${1:-}" in - -h|--help|"") usage ;; -esac +DRY_RUN=0 +URL="" +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) usage ;; + -o|--output) + [ $# -ge 2 ] || err "--output needs a value" + OUT_DIR="$2"; shift 2 ;; + --by-artist) BY_ARTIST=1; shift ;; + --no-playlist) EXTRA_ARGS+=(--no-playlist); shift ;; + --cookies) + [ $# -ge 2 ] || err "--cookies needs a value" + [ -f "$2" ] || err "cookies file not found: $2" + EXTRA_ARGS+=(--cookies "$2"); shift 2 ;; + --dry-run) DRY_RUN=1; shift ;; + -*) err "Unknown option: $1 (see --help)" ;; + *) + [ -z "$URL" ] && URL="$1" && shift || err "Unexpected argument: $1" ;; + esac +done +[ -n "$URL" ] || usage -command -v yt-dlp &>/dev/null || err "yt-dlp not found — install it with: sudo apt install yt-dlp" +# Dependency checks (skipped under --dry-run: the command is only previewed). +if [ "$DRY_RUN" -eq 0 ]; then + command -v yt-dlp &>/dev/null || err "yt-dlp not found — install it with: sudo apt install yt-dlp" + command -v ffmpeg &>/dev/null || err "ffmpeg not found (needed for MP3 conversion) — install it with: sudo apt install ffmpeg" +fi -url="$1" +if [ "$BY_ARTIST" -eq 1 ]; then + OUT_TEMPLATE="$OUT_DIR/%(artist,uploader)s/%(title)s.%(ext)s" +else + OUT_TEMPLATE="$OUT_DIR/%(title)s.%(ext)s" +fi -yt-dlp \ - -x \ - --audio-format mp3 \ - --audio-quality 0 \ - --embed-thumbnail \ - --convert-thumbnails jpg \ - --add-metadata \ - -o "$HOME/Music/%(title)s.%(ext)s" \ - "$url" +args=(yt-dlp -x --audio-format mp3 --audio-quality 0 + --embed-metadata --embed-chapters + --embed-thumbnail --convert-thumbnails jpg + --parse-metadata "%(artist,uploader)s:%(artist)s" + --no-overwrites + -o "$OUT_TEMPLATE" + "${EXTRA_ARGS[@]}" "$URL") + +if [ "$DRY_RUN" -eq 1 ]; then + echo "yt-dlp ${args[*]:1}" +else + spawn "downloading audio → $OUT_DIR" "${args[@]}" +fi diff --git a/bin/pos-media-mp4 b/bin/pos-media-mp4 index b0b8bfa..520dadd 100755 --- a/bin/pos-media-mp4 +++ b/bin/pos-media-mp4 @@ -1,38 +1,126 @@ #!/usr/bin/env bash set -euo pipefail -# POS: media mp4 — Download video as MP4 (interactive format select) +# POS: media mp4 — Download video as MP4 (smart/interactive format select) +# POS_FLAGS: --format --best --worst --output --no-playlist --cookies --dry-run source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh" +OUT_DIR="$HOME/Videos" +FORMAT="" +BEST=0 +WORST=0 +EXTRA_ARGS=() + usage() { cat <<EOF -Usage: pos media mp4 <url> +Usage: pos media mp4 [options] <url> -Download video from a URL via yt-dlp with interactive format selection. +Download video from a URL via yt-dlp. Without -f/--best/--worst, formats are +listed and picked interactively. + +Options: + -f, --format <id> Download that format id directly (no prompt) + --best Best video + audio (no prompt) + --worst Lowest quality (no prompt) + -o, --output <dir> Output directory (default: $OUT_DIR) + --no-playlist Download only the single video + --cookies <file> Netscape cookies.txt for age-gated content + --dry-run Print the yt-dlp command without running it + -h, --help This help Examples: pos media mp4 https://youtube.com/watch?v=dQw4w9WgXcQ + pos media mp4 --best https://youtube.com/watch?v=dQw4w9WgXcQ + pos media mp4 -f 22 https://youtube.com/watch?v=dQw4w9WgXcQ EOF exit 0 } -case "${1:-}" in - -h|--help|"") usage ;; -esac +DRY_RUN=0 +URL="" +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) usage ;; + -f|--format) + [ $# -ge 2 ] || err "--format needs a value" + FORMAT="$2"; shift 2 ;; + --best) BEST=1; shift ;; + --worst) WORST=1; shift ;; + -o|--output) + [ $# -ge 2 ] || err "--output needs a value" + OUT_DIR="$2"; shift 2 ;; + --no-playlist) EXTRA_ARGS+=(--no-playlist); shift ;; + --cookies) + [ $# -ge 2 ] || err "--cookies needs a value" + [ -f "$2" ] || err "cookies file not found: $2" + EXTRA_ARGS+=(--cookies "$2"); shift 2 ;; + --dry-run) DRY_RUN=1; shift ;; + -*) err "Unknown option: $1 (see --help)" ;; + *) + [ -z "$URL" ] && URL="$1" && shift || err "Unexpected argument: $1" ;; + esac +done +[ -n "$URL" ] || usage -command -v yt-dlp &>/dev/null || err "yt-dlp not found — install it with: sudo apt install yt-dlp" +[ -n "$FORMAT" ] && { [ "$BEST" -eq 1 ] || [ "$WORST" -eq 1 ]; } \ + && err "--format and --best/--worst are mutually exclusive" +[ "$BEST" -eq 1 ] && [ "$WORST" -eq 1 ] && err "--best and --worst are mutually exclusive" -url="$1" +# Dependency checks (skipped under --dry-run: the command is only previewed). +if [ "$DRY_RUN" -eq 0 ]; then + command -v yt-dlp &>/dev/null || err "yt-dlp not found — install it with: sudo apt install yt-dlp" + command -v ffmpeg &>/dev/null || err "ffmpeg not found (needed for MP4 merge) — install it with: sudo apt install ffmpeg" +fi -yt-dlp -F "$url" +# Resolve the format selector: explicit flag → smart preset → interactive. +# The curated table goes to stderr; stdout carries ONLY the chosen format id +# (same ui_pick lesson — a $(...) capture must not swallow the menu). +fmt_spec="" +if [ -n "$FORMAT" ]; then + fmt_spec="$FORMAT" +elif [ "$BEST" -eq 1 ]; then + fmt_spec="bestvideo*+bestaudio/best" +elif [ "$WORST" -eq 1 ]; then + fmt_spec="worst" +else + table="$(yt-dlp -F "$URL" 2>&1)" || { + echo "ERROR: no formats listed (unsupported site or age-gated content)" >&2 + exit 1 + } + printf '%s' "$table" | awk -F'[[:space:]][[:space:]]+' ' + NR <= 2 { print; next } + $0 ~ /^-+$/ { print; next } + $0 ~ /audio only/ && $0 !~ /storyboard/ && $1 !~ /^sb/ { print " [audio] " $0; next } + $0 ~ /video only/ && $1 !~ /^sb/ { print " [video] " $0; next } + $1 ~ /^[0-9]+$/ && $2 ~ /^(mp4|webm)$/ && $3 ~ /x/ { print " [combo] " $0 } + ' >&2 + echo >&2 + read -rp "Format ID ('best'/'worst'/empty = best): " chosen + case "${chosen:-best}" in + best) fmt_spec="bestvideo*+bestaudio/best" ;; + worst) fmt_spec="worst" ;; + *) + if printf '%s' "$table" | grep -qE "^[[:space:]]*${chosen}[[:space:]]"; then + fmt_spec="$chosen" + else + echo "ERROR: unknown format id '$chosen'" >&2 + exit 1 + fi + ;; + esac +fi -echo -read -rp "Enter format ID: " format +args=(yt-dlp -f "$fmt_spec" + --merge-output-format mp4 + --embed-metadata --embed-chapters + --embed-subs --sub-langs all + --embed-thumbnail + --no-overwrites + -o "$OUT_DIR/%(title)s.%(ext)s" + "${EXTRA_ARGS[@]}" "$URL") -yt-dlp \ - -f "$format" \ - --merge-output-format mp4 \ - --embed-thumbnail \ - --add-metadata \ - -o "$HOME/Videos/%(title)s.%(ext)s" \ - "$url" +if [ "$DRY_RUN" -eq 1 ]; then + echo "yt-dlp ${args[*]:1}" +else + spawn "downloading video → $OUT_DIR" "${args[@]}" +fi diff --git a/completions/pos.bash b/completions/pos.bash index f18f84f..872e984 100644 --- a/completions/pos.bash +++ b/completions/pos.bash @@ -7,6 +7,8 @@ _pos_flags[ai-gemini]="--model --session --system" _pos_flags[communication-telegram-listener]="--enable --disable --status --sync-commands --run" _pos_flags[communication-telegram-sender]="--type --caption --parse-mode --no-preview --token --chat-id --markdown" _pos_flags[entertainment-send]="--print --markdown" +_pos_flags[media-mp3]="--output --no-playlist --cookies --by-artist --dry-run" +_pos_flags[media-mp4]="--format --best --worst --output --no-playlist --cookies --dry-run" _pos_flags[network-hotspot]="--foreground" _pos_flags[system-backup]="--service" _pos_flags[tree]="--depth"