video2bunny
v1.1.0
Published
CLI tool to migrate videos from Vimeo, AWS S3, Wistia, Mux, Cloudflare Stream, JW Player, or Brightcove to bunny.net Stream
Downloads
285
Maintainers
Readme
video2bunny
Migrate videos from Vimeo, AWS S3, Wistia, Mux, Cloudflare Stream, JW Player, or Brightcove to bunny.net Stream.
Videos transfer directly from the source platform to Bunny via URL fetch — nothing is downloaded to your machine.
Quick Start
npm install -g video2bunny
video2bunny config # select source, enter credentials
video2bunny migrate # run the migrationOr from source:
git clone https://github.com/nathanzbunny/video2bunny.git
cd video2bunny
npm install
npm run build
npm linkSetup
1. Configure your source and Bunny credentials
video2bunny configThe interactive wizard prompts you to choose a source, enter source-specific credentials, then your Bunny Stream library ID and API key.
Every prompt has an environment variable equivalent for non-interactive / CI use (see platform sections below).
2. Bunny Stream credentials (required for all sources)
Get these from your Bunny Stream library: Stream Library → API.
| Setting | Env var | Description |
|---|---|---|
| Library ID | BUNNY_LIBRARY_ID | Numeric ID from the library URL or API page |
| Library API Key | BUNNY_LIBRARY_API_KEY | The per-library key (not your account API key) |
Supported Sources
📹 Vimeo
Downloads are accessed via the Vimeo API. Requires a Standard plan or above for download links.
Credentials:
| Setting | Env var | How to get it |
|---|---|---|
| Access Token | VIMEO_ACCESS_TOKEN | developer.vimeo.com/apps → Create App → Generate Token |
Required token scopes: public, private, video_files.
Folder mapping: Vimeo Projects → Bunny Collections.
Usage:
video2bunny config # select "Vimeo", paste token
video2bunny list --source vimeo
video2bunny migrate --source vimeo --dry-run
video2bunny migrate --source vimeo
video2bunny migrate --source vimeo --folder <project_id>Dedup tag: vimeoId — the numeric Vimeo video ID is written to each Bunny video as a metaTag so re-runs skip already-migrated videos.
☁️ AWS S3
Uses S3 pre-signed URLs. Bunny fetches directly from S3 — no data passes through your machine.
Credentials:
| Setting | Env var | How to get it |
|---|---|---|
| Access Key ID | AWS_ACCESS_KEY_ID | IAM console → Users → Security credentials |
| Secret Access Key | AWS_SECRET_ACCESS_KEY | Generated alongside the access key |
| Region | AWS_REGION | The bucket's region (e.g. us-east-1) |
| Session Token (optional) | AWS_SESSION_TOKEN | For temporary/assumed-role credentials |
| Bucket | S3_BUCKET | The source bucket name |
| Prefix (optional) | S3_PREFIX | Scope to a key prefix (e.g. videos/) |
| URL TTL | S3_PRESIGNED_URL_TTL | Seconds (default 21600 = 6h, max 604800 = 7d) |
Alternatively, leave credentials blank during video2bunny config to use the default AWS credential chain (~/.aws/credentials, instance profile, etc.).
Required IAM permissions: s3:ListBucket on the bucket, s3:GetObject on the objects.
Folder mapping: First-level subprefixes below the configured root → Bunny Collections. Root-level objects become uncategorized.
Usage:
video2bunny config # select "AWS S3"
video2bunny list --source s3
video2bunny list --source s3 --bucket other-bucket --prefix raw/
video2bunny migrate --source s3 --dry-run
video2bunny migrate --source s3 --concurrency 5
video2bunny migrate --source s3 --url-ttl 86400Pre-signed URL generation (S3 only):
video2bunny presign # print URLs to terminal
video2bunny presign --format json -o urls.json
video2bunny presign --format csv
video2bunny presign --bucket b --prefix p/ --url-ttl 3600Dedup tag: s3Source — the value bucket/key is written to each Bunny video.
Recognised video extensions: .mp4, .m4v, .mov, .mkv, .webm, .avi, .wmv, .flv, .mpg, .mpeg, .ts, .mts, .m2ts, .3gp. All other S3 objects are ignored.
📊 Wistia
Downloads are accessed via asset URLs retrieved through the Wistia Data API. The tool picks the best available MP4 rendition (HD → MD → SD → Original).
Credentials:
| Setting | Env var | How to get it |
|---|---|---|
| API Token | WISTIA_ACCESS_TOKEN | Wistia account → Settings → API Access |
Folder mapping: Wistia Projects → Bunny Collections.
Usage:
video2bunny config # select "Wistia", paste token
video2bunny list --source wistia
video2bunny migrate --source wistia --dry-run
video2bunny migrate --source wistia
video2bunny migrate --source wistia --folder <project_hashed_id>Dedup tag: wistiaId — the media hashed_id.
Note: Wistia asset URL access bypasses their analytics and counts toward your bandwidth. See Wistia's asset URL docs for details.
⚡ Mux
Two download mechanisms: static renditions (MP4 at low/medium/high quality) and master access (original mezzanine file, 24h expiry). The tool prefers static renditions when available.
Credentials:
| Setting | Env var | How to get it |
|---|---|---|
| Token ID | MUX_TOKEN_ID | dashboard.mux.com/settings/api-keys |
| Token Secret | MUX_TOKEN_SECRET | Generated alongside the token ID |
Auth is HTTP Basic (token_id:token_secret).
Folder mapping: Mux has no folder concept. All assets migrate as uncategorized. Use --folder is not applicable.
Prerequisites: For static rendition downloads, assets must have mp4_support set to "standard" or "capped-1080p". You can enable this per-asset via the Mux API or dashboard before running the migration. If static renditions aren't available, the tool falls back to master access if enabled.
Usage:
video2bunny config # select "Mux", enter token ID + secret
video2bunny list --source mux
video2bunny migrate --source mux --dry-run
video2bunny migrate --source mux
video2bunny migrate --source mux --concurrency 5Dedup tag: muxAssetId — the Mux asset ID.
🌐 Cloudflare Stream
Downloads are generated on-demand via the /downloads API endpoint. The tool triggers MP4 generation, polls until the download is ready, then hands the URL to Bunny. Private videos require a token with Stream read access.
Credentials:
| Setting | Env var | How to get it |
|---|---|---|
| API Token | CLOUDFLARE_API_TOKEN | dash.cloudflare.com/profile/api-tokens → Create Token |
| Account ID | CLOUDFLARE_ACCOUNT_ID | Dashboard URL: dash.cloudflare.com/<account_id> or Account → Overview sidebar |
Required token permission: Stream:Read (and Stream:Edit if downloads haven't been pre-generated).
Folder mapping: Cloudflare Stream has no folder concept. All videos migrate as uncategorized.
Usage:
video2bunny config # select "Cloudflare Stream"
video2bunny list --source cloudflare
video2bunny migrate --source cloudflare --dry-run
video2bunny migrate --source cloudflare
video2bunny migrate --source cloudflare --concurrency 3Dedup tag: cfStreamId — the Cloudflare Stream video UID.
Note: MP4 download generation may take a few minutes per video. The tool polls automatically. Downloads count toward your Cloudflare Stream encoding minutes and storage.
🎯 JW Player (JWX)
Media is retrieved via the JW Player Management API v2. Download URLs are sourced from the Delivery API media endpoint, picking the highest-quality MP4 rendition.
Credentials:
| Setting | Env var | How to get it |
|---|---|---|
| API Key (v2) | JWPLAYER_API_KEY | dashboard.jwplayer.com → Properties → API Credentials |
| Site ID | JWPLAYER_SITE_ID | Dashboard → Properties → your property's 8-character ID |
Folder mapping: JW Player has no folder concept. All media migrate as uncategorized.
Usage:
video2bunny config # select "JW Player (JWX)"
video2bunny list --source jwplayer
video2bunny migrate --source jwplayer --dry-run
video2bunny migrate --source jwplayer
video2bunny migrate --source jwplayer --concurrency 5Dedup tag: jwPlayerId — the JW Player media ID.
📡 Brightcove
Uses the CMS API with OAuth 2.0 client credentials authentication. The tool fetches the highest-resolution MP4 source for each video. Brightcove has native folders, which map directly to Bunny collections.
Credentials:
| Setting | Env var | How to get it |
|---|---|---|
| Client ID | BRIGHTCOVE_CLIENT_ID | studio.brightcove.com/admin/oauthclient → Register New Application |
| Client Secret | BRIGHTCOVE_CLIENT_SECRET | Generated when registering the OAuth client |
| Account ID | BRIGHTCOVE_ACCOUNT_ID | Studio → Admin → Account Information, or from your Studio URL |
Required OAuth permissions: CMS → Video Read (at minimum). CMS → Video Read/Write if you want to access the digital master.
The tool handles token refresh automatically — OAuth access tokens are short-lived (~5 minutes) and re-requested as needed.
Folder mapping: Brightcove Folders → Bunny Collections. Videos not in a folder migrate as uncategorized.
Usage:
video2bunny config # select "Brightcove"
video2bunny list --source brightcove
video2bunny list --source brightcove --folders
video2bunny migrate --source brightcove --dry-run
video2bunny migrate --source brightcove
video2bunny migrate --source brightcove --folder <folder_id>
video2bunny migrate --source brightcove --concurrency 5Dedup tag: brightcoveId — the Brightcove video ID.
Common Commands
All commands work identically across every source. The --source flag overrides whatever was set during video2bunny config.
List content
video2bunny list # uses configured source
video2bunny list --source <source> # override source
video2bunny list --folders # folders/projects only
video2bunny list --videos # videos onlyMigrate videos
video2bunny migrate # uses configured source
video2bunny migrate --source <source> # override source
video2bunny migrate --dry-run # preview without changes
video2bunny migrate --folder <id> # single folder/project only
video2bunny migrate --concurrency 5 # parallel transfers (default 3, max 20)
video2bunny migrate --resume # retry failed / continue interrupted
video2bunny migrate --verbose # show already-migrated videos
video2bunny migrate --processing-timeout 120 # Bunny encode timeout in minutes
video2bunny migrate --request-timeout 60 # HTTP timeout in secondsCheck migration status
video2bunny statusReset configuration
video2bunny config:resetHow It Works
- Discover — The tool lists all videos and folders/projects from the source platform's API.
- Map folders — Source folders (Vimeo Projects, Wistia Projects, S3 subprefixes, Brightcove Folders) become Bunny collections with matching names. Platforms without folders (Mux, Cloudflare, JW Player) skip this step.
- Get download URL — Each platform provides the URL differently:
| Source | Download mechanism | URL lifetime |
|---|---|---|
| Vimeo | API download links | ~24 hours |
| S3 | Pre-signed GetObject URL | Configurable (1m–7d) |
| Wistia | Asset URL from Data API | Persistent (token-scoped) |
| Mux | Static rendition URL or master access | Persistent / 24h |
| Cloudflare | On-demand /downloads endpoint | Persistent once generated |
| JW Player | Delivery API source URL | Persistent (key-scoped) |
| Brightcove | CMS API video sources | Token-scoped (~5 min, auto-refreshed) |
- Transfer — The URL is sent to Bunny's
videos/fetchendpoint. Bunny downloads the file directly from the source platform — nothing passes through your machine. - Metadata — Title, description, and tags are copied where the source API provides them.
- Track — A dedup metaTag is written to each Bunny video so re-runs and
--resumeskip already-migrated content.
Dedup tags by source
| Source | Bunny metaTag | Value |
|---|---|---|
| Vimeo | vimeoId | Numeric video ID |
| S3 | s3Source | bucket/key |
| Wistia | wistiaId | Media hashed ID |
| Mux | muxAssetId | Asset ID |
| Cloudflare | cfStreamId | Video UID |
| JW Player | jwPlayerId | Media ID |
| Brightcove | brightcoveId | Video ID |
Environment Variables Reference
All credentials can be set via environment variables instead of (or in addition to) the interactive config command. Environment variables take priority over stored configuration.
# ── Bunny Stream (always required) ──────────────────
BUNNY_LIBRARY_ID=...
BUNNY_LIBRARY_API_KEY=...
# ── Vimeo ────────────────────────────────────────────
VIMEO_ACCESS_TOKEN=...
# ── AWS S3 ───────────────────────────────────────────
AWS_ACCESS_KEY_ID=...
AWS_SECRET_ACCESS_KEY=...
AWS_REGION=us-east-1
AWS_SESSION_TOKEN=... # optional, for temp creds
S3_BUCKET=my-bucket
S3_PREFIX=videos/ # optional
S3_PRESIGNED_URL_TTL=21600 # optional, seconds
# ── Wistia ───────────────────────────────────────────
WISTIA_ACCESS_TOKEN=...
# ── Mux ──────────────────────────────────────────────
MUX_TOKEN_ID=...
MUX_TOKEN_SECRET=...
# ── Cloudflare Stream ────────────────────────────────
CLOUDFLARE_API_TOKEN=...
CLOUDFLARE_ACCOUNT_ID=...
# ── JW Player ────────────────────────────────────────
JWPLAYER_API_KEY=...
JWPLAYER_SITE_ID=...
# ── Brightcove ───────────────────────────────────────
BRIGHTCOVE_CLIENT_ID=...
BRIGHTCOVE_CLIENT_SECRET=...
BRIGHTCOVE_ACCOUNT_ID=...Credential Storage
Credentials are stored securely using the system keychain when available (macOS Keychain, Windows Credential Vault, Linux libsecret). If the keychain isn't available (e.g. headless servers, CI), credentials fall back to a JSON config file at ~/.config/video2bunny-nodejs/config.json.
Migration state is saved to ~/.video2bunny/migration-state.json with 0600 permissions so interrupted migrations can be resumed with --resume.
Troubleshooting
"No download link available" — The source platform couldn't provide a download URL. For Vimeo, this means a Standard plan or above is required. For Mux, ensure mp4_support is enabled on the asset. For Cloudflare, the download may still be generating — the tool retries automatically for up to 5 minutes.
"Access denied to bucket" — S3 source: check that IAM permissions include s3:ListBucket and s3:GetObject.
"Bucket exists in a different region" — S3 source: set AWS_REGION to match the bucket's actual region.
"Invalid ... credentials" — Double-check the token/key you entered. For Brightcove, ensure your OAuth client has CMS Video Read permission. For JW Player, confirm you're using a v2 Management API key (not a legacy v1 key).
"Rate limited" — All API clients handle rate limiting automatically with backoff and retry. No action needed.
Migration stalled or interrupted — Run video2bunny migrate --resume to pick up where it left off. The tool skips already-completed videos.
License
MIT
