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:
@@ -16,6 +16,8 @@ summary (newest last).
|
|||||||
|
|
||||||
## Done
|
## 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** — 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.
|
- **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.
|
||||||
|
|||||||
@@ -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-send # Run a public-API plugin and send its output via Telegram (default sender)
|
||||||
│ ├── pos-entertainment-status # Show enabled plugins and scheduler state
|
│ ├── pos-entertainment-status # Show enabled plugins and scheduler state
|
||||||
│ ├── pos-media-mp3 # Download audio as MP3 (yt-dlp)
|
│ ├── 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-checkport # Check TCP port connectivity
|
||||||
│ ├── pos-network-hotspot # Wi-Fi hotspot via create_ap + wihotspot-gui
|
│ ├── pos-network-hotspot # Wi-Fi hotspot via create_ap + wihotspot-gui
|
||||||
│ ├── pos-network-ip # Show interfaces, routes, public IP + location
|
│ ├── 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 | 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 |
|
| entertainment | status | `pos-entertainment-status` | Show enabled plugins and scheduler state |
|
||||||
| media | mp3 | `pos-media-mp3` | Download audio as MP3 (yt-dlp) |
|
| 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 | checkport | `pos-network-checkport` | Check TCP port connectivity |
|
||||||
| network | hotspot | `pos-network-hotspot` | Wi-Fi hotspot via create_ap + wihotspot-gui |
|
| network | hotspot | `pos-network-hotspot` | Wi-Fi hotspot via create_ap + wihotspot-gui |
|
||||||
| network | ip | `pos-network-ip` | Show interfaces, routes, public IP + location |
|
| 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-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-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-entertainment-status` | 49 | Show enabled plugins and scheduler state |
|
||||||
| `bin/pos-media-mp3` | 35 | Download audio as MP3 (yt-dlp) |
|
| `bin/pos-media-mp3` | 80 | Download audio as MP3 (yt-dlp) |
|
||||||
| `bin/pos-media-mp4` | 38 | Download video as MP4 (interactive format select) |
|
| `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-checkport` | 45 | Check TCP port connectivity |
|
||||||
| `bin/pos-network-hotspot` | 93 | Wi-Fi hotspot via create_ap + wihotspot-gui |
|
| `bin/pos-network-hotspot` | 93 | Wi-Fi hotspot via create_ap + wihotspot-gui |
|
||||||
| `bin/pos-network-ip` | 69 | Show interfaces, routes, public IP + location |
|
| `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-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-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) |
|
| `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 -->
|
<!-- GEN:END filetable -->
|
||||||
| `apps/install.sh` | 171 | App install/uninstall picker/orchestrator |
|
| `apps/install.sh` | 171 | App install/uninstall picker/orchestrator |
|
||||||
|
|
||||||
|
|||||||
+58
-19
@@ -5,10 +5,11 @@ Download audio and video from the web via `yt-dlp`. Tools: `mp3`, `mp4`.
|
|||||||
| Tool | What it does |
|
| Tool | What it does |
|
||||||
|------|--------------|
|
|------|--------------|
|
||||||
| `pos media mp3` | Download audio, convert to MP3 |
|
| `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
|
Requires `yt-dlp` and `ffmpeg` (`sudo apt install yt-dlp ffmpeg`); the tools
|
||||||
error message instead of a raw `command not found` if it's missing.
|
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>
|
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
|
```bash
|
||||||
pos media mp3 https://youtube.com/watch?v=dQw4w9WgXcQ
|
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:
|
**Recipe:** batch — loop over a list of URLs:
|
||||||
```bash
|
```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
|
```bash
|
||||||
pos media mp4 <url>
|
pos media mp4 <url>
|
||||||
```
|
```
|
||||||
|
|
||||||
Lists the available formats (`yt-dlp -F`) and lets you pick interactively, then
|
Without a format flag, the available formats are shown as a **short curated
|
||||||
downloads to the current directory.
|
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
|
```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 +
|
Videos merge to MP4 with metadata, chapters, subtitles (all languages) and the
|
||||||
best audio; yt-dlp merges them (again needs `ffmpeg`).
|
thumbnail embedded (`--embed-metadata --embed-chapters --embed-subs
|
||||||
|
--sub-langs all --embed-thumbnail`); existing files are never overwritten.
|
||||||
|
|
||||||
**Troubleshooting:**
|
**Recipe:** grab a 4K stream for later — `--best` already picks the best
|
||||||
- Format list is empty → the site/age-gate requires cookies/auth; yt-dlp can't
|
video+audio and merges them.
|
||||||
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.
|
---
|
||||||
|
|
||||||
|
## 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).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+58
-13
@@ -1,35 +1,80 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
# POS: media mp3 — Download audio as MP3 (yt-dlp)
|
# 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"
|
source "$(dirname "$0")/../lib/common.sh" 2>/dev/null || source "$(dirname "$0")/common.sh"
|
||||||
|
|
||||||
|
OUT_DIR="$HOME/Music"
|
||||||
|
BY_ARTIST=0
|
||||||
|
EXTRA_ARGS=()
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
cat <<EOF
|
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.
|
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:
|
Examples:
|
||||||
pos media mp3 https://youtube.com/watch?v=dQw4w9WgXcQ
|
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
|
EOF
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
case "${1:-}" in
|
DRY_RUN=0
|
||||||
-h|--help|"") usage ;;
|
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
|
esac
|
||||||
|
done
|
||||||
|
[ -n "$URL" ] || usage
|
||||||
|
|
||||||
|
# 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 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 \
|
args=(yt-dlp -x --audio-format mp3 --audio-quality 0
|
||||||
-x \
|
--embed-metadata --embed-chapters
|
||||||
--audio-format mp3 \
|
--embed-thumbnail --convert-thumbnails jpg
|
||||||
--audio-quality 0 \
|
--parse-metadata "%(artist,uploader)s:%(artist)s"
|
||||||
--embed-thumbnail \
|
--no-overwrites
|
||||||
--convert-thumbnails jpg \
|
-o "$OUT_TEMPLATE"
|
||||||
--add-metadata \
|
"${EXTRA_ARGS[@]}" "$URL")
|
||||||
-o "$HOME/Music/%(title)s.%(ext)s" \
|
|
||||||
"$url"
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
|
echo "yt-dlp ${args[*]:1}"
|
||||||
|
else
|
||||||
|
spawn "downloading audio → $OUT_DIR" "${args[@]}"
|
||||||
|
fi
|
||||||
|
|||||||
+105
-17
@@ -1,38 +1,126 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
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"
|
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() {
|
usage() {
|
||||||
cat <<EOF
|
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:
|
Examples:
|
||||||
pos media mp4 https://youtube.com/watch?v=dQw4w9WgXcQ
|
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
|
EOF
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
case "${1:-}" in
|
DRY_RUN=0
|
||||||
-h|--help|"") usage ;;
|
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
|
esac
|
||||||
|
done
|
||||||
|
[ -n "$URL" ] || usage
|
||||||
|
|
||||||
|
[ -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"
|
||||||
|
|
||||||
|
# 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 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
|
||||||
|
|
||||||
url="$1"
|
# 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
|
||||||
|
|
||||||
yt-dlp -F "$url"
|
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")
|
||||||
|
|
||||||
echo
|
if [ "$DRY_RUN" -eq 1 ]; then
|
||||||
read -rp "Enter format ID: " format
|
echo "yt-dlp ${args[*]:1}"
|
||||||
|
else
|
||||||
yt-dlp \
|
spawn "downloading video → $OUT_DIR" "${args[@]}"
|
||||||
-f "$format" \
|
fi
|
||||||
--merge-output-format mp4 \
|
|
||||||
--embed-thumbnail \
|
|
||||||
--add-metadata \
|
|
||||||
-o "$HOME/Videos/%(title)s.%(ext)s" \
|
|
||||||
"$url"
|
|
||||||
|
|||||||
@@ -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-listener]="--enable --disable --status --sync-commands --run"
|
||||||
_pos_flags[communication-telegram-sender]="--type --caption --parse-mode --no-preview --token --chat-id --markdown"
|
_pos_flags[communication-telegram-sender]="--type --caption --parse-mode --no-preview --token --chat-id --markdown"
|
||||||
_pos_flags[entertainment-send]="--print --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[network-hotspot]="--foreground"
|
||||||
_pos_flags[system-backup]="--service"
|
_pos_flags[system-backup]="--service"
|
||||||
_pos_flags[tree]="--depth"
|
_pos_flags[tree]="--depth"
|
||||||
|
|||||||
Reference in New Issue
Block a user