████████████████████████████████████████████████████████████████████████████████████████████████
⠀⠀⠀⠀⠀⠀⠀⠀⢀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⣰⠋⣷⠀⠀⠀⠀⠀ ⠀⠀⢀⡼⢲⠀⠀⠀⠀⠀⠀⠀⠀ ⢀⡖⢳⠄⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⣸⠃⢀⣿⠖⢳⡆⠀⠀⠀⢀⡞⠀⣸⣠⠤⡄⠀⠀⠀⠀⢠⡟⠁⣸⣤⠶⡄⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⢰⡇⣴⡟⠁⠀⡼⠁⠀⠀⠀⡾⢀⣴⠏⠀⢠⠇⠀⠀⠀⠀⡾⢀⣴⠋⠀⢰⠇⠀⠀⠀⠀⠀⠀
⠀⠀⣀⠴⠋⠉⠘⢹⡇⢀⡼⠳⠤⣤⡤⠒⠣⢟⡇⠀⢠⣏⣀⡀⣠⠴⠚⠣⢯⡇⠀⣠⢯⣄⣀⠀⠀⠀⠀⠀
⠀⡼⠃⢀⣄⠀⠀⠘⠓⠋⠀⢠⡞⠁⠀⠀⠀⠘⢧⠴⠋⠀⢠⠟⠁⠀⠀⠀⠘⠧⠞⠁⠀⠀⠈⠙⢦⡀⠀⠀
⢸⡇⠀⠈⠉⠀⠀⠀⠀⠀⢠⣏⡀⠐⠟⠃⠀⠀⠀⠀⠀⢠⣏⠀⠘⠛⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠹⡄⠀
⠀⠙⠦⣤⣀⣤⠤⠀⠀⠀⠈⠳⣄⡀⠀⠀⢀⠀⠀⠀⠀⠀⠻⢆⣀⢀⡀⣀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⣿⠀
⠀⠀⠀⢻⡀⠀⡀⠀⠀⠀⠀⠀⠀⢻⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⢹⡉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⡿⣄
⠀⠀⠀⢨⠇⢀⡿⠀⠀⢀⣀⡤⠄⠈⣧⠀⢲⠀⠀⠀⠀⢀⡀⠀⠈⣷⠀⢸⡀⠀⠀⠀⣀⡀⠀⠀⠀ ⠀⣡⠟
⠀⠀⠀⠙⠒⢯⣀⣠⠴⠻⢧⣄⠤⠼⢥⣴⠋⠀⢀⡴⡟⠉⢀⣀⣸⡧⣴⠋⢀⣀⡴⣟⣁⣀⣀⡠⠴⠊⠁⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠉⠁⠀⠉⠉⠉⠀⠀⠀⠈⠉⠉⠀⠀⠈⠉⠁⠀⠀⠀⠀⠀⠀
<[version 0.0.1]>
██████ ██ ██ ███ ██ ███ ██ ██ ██ ██ ██ ██████ ██ ██████ ██ ████████
██ ██ ██ ██ ████ ██ ████ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██
██████ ██ ██ ██ ██ ██ ██ ██ ██ ████ █████ ███ ██████ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██
██████ ██████ ██ ████ ██ ████ ██ ██ ██ ██ ███████ ██████ ██ ██
████████████████████████████████████████████████████████████████████████████████████████████████Technical, no-BS guide to download Bunny.net embeds in batch, then extract normalized podcast audio — with a one-shot orchestrator on top.
- Overview
- Requirements
- Folder Layout
- Scripts
bunny_downloader.py– single video downloader (HLS/MP4)batch_bunny.py– batch from a list filebunny_podcaster.py– audio extractor with progressmain.py– orchestrator driven byconfig.bunny
- Configuration
- List File Format
- Examples
- Troubleshooting
- Exit Codes
- Notes for Security Audits
bunny_downloader.pyparses a Bunny iframe URL, discovers the MP4/HLS source, and downloads it (HLS is remuxed to MP4 viaffmpeg). It sends the correctReferer/Originheaders to avoid hotlink 403s.batch_bunny.pyreads a custom list file and callsbunny_downloader.pyone by one. All videos are written to./downloads/video/. If no output name is provided, it autoincrements (1.mp4,2.mp4, …).bunny_podcaster.pyconverts each video in./downloads/video/to a podcast-ready MP3 in./downloads/audio/with real-time progress and loudness normalization.main.pyorchestrates both steps based onconfig.bunny.
- Python ≥ 3.9
- ffmpeg and ffprobe available in
PATH- Linux:
apt install ffmpeg/dnf install ffmpeg/brew install ffmpeg(macOS) - Windows: use a static build and add to PATH
- Linux:
- Network access to Bunny CDN endpoints from your machine
Optional but recommended:
- A Python virtual environment
.
├─ bunny_downloader.py
├─ batch_bunny.py
├─ bunny_podcaster.py
├─ main.py
├─ config.bunny
├─ list.bunny
└─ downloads/
├─ video/ # mp4s saved by batch_bunny.py
└─ audio/ # mp3s produced by bunny_podcaster.py
Purpose: Download a single Bunny video given an iframe URL.
Key features
- Parses the iframe HTML (meta/OG + HLS sources).
- Sends
Referer/Originheaders (in bothrequestsandffmpeg) to bypass hotlink filters. - HLS: picks the best variant and remuxes to MP4 (no re-encode).
- Fallback to direct MP4 if exposed via
og:video:url.
Usage
python bunny_downloader.py --iframe "https://iframe.mediadelivery.net/embed/<ACCOUNT>/<VIDEO_ID>" --out "./downloads/video/sample.mp4"
# Force direct MP4 (if present in OG tags)
python bunny_downloader.py --iframe "<...>" --out "./downloads/video/out.mp4" --prefer-mp4
# Manually set Referer/Origin (optional)
python bunny_downloader.py --iframe "<...>" --out "./downloads/video/out.mp4" --referer "https://iframe.mediadelivery.net"Important: A 403 may still happen if the CDN uses URL signing or session cookies; see Troubleshooting.
Purpose: Batch-download from a list file using bunny_downloader.py.
Behavior
- Always saves to
./downloads/video/. - Lines without an explicit output name get an auto-increment filename:
1.mp4,2.mp4, … - Lines with explicit output names are sanitized and de-duplicated (suffix
_1,_2, … if needed). - Runs in series (one by one), prints a summary.
Usage
python batch_bunny.py --file ./list.bunny
python batch_bunny.py --file ./list.bunny --prefer-mp4
python batch_bunny.py --file ./list.bunny --referer "https://iframe.mediadelivery.net"
python batch_bunny.py --file ./list.bunny --bunny-script ./bunny_downloader.pyPurpose: Extract podcast-style audio from all videos in ./downloads/video/ into ./downloads/audio/.
Audio output
- MP3 @ 44.1 kHz
- Mono by default (use
--stereoto keep stereo) - 96 kbps CBR by default (configurable)
- EBU R128 loudness normalization (≈ −16 LUFS)
- Real-time progress per file:
%, processed time, speed, ETA (uses-progress pipe:1+ffprobe)
Usage
# Default: mono 96k
python bunny_podcaster.py
# Stereo 128k
python bunny_podcaster.py --stereo --bitrate 128k
# Overwrite existing MP3s
python bunny_podcaster.py --overwrite
# Different extension (e.g., .m4a via external change – script defaults to MP3 encoder)
python bunny_podcaster.py --ext .mp3Outputs are written to ./downloads/audio/.
Purpose: High-level orchestrator. Reads config.bunny, runs batch download, then (optionally) runs podcast export.
Usage
python main.py
# or
python main.py --config ./config.bunnyFlow
- Validates paths and config.
- Runs
batch_bunny.pywith configured flags. - If
[podcast].enabled = true, runsbunny_podcaster.pywith configured audio options. - Exits with the batch’s exit code (useful for CI).
config.bunny is an INI file:
[batch]
batch_script = ./batch_bunny.py
bunny_dl = ./bunny_downloader.py
list_file = ./list.bunny
prefer_mp4 = true
referer = https://iframe.mediadelivery.net
[podcast]
enabled = true
script = ./bunny_podcaster.py ; point to your actual audio script
bitrate = 96k
stereo = false
overwrite= false
ext = .mp3If you renamed the audio script, set
[podcast].scriptto the correct path.
list.bunny supports one entry per line:
- Just the iframe URL (auto-number output):
https://iframe.mediadelivery.net/embed/399263/c2c8521b-2329-494a-8294-f5f9e71bf902
- URL + explicit output name (any of these separators:
|,,,->):
https://iframe.../embed/.../aaaaaaaa-bbbb | class_01.mp4
https://iframe.../embed/.../bbbbbbbb-cccc -> my_video.mp4
https://iframe.../embed/.../cccccccc-dddd , lesson3
The name is sanitized;
.mp4is appended if missing.
- Comments / blanks are ignored:
# this is a comment
1) Batch only
python batch_bunny.py --file ./list.bunny --prefer-mp4 --referer "https://iframe.mediadelivery.net"
# Videos → ./downloads/video/2) Full pipeline via orchestrator
# config.bunny prepared as above
python main.py
# Videos → ./downloads/video/
# Audios → ./downloads/audio/3) Single video
python bunny_downloader.py --iframe "https://iframe.mediadelivery.net/embed/399263/<VIDEO_ID>" --out "./downloads/video/demo.mp4"4) Convert to podcast with progress
python bunny_podcaster.py --stereo --bitrate 128k-
403 Forbidden (m3u8 or segments)
The script already sendsReferer/Origin. If 403 persists, the CDN likely enforces URL signing and/or session cookies.
→ Conclusion for audits: not downloadable off-site without valid signed URL/session. -
ffmpeg/ffprobenot found
Install them and ensure they’re inPATH. The scripts will exit early with a clear message. -
Playlist without variants
bunny_downloader.pydetects media playlists (no#EXT-X-STREAM-INF) and downloads directly. -
Output collisions
Batch will suffix duplicates (_1,_2, …). Auto-numbering scans existingN.mp4files and continues from the highest. -
Weird resolutions
Variant parser infers height fromRESOLUTION=WxH,NAME="1080p", or the URL itself (/.../1080p/...,.../1920x1080/...).
0— success1— missingffmpeg/ffprobeor processing error2— invalid/missing inputs (config/list/script not found)3— podcast stage script missing (orchestrator)
Orchestrator (main.py) returns the batch exit code (even if podcast ran).
- This tooling is intended for authorized security testing.
- If assets are protected by hotlink checks, signed URLs, session tokens, or DRM, downloads should fail when requested off-site — that’s expected and should be recorded as properly protected.
- If assets are retrievable off-site without such controls, document the precise request headers/flows demonstrating the exposure.
That’s it. Plug your list, run the batch, and get clean, normalized podcast audio with progress.