Skip to main content

PyPI version Python 3.12+ License: MIT CI Code style: ruff

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> to safari fetch <ID>. Run safari --help for the new commands. The old scripts remain as _safaribooks_legacy.py for reference only.


safari fetch demo

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

  1. Set up authentication -- paste cookies from your browser:

    safari auth setup
    
  2. Download a book by ID:

    safari fetch 9781491958698
    
  3. Download an entire playlist:

    safari fetch --playlist 6f612b99-bebc-41e1-8fff-6b655507b7af
    
  4. 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.json contains 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 9781491958698 to 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)

Source distribution for safaribookshelf 0.2.0
File Size Uploaded
safaribookshelf-0.2.0.tar.gz 92.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for safaribookshelf 0.2.0
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page