7.2 KiB
pos media ytsync — implementation research notes
Internal reference for maintainers touching bin/pos-media-ytsync. The user-facing
docs live in DOC/POS.md → media and DOC/howto/media.md; the behavioral contract
is reportAgents/2026-08-22-designer-ytsync.md + the Architect decisions D1–D9 in
reportAgents/2026-08-22-architect-ytsync.md.
Probe mechanics
One fast call per source per run:
yt-dlp --flat-playlist -J --no-warnings -- <url>
--flat-playlistkeeps entries as stubs, so cost is 1–2 HTTP round trips regardless of library size (seconds even for 2000-video channels).-Jdumps a single JSON object. Parsed withjq:- display name:
.channel // .uploader // .uploader_id // .title - owner (for playlist subdirs):
.channel // .uploader // .uploader_id— no title fallback (avoidsPlaylist Title/Playlist Titledoubling when a playlist probe has no channel fields) - key (slug source):
.uploader_id // .channel_id // .id - entries:
.entries[] | (.id) + "\x1f" + (.title); a?v=URL returns a bare video object with no.entries, handled as a one-entry list.
- display name:
- The new-list is computed BEFORE any download by diffing entry ids against
archive/<slug>.txt— exact[n/N]counts, exact dry-run plans, zero speculative downloads. - Sign-in-skipped bucket: entries with an empty id OR titles starting with
[Private/[Deleted/[Unavailable. Reported as "N videos require sign-in — skipped"; never counted as failures.
Why probes don't use spawn()
lib/common.sh spawn() exits the process on failure and captures output until
completion. ytsync needs recoverable probes (interactive add re-prompts ≤3×; sync
must continue other sources after one bad probe) and never wraps the download batch
(hours of silence behind a spinner). _probe_run mirrors spawn's OK/FAIL line UX but
returns rc, and its braille spinner frames are emitted only on a TTY so no \r
bytes ever reach the dispatcher's tee'd logs.
Classification (URL shape, deterministic)
| URL contains | Type | Behavior |
|---|---|---|
youtu.be/<id> path (with or without list=) |
video |
short links name one video — treated exactly like v= |
v= present (with or without list=) |
video |
single video, downloaded with --no-playlist; nobody backfills a 500-video playlist by pasting a watch link |
list= without any video-naming part |
playlist |
tracked source; files land as <videos>/<owner>/<playlist>/<NNN> - <title>.<ext> |
| neither | channel |
flat source; <videos>/<channel>/<title>.<ext> |
Download invocation (one yt-dlp call PER NEW VIDEO)
yt-dlp -f "bestvideo*+bestaudio/best"
--merge-output-format mp4 # parity with pos media mp4
--embed-metadata --embed-chapters --embed-thumbnail
--convert-thumbnails jpg
--no-overwrites # parity; collisions become "exists, kept" warnings
--download-archive <state>/archive/<slug>.txt # crash-safe per-video recording
--windows-filenames # USB/Samba/TV-safe names
--trim-filenames 120 # headroom for the NNN prefix under 255-byte limits
--retries 3 --fragment-retries 3
[--no-playlist] # only for type=video sources
${YTSYNC_EXTRA_ARGS} # appended last — user override hatch
-o "<template>" "https://www.youtube.com/watch?v=<id>"
- Templates are COMPUTED from stored registry fields (
subdir), never from yt-dlp placeholders like%(playlist_title)s— a stored subdir cannot be NA and cannot drift mid-library. - Playlist template gets a LITERAL zero-padded index injected by the tool (from the
probe's entry position), because standalone watch URLs have no live
%(playlist_index)s. Caveat: inserting a video mid-playlist shifts FUTURE numbering; existing files are never renamed. - Deliberate divergences from mp4: no
--embed-subs --sub-langs all(library bloat; re-add per-source viaYTSYNC_EXTRA_ARGS, appended-last wins);--quiet --no-warningsplus--progressonly on a TTY (yt-dlp auto-simplifies progress when piped, keeping\rout of logs). - Sequential downloads; batching is the listed future optimization (accepted v1 cost: one extra extraction round trip per video).
State layout (machine-owned, outside ~/Videos)
${YTSYNC_STATE_DIR:-~/.local/share/linux_post_install/ytsync}/
├── registry # \x1f-delimited: slug⇥type⇥url⇥subdir⇥playlist_title⇥added_ts
├── archive/<slug>.txt # native yt-dlp archive format ("<extractor> <id>"), one per source
└── history.log # append-only: "<YYYY-MM-DD HH:MM> · <name> · N new · M skipped · K failed"
slug:[a-z0-9][a-z0-9_-]*, derived from probed uploader_id/handle; numeric-2suffix on collision, checking registry slugs only.- Registry/archive writes are atomic (mktemp inside the state dir + mv).
removedrops only the registry line. Keeping the orphaned archive makes a future re-add of the same source an incremental resume instead of a full re-download that would collide with existing files under--no-overwrites.- History grammar note: a wholesale probe failure appends
<date> · <name> · FAILED (probe)instead of the numeric triple — the pass did not complete, and faking zeros would hide it fromgrep.
Edge cases
- Mid-playlist inserts shift future numbering only (see above).
- Title renames on YouTube never rename local files — the archive is keyed by video id; local files are immutable once written.
- Same-title collision (
--no-overwritesrefusal): stderr is matched for "already been downloaded" →[!] exists, kept: <file>, counted into the "already present" tally of the summary (the fixed summary grammar has three buckets; tools-docs records this folding). - Disk full (ENOSPC): stderr matched for "No space left on device"/Errno 28 → prominent warning, that source stops mid-run (unfetched videos simply stay "new" next run since the archive was untouched), other channels continue.
- Per-video generic failure:
[!] unavailable: <title>+ first 3 stderr lines, continue, counted as failed. Per-video failures never flip the exit code; rc 1 is reserved for missing deps, invalid explicit URL, unknown/ambiguous<name>, or ≥1 requested source failing wholesale during sync. - Slug stability: the slug derives from the probed
uploader_id. If YouTube ever changes that id for a channel, a re-add computes a different slug whose archive starts empty → existing files would be "re-downloaded", hitting--no-overwritesand spammingexists, keptwarnings. Manual mitigation if it ever bites: rename the oldarchive/<old-slug>.txtto the new slug before syncing. Accepted v1 limitation (Architect §Remaining uncertainty #4).
Testing seams
YTSYNC_STATE_DIR,YTSYNC_VIDEOS_DIR— every written path honors them (writtenVAR="${VAR:-default}"). Fakeyt-dlparrives via stub PATH emitting canned-JJSON; fake notify via a stubbed sender appending tosends.log.- Interactive reads all come from
/dev/tty(NOT stdin), so the tool is NOT inbin/pos's INTERACTIVE_CMDS and dispatched runs keep full tee logging; non-tty interactive entry points print the guard line and exit 0.