<!--
  Public educational copy — Division Agent skill pack.
  Scrubbed for public download: no personal emails, Drive folder IDs, or private paths.
  Status labels such as Working Draft refer to the draft→approved lifecycle, not a site watermark.
-->
---
name: video-dl
description: Download videos from YouTube, Instagram and Threads (Threads needs no login) and also TikTok, Facebook, X and most other video sites, to a local file. Use when the user shares a video/post link and asks to download, save, grab or keep it, wants just the audio/mp3, or says "dl this", "download this reel", "save this Threads video", "下載呢條片", "幫我download條片".
---

# Video Download

One script, `scripts/vdl.py`. Run it from this skill's folder with `python3` on Linux/macOS, `python` or `py -3` on Windows. Python 3.9+, standard library only. Every command prints **one JSON object**; read `success`, `error` and `next`.

```bash
python3 scripts/vdl.py check                              # first use on a machine
python3 scripts/vdl.py get "URL" [--out DIR] [--res 1080] [--compat] [--best] [--audio-only]   # Threads too
python3 scripts/vdl.py media MEDIA_URL... --page-url "POST_URL" [--uploader H --title T --id ID]
```

On success, give the user the `path`, `size_mb` and `media` line (codec, resolution, duration) from `files`. Pass on any `warnings`.

## Setup (first use)

Run `check`. If `missing` lists anything, install it with the command shown there, if you are allowed to install packages. Otherwise tell the user what is missing.

- **yt-dlp** is required for `get` on every site except Threads. Keep it current, because sites break old versions often: `python3 -m pip install -U "yt-dlp[default]"`.
- **ffmpeg/ffprobe** are needed to merge video and audio, make mp3s, and verify files. Without them, `get` falls back to lower-quality single files and `media` can't merge tracks.
- **deno or Node.js** lets yt-dlp unlock all YouTube formats.

Files go to `~/video-downloads/<site>/` (`youtube`, `instagram`, `threads`, …). Override this per call with `--out DIR`, or for the whole machine with `VIDEO_DL_OUTPUT_DIR`. If you work in a shared or synced workspace folder, save there so the user can reach the file.

## Which route

| Link | Route |
|---|---|
| YouTube (watch, shorts, youtu.be) | `get` |
| Instagram (reel, post, tv) | `get`. If it returns `auth_required`, see **Login** |
| **Threads** (threads.com / threads.net) | `get`, which needs no login and no yt-dlp. It reads the post's own data and picks the best video and audio. Only an audience-restricted post returns `auth_required`; for that, use the **browser capture** in a logged-in browser |
| Anything else | `get`, since yt-dlp supports ~1,800 sites |

## Results and what to do next

| Exit | `error` | Meaning | Do next |
|---|---|---|---|
| 0 | — | Downloaded and verified | Report the path(s) |
| 1 | `failed` | Real failure: dead link, removed video, bad format | Report the message. Don't retry with cookies; they won't help |
| 2 | `auth_required` | The site wants a logged-in session, YouTube's "confirm you're not a bot", or an audience-restricted/private Threads post | **Login** below, or browser capture (Threads: browser capture only) |
| 3 | `expired` | A media URL went stale (signed CDN links last minutes to hours) | `get`: run it again. `media`: capture again, then run `media` straight away |
| 4 | `no_extractor` | yt-dlp has no reader for this site | Browser capture → `media` |
| 5 | `missing_dependency` | yt-dlp or ffmpeg is not installed | Install from `install`, or tell the user |

## Options for `get`

**Default output: 1080p, H.264/AAC, `.mp4`.** That plays on every phone and editor. If a video has no 1080p version, the next size down is used; a video only available above 1080p takes the smallest size above. 1080p is measured on the shorter side, so a vertical 1080×1920 reel counts as 1080p.

- `--res 720`: a lower cap for smaller files. `--res 0` means no cap.
- `--best`: best codec (VP9/AV1) at the chosen resolution. Add `--res 0` for 4K/8K. Files are still `.mp4`, but some editors won't open VP9/AV1.
- `--compat`: some sites have H.264 only at a lower resolution. An Instagram reel can be VP9 at 1080p but H.264 only at 720p; by default you get the VP9 1080p file, and a warning says so. If the user edits in CapCut, QuickTime or Premiere, add `--compat`: it re-encodes to H.264/AAC 1080p, with a small quality loss and some extra time. `--res 720` gets the site's own H.264 instead.
- **Threads**: most qualities are VP9, so the default gives VP9 1080p with a warning. Add `--compat` for editors. A carousel with several videos saves each one as `[CODE_1]`, `[CODE_2]`, …. A text post that embeds an Instagram reel downloads that reel, and a warning names its creator.
- `--audio-only`: mp3 instead of video (needs ffmpeg).
- `--out DIR`: the output folder.
- Downloading the same link again into the same folder is skipped, and the existing file is returned.

## Login (only when `auth_required`)

Cookies are the user's account credentials. Treat them that way:

- **`--cookies-from-browser firefox|chrome|edge|safari`**: only when this script runs **on the user's own computer**, in a browser that is logged in to the site. Firefox is the most reliable. Recent Chrome on Windows often can't be read.
- **`--cookies FILE`**: a Netscape-format `cookies.txt` that **the user** exported and put on this machine themselves. Ask them for the file path. Never ask them to paste cookie values into the chat.
- Never print, copy, upload or commit a cookie file or its contents. Delete it when the user says they're done.
- No cookies available? Try the browser capture route: a logged-in browser tab already has the media URL.

## Browser capture (restricted Threads posts, or Instagram without cookies)

Read `references/browser-capture.md`. Briefly:

1. Open the post in a browser you can control, with the network log recording.
2. Work out which `<video>` on the page belongs to the requested post. A Threads page also shows the parent post and replies.
3. Copy that video's media URLs from the network log: usually **two**, a video-only track and an audio-only track.
4. Write them to a file and run `media --url-file urls.txt --page-url POST_URL`. The script strips byte ranges, merges the tracks and verifies the result.

If you have no browser tool, tell the user the post is restricted and can only be fetched through their logged-in browser.

## Do not

- Do not download private or paid content the user has no access to, and do not try to get around DRM. yt-dlp cannot decrypt DRM-protected streams (e.g. Netflix) and this skill won't try.
- Do not loop retries on `auth_required` or `failed`. Retrying makes rate limits worse.
- Do not hand-edit captured URLs or download them with curl/wget. Ranged URLs give a silently truncated video. Always use `media`.
- Remind the user that downloaded videos belong to their creators: fine for personal use, reference and editing practice, but re-uploading needs the owner's permission.
