feat: harden pos media mp3/mp4 — smart format selection, metadata, dry-run

Both tools: yt-dlp calls via spawn (honor DRY_RUN; --dry-run prints the
command and skips dep checks), -o/--output, --no-playlist, --cookies (with
file check), clean ffmpeg/yt-dlp guards, POS_FLAGS for completion, full
embedded metadata (--embed-metadata --embed-chapters --embed-thumbnail
--no-overwrites; mp3 adds --convert-thumbnails jpg + artist-from-uploader
parse-metadata). mp3 gains --by-artist (~/Music/<artist>/<title>.mp3).

mp4: -f <id>/--best/--worst skip the prompt (mutual-exclusion validated),
and the interactive picker now shows a curated [audio]/[video]/[combo]
format table on stderr (stdout carries only the chosen id — ui_pick
lesson), validates the id against the real -F output, and defaults empty
input to best. Docs: howto/media.md rewritten.
This commit is contained in:
Your Name
2026-08-09 15:56:47 +00:00
parent 0146dbe5f9
commit 2e57634877
6 changed files with 233 additions and 57 deletions
+2
View File
@@ -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/<artist>/<title>.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.
+5 -5
View File
@@ -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 |
+58 -19
View File
@@ -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).
---
+60 -15
View File
@@ -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
+106 -18
View File
@@ -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
+2
View File
@@ -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"