Terminal tool for batch-downloading and streaming anime from AnimePahe. Features a parallel HLS engine with segment-level crash recovery, Rich-powered live dashboard, and MPV streaming with mid-playback SUB/DUB switching.
- About
- Prerequisites
- Installation
- Quick Start
- Feature Tour
- CLI Reference
- Architecture
- Package Structure
- Development
- Troubleshooting
- Contributing
- License
Pahebatcher is a terminal application for batch-downloading anime from AnimePahe. It supports parallel episode downloads with per-episode HLS segment concurrency, segment-level crash recovery, interactive episode selection, and MPV streaming with mid-playback audio track switching.
Key characteristics:
- Single-site focus. Pahebatcher targets AnimePahe exclusively and does not support other sources.
- Self-hosted infrastructure. Cloudflare bypass uses a local FlareSolverr instance via Docker. All traffic stays on your machine — no third-party proxies. You can also inject your browser's
cf_clearancecookie to skip challenge solving entirely. - Atomic segment writes. HLS segments are written to
.tmpand renamed to.tsafter completion. A mid-download interruption picks up at the exact segment where it left off, without re-downloading completed work. - Concurrent pipeline. A two-stage prefetch architecture resolves stream URLs ahead of downloaders via an
asyncio.Queue. A configurableresolve_aheadthrottle controls how many episodes the resolver stays ahead of downloaders to avoid Cloudflare request bursts. Episodes download in parallel (configurable 1–6), with per-episode segment concurrency (configurable 8–32). - Rich terminal UI. Progress dashboard shows all episodes simultaneously with per-episode segment counts, transfer speeds, ETAs, file sizes, and color-coded state transitions. Interactive episode selection includes range input, a toggle checklist, and "latest N" mode.
- MPV streaming. Episodes can be streamed without downloading. A playback panel shows the current episode and playlist position. Audio tracks can be toggled between SUB and DUB mid-session.
- Session management. Previous download sessions can be resumed, deleted, or cleared from the cache. Cached segments are reused on restart. Scan results are cached to disk with a configurable TTL so re-running the tool skips all AnimePahe API calls for fresh data.
- Persistent configuration. Quality, audio, concurrency, and output directory are saved to
pahebatcher.tomlin the project directory. Set once via the interactive wizard orpahebatcher config set, reused on every subsequent run. CLI flags override persisted values when needed. Edit the file directly or use theconfigsubcommand. - MIT licensed. Free to use, modify, and redistribute.
| Requirement | Purpose | Install |
|---|---|---|
| FlareSolverr | Cloudflare bypass (headless Chromium) | docker run -d --name=flaresolverr -p 8191:8191 ghcr.io/flaresolverr/flaresolverr |
| FFmpeg | TS segment concatenation into MP4 | sudo apt install ffmpeg (Linux) / brew install ffmpeg (macOS) |
| MPV | Streaming mode only | sudo apt install mpv / brew install mpv |
| Python 3.11+ | Runtime | python3 --version |
FlareSolverr must be running before pahebatcher starts. The tool checks reachability on launch and prints the exact Docker command if it cannot connect. The default URL is http://localhost:8191/v1; override with the FLARESOLVERR_URL environment variable.
git clone https://github.com/smolfiddle/pahebatcher.git
cd pahebatchermake run # interactive wizard
make run "URL" # skip search, go directly to series
make help # show all targets
make config-show # display current settings
make test # run all 195 tests
make lint # ruff check (0 errors)
make typecheck # mypy strict (0 errors)
make benchmark # full coherence benchmark
make help # show all targetsAfter first run, set persistent defaults:
make config-show
venv/bin/python -m pahebatcher config set quality 720
venv/bin/python -m pahebatcher config set audio_lang engPass a URL directly (positional or via URL=):
make run "https://animepahe.pw/anime/<uuid>"
make run URL="https://animepahe.pw/anime/<uuid>" ARGS="--all -q 720"pipx install .
pahebatcherpip install -e ".[dev]"
pahebatcherAll three methods produce the pahebatcher command. You can also run via python -m pahebatcher.
# Interactive wizard -- search for a series or paste a URL
pahebatcher
# Download entire series, 720p, Japanese audio, 2 concurrent episodes
pahebatcher "https://animepahe.pw/anime/<uuid>" --all -q 720
# Download episodes 1 through 12, English dub, 1080p, custom output directory
pahebatcher "https://animepahe.pw/anime/<uuid>" --range 1-12 --audio eng -q 1080 -o ~/anime
# Download only the 3 most recently aired episodes
pahebatcher "https://animepahe.pw/anime/<uuid>" --latest 3
# List all episodes and exit (no download)
pahebatcher "https://animepahe.pw/anime/<uuid>" --list
# Stream episodes in MPV with on-the-fly SUB/DUB switching
pahebatcher "https://animepahe.pw/anime/<uuid>" --stream -q 1080
# 4 concurrent episodes, 32 HLS workers per episode
pahebatcher "https://animepahe.pw/anime/<uuid>" --all -q 1080 -j 4 -w 32
# Save default preferences so you don't need flags every time
pahebatcher config set quality 720
# Enable debug logging for troubleshooting
pahebatcher "https://animepahe.pw/anime/<uuid>" --all --verboseOutput files are saved as Ep 001 - Episode Title.mp4 in the output directory (default: ./downloads/<series_name>/).
The core workflow: scan a series, select episodes, configure settings, download.
Episode selection offers five modes accessible from the interactive wizard or CLI flags:
| Mode | CLI flag | Interactive | Description |
|---|---|---|---|
| All | --all |
Press A |
Download every episode in the series |
| Range | --range 1-12 |
Press R |
Specify with 1-12, 1,4,7, 13- (open-ended), or mixed 1-6,10,14- |
| Toggle checklist | — | Press L |
Interactive table; toggle individual episodes with numbers, a=select all, n=deselect all, done=confirm |
| Latest N | --latest 3 |
Press N |
Grab the most recent N episodes |
| Skip | — | Press S |
Return to action menu without selecting |
Settings wizard (interactive mode only) prompts for quality, audio language, output directory, and concurrency. Choices are automatically persisted to pahebatcher.toml and reused on future runs — run the wizard once, no need to reconfigure on subsequent sessions.
Download dashboard shows every episode simultaneously with live per-episode metrics: segment counter (M of N), percentage, transfer speed, ETA, and file size. Each episode transitions through color-coded states: resolving (cyan) -> queued (dim cyan) -> downloading (bold white) -> remuxing (yellow) -> done (green checkmark) / fail (red cross).
Segment-level crash recovery. HLS segments are written atomically (.tmp file renamed to .ts after write completes). On restart, the tool reads done_indices() and only fetches missing segments. Already-completed MP4 files in the output directory are skipped entirely.
Launches MPV with the resolved M3U8 URL and authentication headers. Displays a live "Now Playing" panel with episode title, audio track, quality, and playlist position.
Post-episode navigation:
N/P— next / previous episode in playlistA— toggle SUB / DUB audio track mid-session (reloads playlist with new audio lane)R— replay current episodeS— jump to any episode by numberQ— quit
Post-episode controls appear after MPV closes, offering navigation, audio switching, replay, and episode selection.
Accessible from the main menu (option 3). Lists all cached sessions with:
- Anime title and URL
- Episode count and segment count
- Total cache size on disk
- Status (Paused)
Actions: Resume (restarts tool with that series URL), Delete (removes single session cache), Clear All (wipes entire pahe_cache/ directory).
Running pahebatcher without a URL opens interactive search. Type an anime title, browse results in a table (title, type, year, episodes, score), select by number. The tool auto-discovers SUB and DUB variants of the selected series.
Settings are persisted at pahebatcher.toml in the current directory and loaded on every run. Saved defaults apply to every session, CLI flags override them, and the interactive wizard auto-saves whatever is chosen.
Manage settings from the command line:
pahebatcher config show # display current values
pahebatcher config set quality 720 # set default quality
pahebatcher config set audio_lang eng # set default audio to DUB
pahebatcher config set max_parallel 4 # set default concurrency
pahebatcher config set hls_workers 16 # set default segment workers
pahebatcher config set output_dir ~/anime # set default output directory
pahebatcher config set resolve_ahead 1 # serial resolution to avoid Cloudflare bursts
pahebatcher config set cache_ttl 120 # cache scan results for 2 hours
pahebatcher config set cookie_string "cf_clearance=abc123; session=xyz" # reuse browser cookies
pahebatcher config set auto_retry 2 # auto-retry failed episodes (0-2, 2 = 3 total attempts)
pahebatcher config reset # restore all defaultsSupported keys and their valid values:
| Key | Type | Default | Values |
|---|---|---|---|
quality |
integer | 1080 |
360, 720, 1080 |
audio_lang |
string | jpn |
jpn, eng |
max_parallel |
integer | 2 |
1–6 |
hls_workers |
integer | 24 |
8–32 |
output_dir |
string | . |
Any valid path |
keep_temp |
boolean | false |
true, false |
resolve_ahead |
integer | 999 |
0+ (how many episodes the resolver stays ahead of downloaders; set to 1 for serial resolution) |
cache_ttl |
integer | 60 |
0+ (minutes before scan cache expires; 0 disables caching) |
cookie_string |
string | "" |
Semicolon-delimited cookies like cf_clearance=abc123; session=xyz |
auto_retry |
integer | 2 |
0–2 (0 = no retry, 2 = 3 total attempts per episode) |
pahebatcher [URL] [options]
| Flag | Shorthand | Description |
|---|---|---|
--all |
-a |
Download every episode |
--range RANGE |
-r RANGE |
Episode range: 1-12, 1,4,7, 13- |
--latest N |
-n N |
Latest N episodes |
--stream |
-s |
Stream via MPV instead of downloading |
| Flag | Shorthand | Description | Default |
|---|---|---|---|
--list |
-l |
Scan series, print episode table, exit | off |
--output DIR |
-o DIR |
Output directory (series name appended) | ./downloads |
--quality Q |
-q Q |
Resolution: 360, 720, 1080 |
1080 |
--audio LANG |
— | Audio track: jpn (subbed), eng (dubbed) |
jpn |
--parallel N |
-j N |
Concurrent episode downloads (1-6) | 2 |
--workers N |
-w N |
HLS segment fetchers per episode (8-32) | 24 |
--keep-temp |
— | Keep raw .ts files after muxing |
off |
--retry N |
— | Auto-retry failed episodes (0-2, 3 total) | 2 |
--verbose |
-v |
Enable debug-level logging | off |
| Command | Description |
|---|---|
pahebatcher config show |
Display all settings with current values and defaults |
pahebatcher config set KEY VALUE |
Persist a setting (see Configuration section for key list) |
pahebatcher config reset |
Restore all settings to factory defaults |
The two concurrency flags control different layers of parallelism:
-
-j/--parallel: How many episodes download at once. Default 2. The resolver stage uses a single FlareSolverr instance, so values above 3-4 may not improve throughput. Increase if your internet connection has significant headroom. -
-w/--workers: How many HLS segments each episode fetches concurrently. Default 24. HLS segments are small (~100 KB each), so high concurrency saturates residential connections efficiently. Lower to 8-12 if you see frequent segment failures from CDN rate limiting.
The combined maximum concurrent TCP streams is parallel * workers (default: 2 * 24 = 48). At -j 4 -w 32, this reaches 128 streams.
+-------------------------------------------------------------+
| Stage 1 -- Resolver (serial, 1 worker) |
| FlareSolverr fetches play pages, extracts Kwik URLs, |
| resolves them to M3U8 manifests. One at a time because |
| FlareSolverr uses a single Chromium tab. |
+-------------------------------------------------------------+
|
v (asyncio.Queue, capacity = max_parallel + 2)
+-------------------------------------------------------------+
| Stage 2 -- Downloaders (concurrent, N workers) |
| Each downloads HLS segments in parallel (24 workers per |
| episode), decrypts AES-128 if needed, writes atomically |
| to the segment store, then muxes via ffmpeg. |
+-------------------------------------------------------------+
The resolver runs ahead of downloaders via asyncio.Queue with resolve_ahead + max_parallel capacity (default 999 + max_parallel, effectively unlimited). Setting resolve_ahead to 1 limits the prefetch to a single episode, spreading the Kwik resolution requests across the download duration to avoid Cloudflare block triggers.
A single aiohttp.ClientSession is created for the entire batch:
limit=0(no global connection cap; semaphores control concurrency)limit_per_host=hls_workers(per-CDN bounds)keepalive_timeout=45s(TCP reuse across segments of the same stream)- 5-minute DNS cache
- TLSv1.2 minimum, forward-secrecy ciphers only, certificate verification required
pahe_cache/
Spy_x_Family_f1a5749e/
session.json -- metadata (title, URL, timestamp)
Ep_001_JPN/
000000.ts
000001.ts
...
concat.txt -- ffmpeg concat demuxer playlist
Ep_002_JPN/
...
- Atomic writes: segments written to
.tmpthen renamed to.ts. Crash at any point leaves the store consistent. - Resume: on restart,
done_indices()reads existing segment IDs. Only missing segments are fetched. Completed MP4 files are skipped entirely. - Orphan cleanup: cache directories older than 24 hours without active downloads are removed on exit.
- Fetch play page via FlareSolverr
- Parse resolution menu buttons (
data-src,data-resolution,data-audio,data-fansub) - Select quality: closest available resolution below or equal to user preference. Falls back to highest available if none match.
- Select audio track: 3-strategy detection --
data-audioattribute, CSS class names, text content. Falls back from DUB to SUB automatically when DUB is unavailable. - Fetch Kwik page directly with
Referer: https://animepahe.com/(bypasses Cloudflare without FlareSolverr for this hop) - Extract M3U8 URL via 3 strategies: direct regex match, JS deobfuscation (eval-unwrap +
JsPackerunpacker forp,a,c,k,e,dpacked scripts),<source>tag fallback - Parse M3U8: follow
#EXT-X-STREAM-INFmaster playlist variants (last = highest quality), extract#EXT-X-KEYfor AES-128 keys - Fetch AES key (cached with 1-hour TTL), download segments, decrypt in-memory, write atomically
FFmpeg concat demuxer: ffmpeg -f concat -safe 0 -i concat.txt -c copy -movflags +faststart out.mp4. Falls back to pipe mode (cat segments | ffmpeg -i pipe:0) if concat demuxer fails on malformed TS.
5 attempts per request with exponential backoff: 0.5s * 2^attempt. Applied to segment fetches, Kwik resolution, and FlareSolverr API calls. FlareSolverr sessions are auto-recreated on "session not found" errors.
src/pahebatcher/
__init__.py Version and package metadata
__main__.py python -m pahebatcher entry point
main.py CLI argument parsing, service wiring, action dispatch
models.py EpisodeInfo, AnimeInfo, StreamInfo, AppContext
config.py Constants (workers, retry, timeouts, version)
config_manager.py Persistent TOML config (validation, clamping, example)
utils.py sanitize, ep_prefix, fmt_bytes, compact_ep_range
tls.py Hardened SSL context (TLSv1.2+, forward secrecy)
cache.py Async-safe TTLCache with asyncio.Lock and LRU eviction
store.py SegmentStore (atomic writes, ffmpeg assembly, orphan cleanup)
http.py HttpClient (shared aiohttp session, curl fallback, retry)
solver.py FlareSolverr client (instance-based, TTL cache, session rotation)
extract/
kwik.py JsPacker, Kwik URL extraction, stream resolution
m3u8.py HLS manifest parser, AES-128 key extraction
scanner.py AnimePahe API scanner, URL validation, variant discovery
downloader.py EpisodeDownloader, BatchOrchestrator (2-stage pipeline)
stream.py MPV stream player with live panel and SUB/DUB navigation
sessions.py Session manager (list, resume, delete, clear)
ui/
console.py Rich console instance and ASCII art banner
dashboard.py Live progress dashboard with per-episode state transitions
tables.py Table rendering helpers (episodes, search results, summary)
prompts.py Episode selection, download confirmation, settings wizard
make test # run all 195 tests
make lint # ruff check (0 errors)
make typecheck # mypy strict (0 errors)
make benchmark # full coherence benchmark: tests + lint + typecheck + coverage
make clean # remove venv, caches, build artifactspip install -e ".[dev]"
pytest tests/ -v # 195 tests, asyncio auto-mode
pytest tests/ --cov=pahebatcher --cov-report=term # with coverage
ruff check src/ # ALL rule select, target py311, 0 errors
mypy src/ # strict mode, full type coverage, 0 errorstests/
conftest.py Fixtures: mock solver, mock http, sample data
test_cache.py 8 tests: set/get, eviction, expiry, concurrent access
test_config_manager.py 10 tests: defaults, save/load, validation, clamping, existence
test_config_extended.py 6 tests: constants, validation edge, roundtrip, corrupted TOML
test_kwik.py 7 tests: JsPacker, M3U8 extraction, resolution buttons
test_m3u8.py 8 tests: playlist parsing, AES keys, variant detection
test_models.py 11 tests: dataclass fields, properties, edge cases (incl. cookie_str)
test_prompts.py 11 tests: episode range parsing, noninteractive selection
test_scanner.py 12 tests: URL validation, episode page parsing, search
test_scanner_extended.py 7 tests: cache path, TTL expiry, glob-stable cache, scan fallback
test_sessions.py 4 tests: cache listing, metadata, error handling
test_solver.py 16 tests: cookie parsing, lifecycle, ping, cache, fetch_json/html
test_http.py 8 tests: thread-local session, lifecycle, retry, 403 fallback, curl fetch
test_tls.py 4 tests: TLS version, verify mode, ciphers, compression
test_store.py 7 tests: segment I/O, atomic writes, assemble, cleanup
test_store_extended.py 7 tests: atomic tmp handling, metadata, concat cleanup, pipe fallback
test_downloader.py 7 tests: _find_existing, skip-existing, mocked download flow
test_retry.py 4 tests: episode/retry (3 attempts), resolver retry, batch auto-retry
test_ui.py 5 tests: episode/search/summary tables, dedup
test_utils.py 20 tests: sanitize, ep_prefix, fmt_bytes, compact_ep_range
test_utils_extended.py 21 tests: fmt_bytes precision, sanitize edge, ep_prefix, compact ranges
test_coherence.py 7 tests: benchmark (ruff/mypy green), version, config coherence
─────────────────────────────────────────────────────────
195 total
All tests run under PYTHONPATH=src pytest tests/ -v or with the package installed via pip install -e ..
- Python 3.11+ with
from __future__ import annotationsthroughout - mypy strict -- no untyped defs, no implicit optionals, full type coverage (0 errors)
- ruff ALL -- pycodestyle + isort + pep8-naming + pyupgrade + bugbear + simplify + ruff-specific (0 errors)
- Dependency injection --
AppContextcarries config through the call chain;Solver,HttpClientare constructor-injected - Zero global mutable state -- no module-level caches, no classmethod singletons, no import-time side effects
make benchmark (or python scripts/benchmark.py) runs the coherence suite:
ruff: 0 errors
mypy: 0 errors
pytest: 195 passed
coverage: 52% (1941 stmts, 938 missed — scrapers/downloader/stream require network/mocks)
loc: 3025 src, 1879 tests
density: 6.44 tests / 100 LOC
version: 3.3.0 coherent across pyproject.toml / config.py / __init__.py
Shared AES cache, atomic segment writes, and glob-stable scan cache are covered by the extended tests.
| Symptom | Likely cause | Fix |
|---|---|---|
FlareSolverr not responding |
FlareSolverr container not running | docker run -d --name=flaresolverr -p 8191:8191 ghcr.io/flaresolverr/flaresolverr |
No Kwik link found on episode page |
Site structure changed or Cloudflare blocking | Try a different quality value; ensure FlareSolverr has a healthy browser session. Alternatively set cookie_string with your browser's cf_clearance cookie |
Resolution timed out |
FlareSolverr overloaded or slow network | Retry with fewer concurrent episodes (-j 1); the resolver retries 5 times per episode |
| Segment download failures | CDN rate limiting | Lower -w to 8-12, lower -j to 1 |
MPV not found |
MPV not installed or not in PATH | Install via package manager or use Download mode instead |
ffmpeg command not found |
FFmpeg not installed | sudo apt install ffmpeg / brew install ffmpeg |
AES-128 stream detected |
pycryptodomex not installed (should not occur with pip install) |
pip install pycryptodomex |
Playback ended immediately in MPV |
MPV HTTP/1.1 403 (CDN requires HTTP/2 headers) / vo missing |
Fixed in 3.3.0 via local HLS proxy (src/pahebatcher/stream.py:60); if still, test mpv --no-config --vo=null --ao=null --ytdl=no http://127.0.0.1:port/uwu.m3u8 and check stderr with --verbose |
ARM64 (OrangepiZero3) vo/hwdec fail / slow software render |
Mali/Panfrost not detected, mpv minimal build |
sudo apt install mpv ffmpeg ca-certificates; try mpv --hwdec=no --vo=gpu or --vo=drm --ao=alsa; Armbian kernel recompile may be needed for GPU |
| Slow single-episode downloads | Low segment concurrency | Increase -w to 24-32; HLS segments are ~100 KB each and benefit from high parallelism |
| Cache directory growing too large | Old sessions accumulating | Use Session Manager (option 3 from main menu) to clear stale entries; >24h orphans are auto-cleaned |
Bug reports, feature requests, and pull requests are welcome.
- Fork the repository
- Create a feature branch (
git checkout -b feature/description) - Install dev dependencies:
pip install -e ".[dev]" - Make changes; ensure
make lintandmake typecheckpass - Add or update tests; ensure
make testpasses (195 tests, asyncio auto-mode) - Commit with a conventional prefix (
fix:,feat:,refactor:,docs:,chore:) - Push and open a pull request
Areas open to contribution:
- PyPI publication pipeline
- Graceful shutdown on Ctrl+C (finish active downloads before exit)
- Additional anime source support
- CI/CD with GitHub Actions
- Arrow-key navigable TUI menus
MIT License. See LICENSE for full text.
This tool is intended for personal and educational use. Users are responsible for complying with the terms of service of the websites they access. Support official creators.