diff --git a/.changeset/stream-skills.md b/.changeset/stream-skills.md new file mode 100644 index 00000000..bf2faceb --- /dev/null +++ b/.changeset/stream-skills.md @@ -0,0 +1,5 @@ +--- +"@bunny.net/cli": patch +--- + +Add the `bunny stream` libraries, videos, captions, collections, and paid commands to the agent skill, and bring the import reference up to date diff --git a/packages/cli/src/commands/skills/content.ts b/packages/cli/src/commands/skills/content.ts index 5179fd23..c20b1e03 100644 --- a/packages/cli/src/commands/skills/content.ts +++ b/packages/cli/src/commands/skills/content.ts @@ -22,6 +22,9 @@ import storageMd from "@skills/bunny-cli/references/storage.md" with { import streamMd from "@skills/bunny-cli/references/stream.md" with { type: "text", }; +import streamImportMd from "@skills/bunny-cli/references/stream-import.md" with { + type: "text", +}; import skillMd from "@skills/bunny-cli/SKILL.md" with { type: "text" }; import type { ProjectSkill } from "@/core/agent-skill.ts"; @@ -37,7 +40,7 @@ This project uses bunny.net. Manage its resources with the \`bunny\` CLI: databa - When the CLI has no command for something, fall back to \`bunny api \` against api.bunny.net.`; /** The shipped bunny-cli skill, embedded at bundle time from skills/bunny-cli/. */ -// Experimental namespaces (apps, registries) stay out of the skill until they stabilize; stream import ships early, labelled experimental. +// Experimental namespaces (apps, registries) stay out of the skill until they stabilize. export const BUNNY_CLI_SKILL: ProjectSkill = { name: "bunny-cli", agentsSection: AGENTS_SECTION, @@ -53,5 +56,6 @@ export const BUNNY_CLI_SKILL: ProjectSkill = { "references/sites.md": sitesMd, "references/storage.md": storageMd, "references/stream.md": streamMd, + "references/stream-import.md": streamImportMd, }, }; diff --git a/skills/bunny-cli/SKILL.md b/skills/bunny-cli/SKILL.md index cb1d827d..ea41e01f 100644 --- a/skills/bunny-cli/SKILL.md +++ b/skills/bunny-cli/SKILL.md @@ -1,6 +1,6 @@ --- name: bunny-cli -description: Manage bunny.net resources from the command line (databases, DNS, Edge Storage, Edge Scripts, static sites, sandboxes, Stream video imports, authentication, and raw API requests) and query a Bunny Database from application code with `@bunny.net/database-client`. Use when working with bunny.net (pullzones, DNS zones/records, databases, Edge Storage zones and files, Edge Scripts, Magic Containers, static-site hosting/deploys, cloud sandboxes, moving a video library into Bunny Stream from Vimeo, S3, Wistia, Mux, Cloudflare Stream, JW Player, or Brightcove), invoking the `bunny` CLI, or making authenticated API calls to api.bunny.net. +description: Manage bunny.net resources from the command line (databases, DNS, Edge Storage, Edge Scripts, static sites, sandboxes, Stream video libraries and imports, authentication, and raw API requests) and query a Bunny Database from application code with `@bunny.net/database-client`. Use when working with bunny.net (pullzones, DNS zones/records, databases, Edge Storage zones and files, Edge Scripts, Magic Containers, static-site hosting/deploys, cloud sandboxes, Stream video libraries, uploads, collections, captions, and transcription, moving a video library into Bunny Stream from Vimeo, S3, Wistia, Mux, Cloudflare Stream, JW Player, or Brightcove), invoking the `bunny` CLI, or making authenticated API calls to api.bunny.net. --- # bunny.net CLI Skill @@ -68,7 +68,13 @@ bunny sites deploy ./dist # deploy a directory and p bunny sites domains add example.com --wait # custom production domain bunny sites deployments publish --previous --force # instant rollback -# import a video library into Bunny Stream (experimental) +# manage Bunny Stream video +bunny stream libraries create my-library --link --save-env --no-add-videos # link this directory, write .env +bunny stream videos upload ./video.mp4 --title "Launch demo" +bunny stream videos list + +# import a video library into Bunny Stream +bunny stream import list --source vimeo # browse folders first bunny stream import --library 12345 --source vimeo --dry-run # plan first bunny stream import --library 12345 --source vimeo --force # hand every video to Bunny bunny stream import status --library 12345 # encoding progress @@ -86,7 +92,8 @@ Use this to route to the correct reference file: - **Static sites (create, deploy, rollback, custom domains, GitHub Actions)** -> `references/sites.md` - **Edge Storage (zones, files, connection credentials, S3/FTP/HTTP, custom domains)** -> `references/storage.md` - **Sandboxes (create, exec, ssh, files list/cp, public URLs, persistent env vars, Claude Code auth)** -> `references/sandbox.md` -- **Import a video library into Bunny Stream (experimental: Vimeo, S3, Wistia, Mux, Cloudflare Stream, JW Player, Brightcove; dry run, resume, status)** -> `references/stream.md` +- **Bunny Stream (libraries, player/security settings, video upload/fetch/embed/cleanup, captions, collections, encoding, transcription, smart titles/chapters)** -> `references/stream.md` +- **Import a video library into Bunny Stream (Vimeo, S3, Wistia, Mux, Cloudflare Stream, JW Player, Brightcove; browse, dry run, resume, status)** -> `references/stream-import.md` - **Make raw API requests** -> `references/api.md` - **CLI doesn't have a command for it** -> use `bunny api` as a fallback (see `references/api.md`) diff --git a/skills/bunny-cli/references/stream-import.md b/skills/bunny-cli/references/stream-import.md new file mode 100644 index 00000000..79a1a19d --- /dev/null +++ b/skills/bunny-cli/references/stream-import.md @@ -0,0 +1,137 @@ +# Stream Import Commands + +`bunny stream import` moves an existing video library into a Bunny Stream library from **Vimeo**, **AWS S3** (or an S3-compatible provider), **Wistia**, **Mux**, **Cloudflare Stream**, **JW Player**, or **Brightcove**. Bunny fetches every file straight from the source by URL, so nothing is downloaded to the local machine. The command hands each video to Bunny, tags it, and returns once they are all queued; encoding then happens on Bunny's side. + +## Resolving the library + +`--library` (alias `--lib`) takes a library name or numeric ID. When omitted, it resolves in this order: + +1. `--library` +2. `.bunny/stream.json` (the directory's linked library, written by `bunny stream link`; see `stream.md`) +3. Interactive picker, which offers to link the directory (never on `--dry-run`) + +The library's own Stream API key is read from the account automatically and never printed. + +## Source credentials + +Credentials come from environment variables, never from flags: + +| Source | Variables | +| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `vimeo` | `VIMEO_ACCESS_TOKEN` (scopes `public`, `private`, `video_files`; downloads need a Standard plan or above) | +| `s3` | `S3_BUCKET`, `AWS_REGION` (or `AWS_DEFAULT_REGION`; default `us-east-1`); optional `S3_PREFIX`, `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `S3_PRESIGNED_URL_TTL`, `S3_ENDPOINT` (or `AWS_ENDPOINT_URL_S3`) | +| `wistia` | `WISTIA_ACCESS_TOKEN` | +| `mux` | `MUX_TOKEN_ID`, `MUX_TOKEN_SECRET` | +| `cloudflare` | `CLOUDFLARE_API_TOKEN` (Stream:Edit), `CLOUDFLARE_ACCOUNT_ID` | +| `jwplayer` | `JWPLAYER_API_KEY` (v2), `JWPLAYER_SITE_ID` | +| `brightcove` | `BRIGHTCOVE_CLIENT_ID`, `BRIGHTCOVE_CLIENT_SECRET` (CMS video read), `BRIGHTCOVE_ACCOUNT_ID` | + +`bunny stream import --help` lists the same variables. Interactively, the command prompts only for missing required values, then offers to save them for the active profile in `$XDG_CONFIG_HOME/bunnynet/stream-import-credentials.json` (default `~/.config/...`). Environment variables always win over saved values, and saved values are removed only when the source rejects them. Unattended runs (`--output json`, no TTY) fail naming the missing variables. + +With `--source` omitted, the command picks the one source whose credentials are all set, otherwise it prompts (or fails when unattended). + +S3 notes: leaving both AWS keys unset uses the default AWS credential chain; setting only one is an error. The IAM policy needs `s3:ListBucket` and `s3:GetObject`. Only video extensions are imported; `.ts` files only with `--include-ts` (or `S3_INCLUDE_TS=true`), since HLS buckets hold thousands of segments. + +## Typical workflows + +```bash +# 1. Browse the source: folders and their IDs, or every video +bunny stream import list --source vimeo +bunny stream import list --source vimeo --videos + +# 2. Look before importing: folder and video counts, what is already in Bunny +bunny stream import --library 12345 --source vimeo --dry-run + +# 3. Import (confirms first; --force skips the prompt and is required without a TTY) +bunny stream import --library 12345 --source vimeo --force + +# 4. Check how far Bunny has got with encoding +bunny stream import status --library 12345 --source vimeo + +# One folder only (Vimeo/Wistia projects, Brightcove folders, S3 prefixes) +bunny stream import --library 12345 --source vimeo --folder 987654 --force + +# S3, unattended, overriding the bucket and prefix for this run +bunny stream import --library 12345 --source s3 --bucket my-videos --prefix 2024/ --force + +# Continue an interrupted or partly failed run +bunny stream import --library 12345 --source vimeo --resume --force + +# Re-fetch videos stuck at zero bytes, or retry ones Bunny rejected +bunny stream import --library 12345 --source vimeo --retry-stalled --force +bunny stream import --library 12345 --source vimeo --clean-failed --force +``` + +--- + +## `bunny stream import`: Import a video library + +| Flag | Description | +| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| `--library`, `--lib` | Destination library name or ID (defaults to the linked library) | +| `--source`, `-s` | `vimeo`, `s3`, `wistia`, `mux`, `cloudflare`, `jwplayer`, `brightcove` | +| `--folder` | One source folder only (Vimeo, S3, Wistia, Brightcove; rejected for Mux, Cloudflare Stream, JW Player) | +| `--dry-run` | Print the plan; writes nothing (no state file, no link, nothing in the library) | +| `--resume` | Continue the saved import for this source and library | +| `--wait` | Stay until Bunny has encoded every video (default: return once they are queued) | +| `--force`, `-f` | Skip the confirmation (which defaults to No) and the post-run retry offer | +| `--concurrency`, `-c` | Videos handed to Bunny in parallel, 1 to 20 (default 3) | +| `--bucket`, `--prefix`, `--url-ttl` | S3 overrides for one run (`--url-ttl` 3600 to 604800 seconds) | +| `--include-ts` | S3: import `.ts` files too | +| `--clean-failed` | Delete a video's failed Bunny copies before importing it again; retries rejected videos and ones that failed 3 times | +| `--retry-stalled` | Delete and import again videos stuck at zero bytes past the 30 minute stall threshold | +| `--request-timeout` | Seconds per HTTP request, 1 to 3600 (default 30) | +| `--video-timeout` | Seconds per video hand-off, or per encode wait with `--wait`, 60 to 86400 (default 5400) | +| `--processing-timeout` | With `--wait`, seconds per encode, 60 to 86400 (default 3600); raises `--video-timeout` when larger | + +**Re-running is safe.** Every imported video carries a per-source metaTag (`vimeoId`, `s3Source`, `wistiaId`, `muxAssetId`, `cfStreamId`, `jwPlayerId`, `brightcoveId`). A re-run skips tagged videos Bunny has finished, leaves ones still encoding alone, and imports again only the ones Bunny failed on. Source folders become Stream collections of the same name. Videos the source reports as beyond Bunny's limits (over 72 hours, or above 2160p) are flagged before you confirm. + +**Progress** is saved to `$XDG_STATE_HOME/bunnynet/stream-import//-.json` (default `~/.local/state/...`). `--resume` retries failed videos, works outstanding ones, and keeps the `--folder` scope the run started with. It does **not** retry videos Bunny rejected for a reason the same file cannot get past (over limits, invalid framerate, corrupt original), ones no longer at the source, or ones that failed 3 times; use `--clean-failed` for those. Two imports cannot share one state file. + +**Ctrl-C** stops queuing new videos, saves progress, prints the `--resume` command, and exits 130. A second Ctrl-C exits at once. + +**Exit codes:** 0 when everything was queued (or finished with `--wait`), 1 when any video failed (they are listed with Bunny's reason; in a terminal it offers to retry them), 130 when paused. + +### JSON output + +Every `--output json` document carries an `outcome`: + +- `no_videos`, `nothing_new`, `dry_run`: stopped before importing. Shape `{ outcome, library, source, folder, dryRun, truncated, summary }`; `summary` holds the counts (including `processingOnBunny`) and name lists (capped at 10000; `truncated` says when). +- `imported`, `paused`: shape `{ outcome, library, source, folder, dryRun, status, waited, queued, completed, processing, failed, collections, warnings, statePath, elapsedMs }`, each `failed` entry `{ name, sourceId, error }`. On `paused`, `library` and `statePath` may be null if it stopped before reaching them. + +Declining the confirmation prints nothing. + +--- + +## `bunny stream import list`: Browse a source + +```bash +bunny stream import list --source vimeo # folders: ID, name, video count +bunny stream import list --source vimeo --videos # every video, with folder, duration, size +bunny stream import list --source vimeo --folder 987654 # videos in one folder +bunny stream import list --source s3 --bucket my-videos --prefix 2024/ +``` + +Resolves the source and credentials the same way an import does and needs no library. The folder IDs it prints are what `--folder` takes. JSON: `{ source, supportsFolders, folders, uncategorized, videos, truncated }`. + +--- + +## `bunny stream import status`: Check a queued import + +```bash +bunny stream import status --library 12345 +bunny stream import status --library 12345 --source s3 --output json +``` + +Reads the saved run (pass `--source` when more than one platform was imported into the library), asks Bunny for the current state of every video it queued, and prints Bunny's status, encode percentage, size, and when each was queued. A video still processing with no data after 30 minutes is marked `(stalled?)`: re-run the import with `--retry-stalled` to delete and fetch it again. Exits 1 when any video failed, so it works as a poll in scripts. JSON: `{ library, source, status, completed, processing, failed, stalled, stalledAfterMinutes, saved, videos }`. + +--- + +## Anti-patterns + +- **Passing credentials as flags or pasting them into commands.** They are read from environment variables only; export them (or use a `.env` the shell loads) instead. +- **Importing without a dry run.** Run `import list` and `--dry-run` first to see the folders, video counts, and what is already in Bunny. +- **Holding the command open with `--wait` in automation.** Queue with `--force`, then poll `bunny stream import status` until it exits 0; encodes can take hours. +- **Deleting the state file to "start fresh".** It is how a resume recovers videos Bunny created but never tagged; deleting it can create duplicates. +- **Treating `(stalled?)` as success.** Bunny gives no signal for a fetch that died silently; re-run with `--retry-stalled`. +- **Looping `--resume` on rejected videos.** It skips them on purpose; use `--clean-failed` once, or fix the source file. diff --git a/skills/bunny-cli/references/stream.md b/skills/bunny-cli/references/stream.md index 6745aa84..5565ae16 100644 --- a/skills/bunny-cli/references/stream.md +++ b/skills/bunny-cli/references/stream.md @@ -1,110 +1,176 @@ -# Stream Import Commands +# Stream Commands -> **Experimental.** `bunny stream` is hidden from `--help` while it stabilizes. Flags and JSON shapes may still change. +All Stream commands live under `bunny stream`, split into resource groups: -`bunny stream import` moves an existing video library into a Bunny Stream library from **Vimeo**, **AWS S3** (or an S3-compatible provider), **Wistia**, **Mux**, **Cloudflare Stream**, **JW Player**, or **Brightcove**. Bunny fetches every file straight from the source by URL, so nothing is downloaded to the local machine. The command hands each video to Bunny, tags it, and returns once they are all queued; encoding then happens on Bunny's side. +- **`bunny stream libraries`** (aliases `library`, `lib`) manages the video library itself, plus its `player` and `security` settings, with the account API key. +- **`bunny stream videos`** (alias `video`) manages the videos inside one library, their `captions`, and the paid `encode`, `transcribe`, and `smart`. +- **`bunny stream collections`** (alias `collection`) manages groups of videos within a library. +- **`bunny stream link`** and **`bunny stream unlink`** mirror `libraries link` and `libraries unlink`. +- **`bunny stream regions`** lists the region codes a library can replicate to. +- **`bunny stream import`** moves a library in from another platform; see `stream-import.md`. + +Each library carries its own Stream API key for the video-level API. Video commands resolve it from the library automatically, so there is nothing extra to configure; the key is never printed unless you ask for it with `libraries credentials`. ## Resolving the library -`--library` (alias `--lib`) takes a library name or numeric ID. When omitted, it resolves in this order: +`libraries` commands take the library as an optional positional (name or numeric ID). Every other stream command takes it as `--lib` (alias `--library`), because its positional is a video GUID, a collection ID, or a local file path. When omitted, the library resolves in this order: -1. `--library` -2. `.bunny/stream.json` (the directory's linked library) -3. Interactive picker, which offers to link the directory (never on `--dry-run`) +1. Explicit name or ID +2. `.bunny/stream.json` (written by `bunny stream link`) +3. Interactive picker, which offers to link the directory (never on destructive commands) -The library's own Stream API key is read from the account automatically; nothing extra to configure, and it is never printed. +Non-interactive runs (`--output json`, no TTY, or `--force`) error instead of prompting: pass a library or link the directory first. A `videos`, `collections`, or `videos captions` command with no ID behaves the same way. On destructive commands and `videos smart`, `--force` also disables the pickers, so it never acts on something you did not name; on `videos transcribe` that is `--overwrite`, and `videos encode` has neither. -## Source credentials +## Typical workflows -Credentials come only from environment variables (or the interactive prompt), never from flags. Set them before running unattended: +```bash +# New library: link this directory and write BUNNY_STREAM_LIBRARY_ID and BUNNY_STREAM_API_KEY to .env +bunny stream libraries create my-library --link --save-env --no-add-videos -| Source | Variables | -| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `vimeo` | `VIMEO_ACCESS_TOKEN` (scopes `public`, `private`, `video_files`; downloads need a paid plan) | -| `s3` | `S3_BUCKET`, `AWS_REGION` (default `us-east-1`); optional `S3_PREFIX`, `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`, `S3_PRESIGNED_URL_TTL`, `S3_ENDPOINT` | -| `wistia` | `WISTIA_ACCESS_TOKEN` | -| `mux` | `MUX_TOKEN_ID`, `MUX_TOKEN_SECRET` | -| `cloudflare` | `CLOUDFLARE_API_TOKEN` (Stream:Edit), `CLOUDFLARE_ACCOUNT_ID` | -| `jwplayer` | `JWPLAYER_API_KEY` (v2), `JWPLAYER_SITE_ID` | -| `brightcove` | `BRIGHTCOVE_CLIENT_ID`, `BRIGHTCOVE_CLIENT_SECRET` (CMS video read), `BRIGHTCOVE_ACCOUNT_ID` | +# Upload a local file, or have bunny.net fetch a URL server-side +bunny stream videos upload ./video.mp4 --title "Launch demo" +bunny stream videos fetch https://example.com/video.mp4 --header "Authorization: Bearer abc" -`bunny stream import --help` lists the same variables. With `--source` omitted, the command picks the one source whose variables are all set, otherwise it prompts (or fails when unattended). For S3, leaving both AWS keys unset uses the default AWS credential chain (profile, SSO, instance role); setting only one is an error. +# Find it, check it, embed it +bunny stream videos list --search launch +bunny stream videos show 1a2b3c4d-... +bunny stream videos embed 1a2b3c4d-... --expires 1h -## Typical workflows +# Organise with collections +bunny stream collections create Tutorials +bunny stream videos update 1a2b3c4d-... --collection 8a7b6c5d-... +``` + +--- + +## `bunny stream libraries`: Libraries ```bash -# 1. Look before importing: folders, video counts, what is already in Bunny -bunny stream import --library 12345 --source vimeo --dry-run +bunny stream libraries list # ID, name, videos, storage, traffic, replication regions +bunny stream libraries create my-library # stored in DE, replicated to SG, LA, NY by default +bunny stream libraries create my-library --replication-regions NY,SG # DE alone means no replication +bunny stream libraries create my-library --encoding-tier premium --codecs x264,vp9 +bunny stream libraries show my-library # API keys are never printed here +bunny stream libraries update my-library --resolutions 720p,1080p +bunny stream libraries update my-library --transcribing --transcribing-languages en,de +bunny stream libraries update my-library --encoding-tier premium --codecs x264,vp9 # paid; warns about billing +bunny stream libraries update my-library --add-replication-regions SYD # add-only +bunny stream libraries credentials my-library # library ID + API key, masked +bunny stream libraries credentials my-library --show-secret --read-only +bunny stream libraries credentials my-library --save-env # write BUNNY_STREAM_LIBRARY_ID and BUNNY_STREAM_API_KEY to .env +bunny stream libraries credentials my-library --rotate # reset the key (confirms first) +bunny stream libraries delete my-library # deletes every video in it; confirms with the count +bunny stream regions # DE is the fixed main region; SG, LA, NY the default replicas +bunny stream link my-library # same as `libraries link` +bunny stream unlink +``` -# 2. Import (confirms first; --force skips the prompt and is required without a TTY) -bunny stream import --library 12345 --source vimeo --force +`create` takes the name as its positional. After creating, it offers to link the directory, save `BUNNY_STREAM_LIBRARY_ID` and `BUNNY_STREAM_API_KEY` to `.env` (the names the Stream examples read), and add videos by importing from another platform or uploading a file. `--link`/`--no-link` and `--save-env`/`--no-save-env` answer the first two, `--no-add-videos` skips the third; non-interactive runs skip anything a flag does not set. -# 3. Check how far Bunny has got with encoding -bunny stream import status --library 12345 --source vimeo +`update` sends only the flags you pass. With no flags it opens an interactive editor (name, resolutions, transcribing, keep original, MP4 fallback, content tagging); unattended runs need at least one flag. Rename with `update --name`. -# One folder only (Vimeo/Wistia projects, Brightcove folders, S3 prefixes) -bunny stream import --library 12345 --source vimeo --folder 987654 --force +Run `bunny stream libraries --help` for every flag. The ones with consequences: -# S3, unattended, overriding the bucket and prefix for this run -bunny stream import --library 12345 --source s3 --bucket my-videos --prefix 2024/ --force +- `--encoding-tier premium` bills per output codec per minute. `--codecs` other than `x264`, and `--jit`, need it. +- `--early-play` needs `--keep-original` and exposes the originals. +- `--transcribing` bills per use on every new upload and re-encode. +- `--add-replication-regions` is add-only: regions can never be removed. +- `--content-tagging` is accepted on `update` only. +- `--force` is required for `unlink`, `delete`, `credentials --rotate`, `player reset`, and `security show --rotate` without a TTY. -# Continue an interrupted or partly failed run -bunny stream import --library 12345 --source vimeo --resume --force +### Player and security + +```bash +bunny stream libraries player show my-library +bunny stream libraries player update my-library --color "#FF7755" --speeds 0.5,1.0,1.15,2.0 --heatmap +bunny stream libraries player update my-library --no-controls +bunny stream libraries player reset my-library # dashboard defaults; confirms first +bunny stream libraries security show my-library # token key masked +bunny stream libraries security update my-library --embed-token --block-direct-access --allow-referrer example.com +bunny stream libraries security update my-library --drm-basic +bunny stream libraries security show my-library --rotate # reset the token key (confirms first); signed URLs stop working ``` +Run `player update --help` and `security update --help` for the full flag lists; every boolean takes a `--no-` form. `security update` applies domain changes one at a time and skips ones already in place, so re-running a partly failed update is safe. + --- -## `bunny stream import`: Import a video library +## `bunny stream videos`: Videos -| Flag | Description | -| ----------------------------------- | -------------------------------------------------------------------------------------------- | -| `--library`, `--lib` | Destination library name or ID (defaults to the linked library) | -| `--source`, `-s` | `vimeo`, `s3`, `wistia`, `mux`, `cloudflare`, `jwplayer`, `brightcove` | -| `--folder` | One source folder only (not for Mux, Cloudflare Stream, or JW Player, which have no folders) | -| `--dry-run` | Print the plan; writes nothing (no state file, no link, nothing in the library) | -| `--resume` | Continue the saved import for this source and library | -| `--wait` | Stay until Bunny has encoded every video (default: return once they are queued) | -| `--force`, `-f` | Skip the confirmation (which defaults to No) | -| `--concurrency`, `-c` | Videos handed to Bunny in parallel, 1 to 20 (default 3) | -| `--bucket`, `--prefix`, `--url-ttl` | S3 overrides for one run | -| `--request-timeout` | Seconds per HTTP request (default 30) | -| `--video-timeout` | Seconds per video hand-off, or per encode wait with `--wait` (default 5400) | -| `--processing-timeout` | With `--wait`, seconds per encode (default 3600); raises `--video-timeout` when larger | +```bash +bunny stream videos upload ./video.mp4 --lib 12345 --title "Launch demo" +bunny stream videos upload ./video.mp4 --collection 8a7b6c5d-... +bunny stream videos fetch https://example.com/video.mp4 --lib 12345 +bunny stream videos list --collection 8a7b6c5d-... +bunny stream videos show 1a2b3c4d-... # includes caption languages and the Direct Play URL +bunny stream videos update 1a2b3c4d-... --title "Launch demo" +bunny stream videos update 1a2b3c4d-... --chapters '[{"title":"Intro","start":0,"end":30}]' +bunny stream videos update 1a2b3c4d-... --collection "" # remove from its collection +bunny stream videos thumbnail 1a2b3c4d-... --file ./thumb.jpg # or --url +bunny stream videos resolutions 1a2b3c4d-... # configured, encoded, and stored +bunny stream videos cleanup 1a2b3c4d-... --non-configured --dry-run +bunny stream videos stats 1a2b3c4d-... --from 2026-01-01 --to 2026-01-31 +bunny stream videos embed 1a2b3c4d-... --expires 30m # signed when embed tokens are on +bunny stream videos delete 1a2b3c4d-... +``` -**Re-running is safe.** Every imported video carries a per-source metaTag (`vimeoId`, `s3Source`, `wistiaId`, `muxAssetId`, `cfStreamId`, `jwPlayerId`, `brightcoveId`). A re-run skips tagged videos Bunny has finished, leaves ones still encoding alone, and imports again only the ones Bunny failed on. Source folders become Stream collections of the same name. +There is no `videos create`: add a video with `upload` (local file) or `fetch` (bunny.net downloads a URL). -**Progress** is saved to `$XDG_STATE_HOME/bunnynet/stream-import//-.json` (default `~/.local/state/...`). `--resume` retries failed videos, works outstanding ones, and keeps the `--folder` scope the run started with. Two imports cannot share one state file; the second fails while the first is running. +- **`upload`** creates the entry, then sends the bytes. Files over 2 GB switch to resumable TUS uploads automatically. If the byte upload fails, an entry that never received bytes is deleted; one that may hold the upload is kept with a warning naming its GUID. Encoding continues after the command returns, so the status is usually `Uploaded` or `Processing`. +- **`fetch`** returns once the fetch is queued and reports no video ID; the video shows up in `videos list` once fetched. +- **`cleanup`** deletes renditions, MP4 fallbacks, or the original, and requires at least one selector (`--resolutions`, `--non-configured`, `--all`, `--original`, `--mp4`); `--outputs hls|mp4|all` only narrows which outputs those selectors touch. `--dry-run` lists what would go and skips the confirmation. -**Ctrl-C** stops queuing new videos, lets a hand-off already sent to Bunny finish so it is recorded, prints the `--resume` command, and exits 130. A second Ctrl-C exits at once. +Run `bunny stream videos --help` for every flag. `--force` is required for `delete` and `cleanup` without a TTY. -**Exit codes:** 0 when everything was queued (or finished with `--wait`), 1 when any video failed (they are listed), 130 when paused. +### Captions -### JSON output +```bash +bunny stream videos captions add ./captions.vtt 1a2b3c4d-... en --label "English" +bunny stream videos captions remove 1a2b3c4d-... en +``` -Every `--output json` document carries an `outcome`: +`captions add [video] [lang]` takes a local `.vtt` or `.srt` and reports the API validator's errors and warnings; `--label` defaults to the language code. `captions remove [video] [lang]` offers the video's caption languages to pick from interactively and needs `--force` without a TTY. Unattended runs need the video and the language. These are captions you wrote yourself, unrelated to the paid transcription. -- `no_videos`, `nothing_new`, `dry_run`: stopped before importing. Shape `{ outcome, library, source, folder, dryRun, truncated, summary }`; `summary` holds the counts and name lists (capped at 10000; `truncated` says when). -- `imported`, `paused`: shape `{ outcome, library, source, folder, dryRun, status, waited, queued, completed, processing, failed, collections, warnings, statePath, elapsedMs }`, each `failed` entry `{ name, sourceId, error }`. On `paused`, `library` and `statePath` may be null if it stopped before reaching them. +--- -Declining the confirmation prints nothing. +## `bunny stream collections`: Collections + +```bash +bunny stream collections list --search tut +bunny stream collections create Tutorials +bunny stream collections show 8a7b6c5d-... +bunny stream collections update 8a7b6c5d-... --name Guides +bunny stream collections delete 8a7b6c5d-... # deletes the videos inside it too +``` + +Deleting a collection deletes every video in it. Move videos out first with `videos update --collection ""`. --- -## `bunny stream import status`: Check a queued import +## Paid commands: premium encoding, `videos encode`, `videos transcribe`, `videos smart` ```bash -bunny stream import status --library 12345 -bunny stream import status --library 12345 --source s3 --output json +bunny stream libraries update my-library --encoding-tier premium --codecs x264,vp9 # switch the library to premium encoding +bunny stream videos encode 1a2b3c4d-... # re-encode one video with current settings (alias reencode) +bunny stream videos transcribe 1a2b3c4d-... --target-languages en,de --source-language en +bunny stream videos smart 1a2b3c4d-... --title --chapters # generate fields from the transcript ``` -Reads the saved run (pass `--source` when more than one platform was imported into the library), asks Bunny for the current state of every video it queued, and prints Bunny's status, encode percentage, size, and when each was queued. A video still processing with no data after 30 minutes is marked `(stalled?)`: delete it in the dashboard and re-run the import. Exits 1 when any video failed, so it works as a poll in scripts. JSON: `{ library, source, status, completed, processing, failed, stalled, stalledAfterMinutes, saved, videos }`. +All of them state the cost before running. + +- **Premium encoding** bills per output codec per minute; `--jit`, `--codecs`, `--resolutions` apply in the same request. `videos encode` regenerates every output at the library's encoding rate. +- **`videos transcribe`** costs $0.10 per language-minute (source transcript plus each target). The estimate is exact with `--source-language`, a range without. `--generate-title`, `--generate-description`, `--generate-chapters`, `--generate-moments` also generate fields. `--overwrite` replaces the existing transcription and overrides the library defaults (it is sent as the API's force flag), and disables the pickers. +- **`videos smart`** needs at least one of `--title`, `--description`, `--chapters`, `--moments`. `--source-language` picks the caption track (auto-generated tracks are `-auto`; `en` falls back to `en-auto`). On a video with no captions it shows the transcription cost and asks first; unattended runs stop unless `--transcribe` approves it. `--force` only disables pickers. --- ## Anti-patterns -- **Passing credentials as flags or pasting them into commands.** They are read from environment variables only; export them (or use a `.env` the shell loads) instead. -- **Importing without a dry run.** Run `--dry-run` first to see the folder and video counts and what is already in Bunny. -- **Holding the command open with `--wait` in automation.** Queue with `--force`, then poll `bunny stream import status` until it exits 0; encodes can take hours. -- **Deleting the state file to "start fresh".** It is how a resume recovers videos Bunny created but never tagged; deleting it can create duplicates. -- **Treating `(stalled?)` as success.** Bunny gives no signal for a fetch that died silently; the stalled video needs deleting and re-importing. +- **Deleting a collection to "ungroup" videos.** It deletes the videos too; clear `--collection` on each video first. +- **Running `videos transcribe` on a library that transcribes automatically.** A fresh upload or `videos encode` already triggers transcription; transcribing again bills twice. +- **Using `--overwrite` on `videos transcribe` to skip a prompt.** It re-runs the transcription and overrides library defaults. +- **Expecting `videos fetch` to return a video ID.** Poll `videos list` instead. +- **Running `videos cleanup` without `--dry-run` first.** A deleted original is gone, and re-creating renditions means a paid re-encode. +- **Expecting to remove replication regions.** `--add-replication-regions` is add-only, permanently. +- **Rotating keys casually.** `credentials --rotate` and `security show --rotate` break existing integrations and signed URLs. +- **Using the old spellings.** `encode enable`, `encode reencode`, top-level `transcribe`, `smart`, and `captions`, `collections rename`, and `security reset-token` still work but are hidden; use the commands above.