safaribooks
Download O'Reilly books as EPUB files. Async. Cookie-based auth that actually works.
Note
For personal and educational use only. Please read O'Reilly's Terms of Service.
Upgrading from v1? The CLI moved from
python3 safaribooks.py <ID>tosafari fetch <ID>. Runsafari --helpfor the new commands. The old scripts remain as_safaribooks_legacy.pyfor reference only.
Text fallback (if the GIF doesn't load)
$ safari auth setup
Paste cookies (JSON, header, or extension export):
{"groot_sessionid":"abc...","logged_in":"y","jwt":"eyJ...","csrf_access_token":"tok_..."}
Cookies saved to ~/.config/safaribooks/cookies.json (4 cookies)
$ safari fetch 9781491958698
Downloading 1 book(s)...
──── Book 1/1: 9781491958698 ────
Fetching book info... ━━━━━━━━━━━━━━━━━━━━ 100% 0:00:01
Downloading chapters (53)... ━━━━━━━━━━━━━━━━━━━━ 100% 0:00:12
Downloading CSS (2 files)... ━━━━━━━━━━━━━━━━━━━━ 100% 0:00:01
Downloading images (142)... ━━━━━━━━━━━━━━━━━━━━ 100% 0:00:08
Building EPUB... ━━━━━━━━━━━━━━━━━━━━ 100% 0:00:01
Saved: Books/Test-Driven Development with Python 2nd Edition.epub
──── Summary ────
Downloaded: 1 book(s)
Install
uv (Recommended)
uv tool install safaribookshelf
pip
pip install safaribookshelf
Docker
docker build . -t safaribooks
# Extract cookies on host first:
safari auth setup
# Run:
docker run --rm \
-e SAFARI_COOKIES_FILE=/app/cookies.json \
-v $(pwd)/cookies.json:/app/cookies.json \
-v $(pwd)/Books:/app/Books \
safaribooks 9781491958698
Quick Start
-
Set up authentication -- paste cookies from your browser:
safari auth setup
-
Download a book by ID:
safari fetch 9781491958698
-
Download an entire playlist:
safari fetch --playlist 6f612b99-bebc-41e1-8fff-6b655507b7af
-
Search by title:
safari fetch "Python Cookbook"
Why This Exists
O'Reilly killed programmatic login years ago, and the v1 API that the original safaribooks tool relied on was eventually shut down. The upstream repo is unmaintained and no longer functional.
This fork synthesized 20+ community PRs into a modern async rewrite: httpx instead of requests, tenacity for retries, a token-bucket rate limiter, Pydantic v2 config, and a proper typer CLI. It migrated the entire API surface from v1 to v2 and unified cookie authentication into a single workflow.
See the archived contributors page for full credit.
CLI Reference
Commands
| Command | Description |
|---|---|
safari fetch <IDs/URLs/titles> |
Download books by ID, URL, or title search |
safari fetch --playlist UUID |
Download all books from a playlist |
safari fetch --file list.txt |
Batch download from a file |
safari auth setup |
Interactive cookie paste (auto-detects format) |
safari auth extract --browser chrome |
Auto-extract cookies from browser |
safari auth import --header "Cookie: ..." |
Import from raw cookie header |
safari auth import --file cookies.json |
Import from file (JSON or extension format) |
safari auth validate |
Check if cookies are still valid |
safari auth status |
Show cookie file location and info |
Fetch Options
| Option | Default | Description |
|---|---|---|
--kindle |
off | Add Kindle-compatible CSS for e-readers |
--output / -o |
Books/ |
Output directory |
--library-dir |
~/.safaribooks/ |
Central library for collected EPUBs |
--rate-limit / -r |
1.0 |
Max requests per second (0=unlimited) |
--rate-burst |
2 |
Rate limiter burst capacity |
--image-max-size |
0 |
Resize images if dimension exceeds N pixels (0=no resize) |
--image-quality |
0 |
JPEG compression quality 1-95 (0=keep original) |
--ssl-skip |
off | Skip SSL certificate verification |
--preserve-log |
off | Keep log file even without errors |
--debug |
off | Enable debug logging |
Authentication Methods
O'Reilly blocks programmatic login. You need cookies from an active browser session.
Step 1: Log in at https://learning.oreilly.com in your browser.
Step 2: Get cookies via one of these methods:
Method 1: Auto-Extract from Browser (Recommended)
pip install browser_cookie3
safari auth extract --browser chrome
Also supports: firefox, edge, chromium.
Auto-refresh: Set SAFARI_AUTO_REFRESH_BROWSER=chrome (or your browser) to automatically re-extract cookies when they expire mid-download — no manual intervention needed.
Method 2: Interactive Paste
safari auth setup
Auto-detects JSON dict, browser extension export, or raw cookie header. Get JSON from your browser console:
JSON.stringify(
document.cookie.split(";").reduce((o, c) => {
c = c.trim();
let i = c.indexOf("=");
o[c.substring(0, i)] = c.substring(i + 1);
return o;
}, {}),
);
Method 3: Raw Cookie Header
safari auth import --header 'Cookie: k1=v1; k2=v2; ...'
Method 4: Browser Extension Export
safari auth import --file exported_cookies.json
Method 5: Manual JSON
Create ~/.config/safaribooks/cookies.json:
{
"groot_sessionid": "...",
"logged_in": "y",
"jwt": "...",
"csrf_access_token": "..."
}
Required cookies: groot_sessionid, jwt, csrf_access_token, logged_in.
Validate: safari auth validate
Troubleshooting
| Problem | Solution |
|---|---|
| "Out-of-Session" error | Cookies expired (~2 hours). Re-extract, or set SAFARI_AUTO_REFRESH_BROWSER to auto-recover. |
| "No cookies found" | Make sure you're logged in at learning.oreilly.com |
| Browser extract fails | Close browser first, or use paste mode |
| Download interrupted | Session expired mid-download. Keepalive pings run automatically every 5 min to prevent this. |
Security warning:
cookies.jsoncontains your active session -- treat it like a password. It's in.gitignore.
Configuration
All settings can be set via environment variables with the SAFARI_ prefix (powered by pydantic-settings):
| Env Var | CLI Flag | Default | Description |
|---|---|---|---|
SAFARI_COOKIES_FILE |
-- | ~/.config/safaribooks/cookies.json |
Cookie file path |
SAFARI_OUTPUT_DIR |
--output |
Books/ |
Output directory |
SAFARI_LIBRARY_DIR |
--library-dir |
~/.safaribooks/ |
Central library |
SAFARI_KINDLE |
--kindle |
false |
Kindle mode |
SAFARI_RATE_LIMIT |
--rate-limit |
1.0 |
Requests per second |
SAFARI_RATE_BURST |
--rate-burst |
2 |
Burst capacity |
SAFARI_AUTO_REFRESH_BROWSER |
-- | None |
Auto re-extract cookies on expiry (chrome/firefox/edge/chromium) |
SAFARI_KEEPALIVE_INTERVAL |
-- | 300 |
Seconds between session keepalive pings during downloads (0=disabled) |
SAFARI_DEBUG |
--debug |
false |
Debug logging |
Architecture
flowchart TD
CLI["CLI (Typer)"] --> BD["BookDownloader"]
BD --> Auth["check_login()"]
BD --> Meta["fetch_book_info()"]
Meta --> Enrich["enrich_metadata()"]
BD --> Chapters["fetch_chapters()"]
Chapters --> Process["process_chapters()"]
Process --> CSS["download_css()"]
CSS --> Fonts["download_fonts()"]
Fonts --> Images["download_images()"]
Images --> Videos["download_videos()"]
Videos --> EPUB["build_epub()"]
BD -.-> API["ApiClient (httpx)"]
API --> RL["TokenBucketRateLimiter"]
API --> Retry["RetryConfig (tenacity)"]
Docker Usage
The Docker image uses uv and runs safari fetch as the entrypoint:
# Build the image
docker build . -t safaribooks
# Extract cookies on host first
safari auth setup
# Run a download
docker run --rm \
-e SAFARI_COOKIES_FILE=/app/cookies.json \
-v $(pwd)/cookies.json:/app/cookies.json \
-v $(pwd)/Books:/app/Books \
safaribooks 9781491958698
To pass additional flags, append them after the book ID:
docker run --rm \
-e SAFARI_COOKIES_FILE=/app/cookies.json \
-v $(pwd)/cookies.json:/app/cookies.json \
-v $(pwd)/Books:/app/Books \
safaribooks 9781491958698 --kindle --rate-limit 0.5
Calibre / E-Reader Tips
The EPUB generated by safari fetch is functional but contains raw HTML/CSS from O'Reilly. For the best reading experience, convert it with Calibre:
ebook-convert \
"Books/Test-Driven Development with Python 2nd Edition.epub" \
"Books/Test-Driven Development with Python 2nd Edition_clean.epub"
For Kindle users:
- Use
safari fetch --kindle 9781491958698to add CSS rules that prevent table and code block overflow on e-ink screens. - Convert to AZW3 or MOBI with Calibre. When converting, select Ignore margins in the conversion options for best results.
Contributing
uv sync # install deps
just check # lint + format + type check
just test # run tests
See CONTRIBUTING.md for the full guide.
Credits
Originally created by Lorenzo Di Fuccia. This fork synthesized 20+ community contributions -- see full credits.
Release files for safaribookshelf 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| safaribookshelf-0.2.0.tar.gz | 92.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| safaribookshelf-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 218.0 kB
Release files / safaribookshelf-0.2.0.tar.gz
| Download URL | safaribookshelf-0.2.0.tar.gz |
|---|---|
| Size | 92.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9e818b2d4dc32c7e696b745c402732dc616a52249d9db8bd62833eb1c6f35dad
|
|
BLAKE2b-256 checksum How to use checksums |
fbe0d77ba8284b1b97b2eff3384a3bca3814b9e59d6f1aca70bbd04eb456208d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 23, 2026.
Transparency logRelease files / safaribookshelf-0.2.0-py3-none-any.whl
| Download URL | safaribookshelf-0.2.0-py3-none-any.whl |
|---|---|
| Size | 125.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6cbef8314100391831761b271016eb62f5438441a315b9875b866126c79cc8d1
|
|
BLAKE2b-256 checksum How to use checksums |
09c4170c5ee992f6cffc0981a3766fb3ff0c35a9345121e1ed1af6c0f52ebdba
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 23, 2026.
Transparency log