feat: pos media sync — incremental Music→USB sync (mp3/mp4) + shared lib/usb-lib.sh
gates / consistency-and-conventions (push) Successful in 46s

This commit is contained in:
he
2026-08-14 18:02:03 -04:00
parent 8026300005
commit 16bda822ff
13 changed files with 451 additions and 192 deletions
+20 -15
View File
@@ -10,19 +10,19 @@
<!-- GEN:START docmap -->
| ## 1. Project Overview | 2843 |
| ## 2. Directory Structure | 44193 |
| ## 3. Installation Flow | 194245 |
| ## 4. The `pos` CLI System | 246316 |
| ## 5. Shared Library — `lib/common.sh` | 317348 |
| ## 6. Docker Compose / ScaleTail | 349391 |
| ## 7. Optional Apps (`apps/`) | 392421 |
| ## 8. Entertainment Module | 422435 |
| ## 9. Systemd Services | 436447 |
| ## 10. Configuration Files | 448474 |
| ## 11. Coding Conventions | 475507 |
| ## 12. Development Workflow | 508560 |
| ## 13. Key File Quick Reference | 561621 |
| ## 14. Common Tasks for Agents | 622652 |
| ## 2. Directory Structure | 44194 |
| ## 3. Installation Flow | 195246 |
| ## 4. The `pos` CLI System | 247318 |
| ## 5. Shared Library — `lib/common.sh` | 319350 |
| ## 6. Docker Compose / ScaleTail | 351393 |
| ## 7. Optional Apps (`apps/`) | 394423 |
| ## 8. Entertainment Module | 424437 |
| ## 9. Systemd Services | 438449 |
| ## 10. Configuration Files | 450476 |
| ## 11. Coding Conventions | 477509 |
| ## 12. Development Workflow | 510562 |
| ## 13. Key File Quick Reference | 563625 |
| ## 14. Common Tasks for Agents | 626657 |
<!-- GEN:END docmap -->
## 1. Project Overview
@@ -78,6 +78,7 @@ Linux_post_install/
│ ├── 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 (smart/interactive format select)
│ ├── pos-media-sync # Incremental Music → USB sync (mp3/mp4, add/update only)
│ ├── pos-network-checkport # Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view
│ ├── pos-network-download # aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits)
│ ├── pos-network-hotspot # Wi-Fi hotspot via create_ap + wihotspot-gui
@@ -279,6 +280,7 @@ All non-interactive `pos` commands log output to `~/.local/share/linux_post_inst
| 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 (smart/interactive format select) |
| media | sync | `pos-media-sync` | Incremental Music → USB sync (mp3/mp4, add/update only) |
| network | checkport | `pos-network-checkport` | Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view |
| network | download | `pos-network-download` | aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits) |
| network | hotspot | `pos-network-hotspot` | Wi-Fi hotspot via create_ap + wihotspot-gui |
@@ -572,6 +574,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
| `lib/entertainment-plugin-lib.sh` | 67 | Message-safe helpers for plugins (config load, require, fetch+retry) — plugins MAY source it |
| `lib/scheduler-lib.sh` | 760 | Scheduler lib (job parsing, notify policies, per-job user timers via user-timers-lib, legacy migrate) |
| `lib/user-timers-lib.sh` | 112 | Shared systemd **user** timer machinery (interval→OnCalendar, unit pair writer, linger) |
| `lib/usb-lib.sh` | 194 | Shared USB-storage detection + pick flow (detect/mount-offer/`usb_pick_root`) — used by `pos system backup` + `pos media sync` |
| `bin/flag-reader` | 58 | Inspect flags (list/status/`--raw`) |
| `bin/flag-set` | 21 | Set a flag (optionally with a value) |
| `bin/flag-clear` | 21 | Unset a flag |
@@ -596,6 +599,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
| `bin/pos-entertainment-status` | 62 | Show enabled plugins and scheduler state |
| `bin/pos-media-mp3` | 86 | Download audio as MP3 (yt-dlp) |
| `bin/pos-media-mp4` | 132 | Download video as MP4 (smart/interactive format select) |
| `bin/pos-media-sync` | 138 | Incremental Music → USB sync (mp3/mp4, add/update only) |
| `bin/pos-network-checkport` | 496 | Check TCP/UDP port reachability (nmap, or bash/nc fallback) + local interface view |
| `bin/pos-network-download` | 951 | aria2 RPC daemon + queue control (add/torrent/metalink, watch, limits) |
| `bin/pos-network-hotspot` | 93 | Wi-Fi hotspot via create_ap + wihotspot-gui |
@@ -607,13 +611,13 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
| `bin/pos-share-smb-server` | 253 | Manage the Samba server (status, share/unshare exports, users, enable/disable) |
| `bin/pos-share-usb-server` | 218 | USB Redirector server control (--ls, --share; prompts when args omitted) |
| `bin/pos-ssh-load-keys` | 31 | Load all SSH keys into the agent |
| `bin/pos-system-backup` | 364 | Encrypted (AES-256) folder snapshots (tar + gpg) |
| `bin/pos-system-backup` | 216 | Encrypted (AES-256) folder snapshots (tar + gpg) |
| `bin/pos-system-firewall` | 308 | Interactive UFW management |
| `bin/pos-system-health` | 209 | Host health dashboard (disk, RAM, services, backup age, fail2ban, docker); exit 1 if any FAIL |
| `bin/pos-system-schedule` | 81 | Scheduled jobs: run a command on a timer; notify on threshold/change/error/always or silently |
| `bin/pos-config` | 80 | Interactive editor for the tools' runtime config (reads # POS_CONFIG: registry) |
| `bin/pos-tree` | 112 | Show the pos CLI command tree: categories, commands, and subcommands |
| `completions/pos.bash` | 292 | Dynamic bash completion |
| `completions/pos.bash` | 293 | Dynamic bash completion |
<!-- GEN:END filetable -->
| `apps/install.sh` | 171 | App install/uninstall picker/orchestrator |
@@ -643,6 +647,7 @@ Use conventional prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`
| Modify NFS share logic | Edit `bin/pos-share-nfs-server` / `bin/pos-share-nfs-client` |
| Modify SMB share logic | Edit `bin/pos-share-smb-server` / `bin/pos-share-smb-client` |
| Modify scrcpy mirroring logic | Edit `bin/pos-communication-scrcpy` (config scope `scrcpy` via `pos config scrcpy`; `SCRCPY_*` keys in `~/.config/linux_post_install/scrcpy.env`) |
| Modify Music→USB sync logic | Edit `bin/pos-media-sync` / shared USB layer `lib/usb-lib.sh` (seams `MEDIA_SYNC_SOURCE`/`MEDIA_SYNC_DEST`/`USB_MOUNT_BASE`/`USB_BYID` in `~/.config/linux_post_install/system.env`) |
| Modify the scheduler / scheduled jobs | Edit `bin/pos-system-schedule` / `lib/scheduler-lib.sh` (jobs in `~/.config/linux_post_install/schedule.d/`) |
| Modify AI/Gemini logic | Edit `bin/pos-ai-gemini` (config scope `ai` via `pos config ai`; `AI_GEMINI_API_KEY`/`AI_GEMINI_MODEL` in `~/.config/linux_post_install/ai.env`) |
| Modify UFW/firewall logic | Edit `bin/pos-system-firewall` |
+2 -2
View File
@@ -154,7 +154,7 @@ Two kinds of config, don't mix them up:
- **Machine defaults shipped by the installer:** place the file in `config/` and add copy logic to `postinstall.sh`. If it contains secrets, add to `.gitignore` and document in `DOC/`.
- **Runtime tool config set by the user:** `~/.config/linux_post_install/<tool>.env` with `chmod 600`. Load it with env-var precedence (flags > environment > file). Patterns: `pos-docker-compose` (`compose.env`), `pos-communication-telegram-sender` (`telegram.env`, edited via `pos config telegram` — token masked), and the shared ones below. Never store tokens in the repo.
- `system.env` — shared "system" settings loaded by `pos-system-*` tools via `load_system_env()` in `lib/common.sh` (currently `BACKUP_SERVICE_ROOTS`, `BACKUP_USB_ROOT`, `BACKUP_MOUNT_BASE`, `BACKUP_USB_BYID`, `HEALTH_BACKUP_MAX_AGE_DAYS`). Env already exported wins over the file.
- `system.env` — shared "system" settings loaded via `load_system_env()` in `lib/common.sh` (currently `BACKUP_SERVICE_ROOTS`, `BACKUP_USB_ROOT`, `BACKUP_MOUNT_BASE`, `BACKUP_USB_BYID`, `HEALTH_BACKUP_MAX_AGE_DAYS`, plus `USB_MOUNT_BASE`/`USB_BYID`/`MEDIA_SYNC_SOURCE`/`MEDIA_SYNC_DEST` for `pos media sync`). Env already exported wins over the file.
- `notify.env` — alerting platform selection (`NOTIFY_PLATFORM=telegram,matrix`), read by `lib/notify.sh`.
### 5. Add SSH keys (if needed)
@@ -193,7 +193,7 @@ make lint # convention gate (scripts/lint-conventions.sh)
`make check` only proves syntax, exec bits, doc sync and dispatch — not behaviour. For tools that need `sudo`, systemd, or binaries absent from the dev box (samba, usbsrv, …), test them end-to-end with two patterns:
- **Env-overridable paths.** Anything that touches a system config location gets an env override whose default is the real path — the seam that lets the tool be exercised against temp files. Precedents: `FLAGS_DIR` (`lib/flags.sh`), `SMB_CONF` (`bin/pos-share-smb-server`, default `/etc/samba/smb.conf`), `SMB_CREDS_DIR`/`UNIT_DIR` (`bin/pos-share-smb-client`), `USER_SYSTEMD_DIR` (`bin/pos-network-download`, `bin/pos-communication-{telegram,matrix}-listener`, `lib/scheduler-lib.sh` — write it as `${USER_SYSTEMD_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user}`), and the scheduler's `SCHEDULE_DIR`/`SCHEDULE_STATE_DIR`/`SCHEDULE_LOG_DIR`/`SCHED_LEGACY_ENV` (`lib/scheduler-lib.sh`). Pick a short tool-specific name and don't advertise it in `usage()` — it's a test seam, not user-facing. **Gotcha (session-learned):** a `VAR="${XDG…:-…}"` without the leading `VAR:-` *overrides* the seam — the stub run then silently writes to the real `$HOME` path and every assertion passes while the bug hides. The override must be written first, then tested with `VAR=/tmp/x …` and a check that the real path is untouched.
- **Env-overridable paths.** Anything that touches a system config location gets an env override whose default is the real path — the seam that lets the tool be exercised against temp files. Precedents: `FLAGS_DIR` (`lib/flags.sh`), `SMB_CONF` (`bin/pos-share-smb-server`, default `/etc/samba/smb.conf`), `SMB_CREDS_DIR`/`UNIT_DIR` (`bin/pos-share-smb-client`), `USER_SYSTEMD_DIR` (`bin/pos-network-download`, `bin/pos-communication-{telegram,matrix}-listener`, `lib/scheduler-lib.sh` — write it as `${USER_SYSTEMD_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user}`), the scheduler's `SCHEDULE_DIR`/`SCHEDULE_STATE_DIR`/`SCHEDULE_LOG_DIR`/`SCHED_LEGACY_ENV` (`lib/scheduler-lib.sh`), and the USB layer shared by `pos system backup` + `pos media sync`: `USB_MOUNT_BASE` (default `/media`, keeps `BACKUP_MOUNT_BASE` as an alias) and `USB_BYID` (default `/dev/disk/by-id`, keeps `BACKUP_USB_BYID` as an alias) — both guarded as `VAR="${VAR:-${BACKUP_…:-default}}"` in `lib/usb-lib.sh` so existing config lines keep working. Pick a short tool-specific name and don't advertise it in `usage()` — it's a test seam, not user-facing. **Gotcha (session-learned):** a `VAR="${XDG…:-…}"` without the leading `VAR:-` *overrides* the seam — the stub run then silently writes to the real `$HOME` path and every assertion passes while the bug hides. The override must be written first, then tested with `VAR=/tmp/x …` and a check that the real path is untouched.
- **Stub PATH.** Create a temp dir with fake binaries, then run the tool with `PATH="$stubs:$PATH"`: fake `sudo` → `exec "$@"`; fake `systemctl`/`smbcontrol`/`mount.cifs` → echo their args; fake `testparm` → `cat` the file back (so validation passes); fake `systemd-escape` → print a fixed name. Assert on output **and** exit codes — happy path plus each failure path (`err` sets rc=1).
- **Interactive prompts** (`read … </dev/tty`): drive them with a PTY — `printf 'answer\n' | script -qec "cmd" /dev/null` — then assert the side effect (e.g. the chmod-600 creds file lands with the right mode).
+1
View File
@@ -206,6 +206,7 @@ The standalone `vbox` command still works and forwards to `pos docker vbox` (see
|---------|------|---------|---------------|
| `pos media mp3 <url>` | `bin/pos-media-mp3` | Download audio as MP3 via yt-dlp, with thumbnail + metadata | Output to `~/Music/%(title)s.%(ext)s`, `--audio-quality 0` |
| `pos media mp4 <url>` | `bin/pos-media-mp4` | Download video via yt-dlp with **interactive format selection** | Lists formats (`yt-dlp -F`), asks for a format ID, saves to `~/Videos/` |
| `pos media sync [--mp3\|--mp4]` | `bin/pos-media-sync` | Incremental Music → USB sync (add/update only — never deletes) | Copies mp3/mp4 from `$HOME/Music` (or `--source <dir>`) into `<usb>/Music/`, preserving the tree; missing or changed (size/mtime) files are copied, identical ones skipped. Same USB detection as `pos system backup` (lsblk TRAN + lsusb/by-id, mount offer for unmounted sticks, multi-stick picker). `--mp3`/`--mp4` filter by extension, neither = both; `--dry-run` previews. Config: `MEDIA_SYNC_SOURCE`, `MEDIA_SYNC_DEST`, shared `USB_MOUNT_BASE`/`USB_BYID` from `~/.config/linux_post_install/system.env`. Result notified via `lib/notify.sh` |
### system
+8 -1
View File
@@ -38,7 +38,7 @@ The phases:
| # | Phase | Script/action |
|---|-------|----------------|
| 1 | preinstall | `preinstall.sh` — apt packages + yt-dlp |
| 2 | scripts | Copies `bin/*``/usr/local/bin/` (755), `lib/common.sh` + `lib/flags.sh` + `lib/notify.sh` + `lib/entertainment-lib.sh` + `lib/entertainment-plugin-lib.sh` + `lib/scheduler-lib.sh` + `lib/config-ui.sh` + `lib/user-timers-lib.sh``/usr/local/bin/` (644). Copies precompiled arch binaries from `x64_bin/` (or `arm64_bin/`) → `/usr/local/bin/`. With `--feature`: also installs `features/*` (see below) |
| 2 | scripts | Copies `bin/*``/usr/local/bin/` (755), `lib/common.sh` + `lib/flags.sh` + `lib/notify.sh` + `lib/entertainment-lib.sh` + `lib/entertainment-plugin-lib.sh` + `lib/scheduler-lib.sh` + `lib/config-ui.sh` + `lib/user-timers-lib.sh` + `lib/usb-lib.sh``/usr/local/bin/` (644). Copies precompiled arch binaries from `x64_bin/` (or `arm64_bin/`) → `/usr/local/bin/`. With `--feature`: also installs `features/*` (see below) |
| 3 | postinstall | `postinstall.sh` — PATH, completion, SSH keys, systemd |
| 4 | scalepoint | Shallow-clones ScaleTail templates to `/usr/local/share/linux_post_install/scale-tail` |
| 5 (opt) | apps | `apps/install.sh` when `--apps` (interactive) or `--full` (all, non-interactive) |
@@ -207,6 +207,13 @@ Sourced by `bin/pos-entertainment-send|config|enable|disable|status` (after `lib
---
## lib/usb-lib.sh — shared USB-storage detection
**File:** `lib/usb-lib.sh` (installed to `/usr/local/bin/usb-lib.sh`)
**Purpose:** the one copy of the USB-storage machinery shared by `pos system backup`'s post-verify USB copy and `pos media sync``usb_detect` (lsblk JSON, TRAN + lsusb/by-id cross-check → `USB_MOUNTED`/`USB_UNMOUNTED`), `usb_related_present`, `usb_mount_offer` (mount an unmounted stick at `/media/<label>`, `usb-automount` scheme), and `usb_pick_root` (detect → mount-offer → single/multi picker → `USB_ROOT`). Defines only `usb_*`; seams `USB_MOUNT_BASE` (default `/media`, alias `BACKUP_MOUNT_BASE`) and `USB_BYID` (default `/dev/disk/by-id`, alias `BACKUP_USB_BYID`) keep existing config lines working.
---
## features/autostart.sh — boot-time feature
**File:** `features/autostart.sh` (installed to `/usr/local/bin/autostart.sh` by `./install.sh --feature`)
+52 -1
View File
@@ -1,11 +1,13 @@
# How-To: `pos media`
Download audio and video from the web via `yt-dlp`. Tools: `mp3`, `mp4`.
Download audio and video from the web via `yt-dlp`, and sync your library to a
USB stick. Tools: `mp3`, `mp4`, `sync`.
| Tool | What it does |
|------|--------------|
| `pos media mp3` | Download audio, convert to MP3 |
| `pos media mp4` | Download video with smart/interactive format selection |
| `pos media sync` | Incrementally copy `~/Music` onto a USB stick (mp3/mp4) |
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
@@ -84,6 +86,55 @@ video+audio and merges them.
---
## `pos media sync` — music onto a USB stick
```bash
pos media sync # copy everything (mp3 + mp4) from ~/Music to the stick
pos media sync --mp3 # only the .mp3 files
pos media sync --mp4 # only the .mp4 files
```
Detects connected USB storage exactly like `pos system backup` (same shared
`lib/usb-lib.sh`): a plugged-in but unmounted stick is offered a mount first
(`/media/<label>`, world-writable, mirrors `usb-automount`), multiple sticks
are listed for you to pick, and if nothing is plugged in it re-scans after you
press Enter. Files are mirrored into `<usb>/Music/` (change with
`MEDIA_SYNC_DEST`) preserving the artist/album tree.
**Sync semantics — add/update only, never delete.** Files missing on the stick
are copied; files whose size or mtime changed are overwritten; everything
identical is skipped. Files on the stick that are no longer in the source are
**left alone** — a playback stick can never lose files to a mirror mistake.
Copies keep the source timestamps (`cp --preserve=timestamps`), so a re-run is
a no-op. Preview before copying with `--dry-run`:
```bash
pos media sync --mp4 --dry-run # shows "would copy" list + counts, copies nothing
```
| Flag | Meaning |
|------|---------|
| `--mp3` | Sync only `*.mp3` (neither flag = both) |
| `--mp4` | Sync only `*.mp4` (neither flag = both) |
| `--source <dir>` | Source folder (default `~/Music`) |
| `--dry-run` | Preview what would be copied, copy nothing |
When it finishes it announces the result via `lib/notify.sh`
(`Music sync completed: N added, M updated → <usb>/Music`).
**Recipes:**
```bash
pos media sync # keep the car stick up to date (both formats)
pos media sync --mp3 --dry-run # check what a new batch will bring first
pos media sync --source /data/Music # sync a library that lives elsewhere
```
Config (all in `~/.config/linux_post_install/system.env` or exported):
`MEDIA_SYNC_SOURCE` (default `$HOME/Music`), `MEDIA_SYNC_DEST` (default
`Music`), plus the shared `USB_MOUNT_BASE` / `USB_BYID` seams.
---
## Troubleshooting
- Format list is empty / download fails → the site or age-gate requires