Skip to content
mavrovdePublic

About

Back up your own purchased litres.ru library (books & audiobooks) — as a zip or a plain folder mirror that only fetches what's missing. Local-only, on your machine: desktop app, web app, and MCP server for Claude.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

📚 BookVault

Own the books you bought.
Back up your own purchased litres.ru library — books & audiobooks — entirely from your own machine.
Browse what you own, pick the titles and format you want, and download them as a zip — with live progress and a stop button.

CI Docker images Python 3.11+ Latest release Runs 100% local License: MIT with attribution

Important

Your own books only — no rights are broken. This tool logs in with your own litres.ru account and can download only the titles you have legally purchased — nothing else is reachable. It makes personal backup copies of content you already own (format-shifting), using your own authenticated session and the site's own download endpoints. It does not crack DRM, bypass payment, or grant access to anything you haven't bought. Keep your downloads for personal use and don't redistribute them. See Legal & fair use.

Note

Not affiliated with LitRes. BookVault is an independent, unofficial project and is not affiliated with, endorsed by, or sponsored by ООО «ЛитРес» (LLC "LitRes"). "LitRes", "ЛитРес", and "litres.ru" are trademarks of their owner, used here only to describe compatibility. See Trademarks and TRADEMARKS.md.

It comes in three flavours that share the same backend and login/session code:

  • 🖼️ A native desktop app (macOS / Windows / Linux) — the web app in a real window, no browser or terminal. Download for macOS.
  • 🖥️ A local web app — browse your library, tick the books you want, choose a format, and download.
  • 🔌 An MCP server — so Claude (or any MCP client) can list your library and download titles as tools.
  • 📁 On-disk library sync — optional LITRES_LIBRARY_DIR writes an Audiobookshelf-friendly Author/Title/metadata.json tree (same shape as litres-downloader), via ⬇ Sync to Audiobookshelf or background autosync.

Contents


✨ Features

  • 📖 Books & 🎧 audiobooks — pick exactly which titles to back up, with cover thumbnails, authors, and file sizes.
  • 🎯 Format of your choice — set a preferred ebook format (epub, fb2, pdf, …) and audiobook format, with sensible fallbacks per title.
  • 📦 One tidy zip — ebooks as single files, each audiobook as a folder of its tracks; packed so macOS Archive Utility opens it cleanly.
  • 🗂 …or a plain folder mirror — download straight into your save folder with no archive around it. Re-run it and only what's missing is fetched; a ✓ on each cover shows what you already have, and the zip builder reuses those files instead of asking litres.ru twice.
  • 📁 Saved where you want it — the finished archive lands in your Downloads folder by default, or any folder you point it at, chosen in your own OS folder dialog rather than typed by hand; timestamped, so a new build never overwrites the last one. Afterwards you can drop an extra copy somewhere else (an external drive, a shared folder) without touching the saved original.
  • ⏳ Live progress + Stop — per-file bytes (12.3 / 45.0 MB) and the whole-build total (~120.0 MB of ~600.0 MB), so a single huge audiobook doesn't leave you guessing how far along you are. Stop interrupts even a mid-transfer download.
  • ✅ Results at a glance — when a build finishes, a summary shows ✓ done · ! skipped · ✗ failed as clickable filters, so a single rights-limited title never hides among hundreds of successes. The results and the download link survive a page reload.
  • 🔄 Same view in every browser — your selection, format choices, save folder, and live progress live on the server (not per-browser), so a second browser or tab — or another device on your machine — shows exactly the same thing.
  • 🛡️ Anti-bot resilient — matches the browser's TLS fingerprint on downloads and retries transient DDoS-Guard blocks automatically (details below).
  • ⚡ Smart caching — your library and file listings are cached on disk, so reloads and restarts stay fast and gentle on litres.ru.
  • 🔒 Local & private — your password lives in your OS keychain (or nowhere, in Docker); nothing is sent anywhere but litres.ru.
  • 🖼️ Native desktop app — the same app in a real window on macOS, Windows, and Linux (no browser tab, no terminal); it starts and stops its own backend. See Desktop app.
  • 🐳 Docker-ready — two published images and a one-command docker compose up.

🚀 Quick start

Pick whichever fits you.

Option A — Download the macOS app (no terminal, no Python)

Grab BookVault.dmg from the latest release, open it, and drag BookVault to Applications. First launch: right-click the app → Open (it's an unsigned build, so Gatekeeper asks once), then log in. On first run it downloads the browser engine it needs (~150 MB, one time) — a splash shows the progress. See Desktop app.

Option B — Docker (easiest cross-platform, nothing to install but Docker)

git clone https://github.com/mavrovde/bookvault.git
cd bookvault
docker compose up -d

Open http://127.0.0.1:8420, log in with your litres.ru account, and you're set. Full details in Running in Docker.

Option C — Run it locally (Python 3.11+)

git clone https://github.com/mavrovde/bookvault.git
cd bookvault
python3 -m venv .venv
.venv/bin/pip install -e ./core -e ./web        # the web app + its shared core
.venv/bin/playwright install chromium           # one-time browser download
.venv/bin/bookvault-web

Then open http://127.0.0.1:8420 and log in. Your password is remembered in your OS keychain, so you won't have to log in again next time.

💡 Don't have Python? Grab it from python.org. On macOS/Linux it's often already installed.


🖥️ Using the web app

  1. Log in once with your litres.ru email and password.
  2. Browse & filter your library — search by title/author, filter books vs. audiobooks, sort by title/author/size.
  3. Select the titles you want (nothing is pre-selected, so you never start a huge download by accident).
  4. Pick a format (optional) — your preferred ebook and audiobook formats, used when available.
  5. Pick a save folder (optional) — the 📁 box in the toolbar. Hit Browse… to choose it in your operating system's own folder dialog, or type a path. Leave it empty to use your system Downloads folder. A folder that doesn't exist yet is fine — it's created when a build finishes, and the app says so. (Browse… is hidden when the app can't open a dialog, e.g. in Docker or over SSH; type the path there.)
  6. Prepare zip — watch the live progress bar; hit Stop anytime. Alongside the book count you get the whole-build total (~120.0 MB of ~600.0 MB), so you can see how far along the download really is — a single audiobook can outweigh fifty ebooks, which the book count alone never shows. The total is an estimate (marked ~) because it sums the sizes already known; see size estimates.
  7. Review results — the summary tallies ✓ done · ! skipped · ✗ failed; click a pill to filter to just those (e.g. the one rights-limited title that couldn't be downloaded).
  8. Find your zip — it's saved to your chosen folder automatically, and the path is shown under the progress bar. Two extra buttons appear next to it, neither of which moves or replaces that saved file:
    • ⬇ Download — streams it through your browser, landing wherever your browser saves downloads.
    • 💾 Save a copy to… — picks a folder in your OS dialog and puts an additional copy there (an external drive, a shared folder). An existing file of the same name is never overwritten. Hidden where no dialog can be drawn (Docker, SSH).

Your selection, format choices, save folder, and progress are kept on the server, so opening the app in another browser/tab shows the same view — and the results and download link stick around after a reload.

Or skip the zip: download as plain files

⬇ Download as files, beside Prepare zip, puts the selected books straight into your save folder as ordinary files — ebooks as files, each audiobook as a folder of its tracks. The result is a plain mirror of your library you can browse, back up, or point any reader at, with no archive to open first.

Running it again only fetches what's missing. Each book is reported as Downloaded, Re-downloaded, or Already saved, and any book you already have shows a ✓ on its cover while you browse. Prepare zip reuses those files too, packing them straight into the archive (From your folder) rather than asking litres.ru for something already on your disk.

How it knows. A small .bookvault-index.json beside your files records what each finished download actually wrote; a book still matching its record is skipped, and one that's been truncated is fetched again. It deliberately does not trust the file size litres.ru advertises — that number doesn't match the bytes the site serves, so relying on it would re-download nearly your whole library every run. Books you put in the folder yourself have no record and are left alone. Delete the index and everything present is simply trusted; delete a book and the next run brings it back.

The archive is named litres-library-<date>-<time>.zip, so a new build never overwrites one you still want. Nothing is written to your folder until a build actually succeeds; if the folder turns out to be unwritable, the zip stays available via the download button instead of being lost.

Opening the zip: double-click it (Finder / Archive Utility) or any modern tool. ⚠️ macOS's built-in Terminal unzip garbles Cyrillic filenames — extract via Finder, or run ditto -x -k litres-library.zip dest/ for correct names.

A note on sizes

Sizes are shown per book and totalled for your selection. They're read from a local cache, and the app never contacts litres.ru just to show you a number: asking for a file listing per book on every page load is exactly the traffic pattern that gets an account flagged as a bot.

The consequence is that sizes appear as you go rather than all at once:

  • A freshly-restored session may show sizes unknown — hit ↻ Refresh. That's literal, not a stalled spinner: nothing is loading, and Refresh is what actually fetches them (it's paced deliberately).
  • Once fetched, sizes are cached for a week and survive restarts, page reloads and downloads.
  • During a build the total is written ~120.0 MB of ~600.0 MB. The ~ is honest: it can only sum books whose sizes are already known, so the real total may come in higher. Each finished file corrects the figure with its true size as it lands.

🖼️ Desktop app (macOS / Windows / Linux)

Prefer a real app window to a browser tab? bookvault-desktop runs the same web app inside a native OS window (WKWebView on macOS, WebView2 on Windows, WebKitGTK on Linux) via pywebview — no browser, no terminal to keep open. It starts the backend for you: the app launches the web server on a private 127.0.0.1 port in the background, shows a brief splash while your saved session restores, then loads the app — and closing the window shuts the backend down cleanly. It reuses bookvault-web verbatim (the backend is imported, not duplicated), so it's the same library browser, formats, progress, and results as the web app.

Install (macOS)

Download BookVault.dmg from the latest release, open it, and drag BookVault to Applications.

  • First launch: it's an unsigned build, so macOS Gatekeeper blocks a plain double-click. Right-click the app → Open → Open (once), or clear the quarantine flag: xattr -dr com.apple.quarantine /Applications/BookVault.app.
  • First run downloads the browser engine it needs for the litres.ru login (~150 MB, one time) — a splash shows progress. After that, launches are quick and offline until you log in.
  • Your session, cache, and settings live in ~/Library/Application Support/BookVault/; downloads go to ~/Downloads/litres-library/.

Install (Windows)

Download BookVault-Setup-<version>.exe from the latest release and run it. It installs BookVault to Program Files with a Start-menu entry (and an optional desktop shortcut).

  • First launch of the installer: it's an unsigned build, so Windows SmartScreen shows a blue "Windows protected your PC" dialog — click More info → Run anyway. (Requires the WebView2 runtime, which ships with Windows 11 and up-to-date Windows 10.)
  • First run of the app: BookVault downloads Chromium (~150 MB) once — the splash shows progress — cached under %LOCALAPPDATA%\ms-playwright (survives reinstalls). App data lives in %LOCALAPPDATA%\BookVault.

Install (Linux)

Download BookVault-<version>-x86_64.AppImage from the latest release, make it executable, and run it:

chmod +x BookVault-*-x86_64.AppImage
./BookVault-*-x86_64.AppImage
  • Runtime prerequisite — WebKitGTK 4.1. The window renders with your system's WebKitGTK, which is not bundled (WebKit's multiprocess helpers use compile-time absolute paths that can't live inside an AppImage). On Ubuntu 24.04+ / Debian, one line covers it:
    sudo apt-get install -y libwebkit2gtk-4.1-0 gir1.2-webkit2-4.1 gir1.2-gtk-3.0
    (Distros that still ship only the WebKit2GTK 4.0 ABI aren't supported — this build targets 4.1.)
  • FUSE. If the AppImage won't mount, sudo apt-get install -y libfuse2t64 (or libfuse2 on older releases), or run it with --appimage-extract-and-run.
  • First run downloads Chromium (~150 MB) once (splash shows progress); on a minimal install it also needs a few libs: sudo apt-get install -y libnss3 libatk-bridge2.0-0 libxkbcommon0 libgbm1 libasound2.
  • App data lives in ~/.local/share/BookVault/; downloads go to ~/Downloads/litres-library/.

Homebrew / winget / AUR channels are still on the way.

Verify your download

The installers are unsigned — there's no Apple Developer ID or Authenticode certificate behind them, which is why macOS and Windows warn on first launch. What you get instead is verifiable evidence of origin: every release artifact carries a build provenance attestation — a signed statement, recorded publicly, that this exact file was produced by this repository's build workflow from a specific commit.

Check it with the GitHub CLI (nothing to install beyond gh):

gh attestation verify BookVault-1.4.0.dmg --repo mavrovde/bookvault

That confirms the file was built by BookVault's own CI from a tagged commit — so a copy tampered with in transit, or re-hosted elsewhere, fails the check. The same works for the .exe, the .AppImage, and the Docker images:

gh attestation verify oci://ghcr.io/mavrovde/bookvault/web:1.4.0 --repo mavrovde/bookvault

Each release also ships a SHA256SUMS file if you'd rather just confirm the download is intact:

sha256sum -c SHA256SUMS      # macOS: shasum -a 256 -c SHA256SUMS

What this does and doesn't buy you. Provenance proves origin; a checksum proves integrity. Neither is a code-signing certificate, so neither silences Gatekeeper or SmartScreen — see the per-OS notes above for those. Verification is worth doing precisely because the builds are unsigned.

Run from source (any OS)

.venv/bin/pip install -e ./core -e ./web -e ./desktop
.venv/bin/playwright install chromium        # if you haven't already
.venv/bin/bookvault-desktop

Build the macOS app yourself

PyInstaller bundles it into a .app + .dmg (Chromium is fetched on first run, so the installer stays ~80 MB):

.venv/bin/pip install pyinstaller
packaging/macos/build.sh                     # -> packaging/macos/dist/BookVault-<version>.dmg

CI (.github/workflows/desktop-macos.yml) runs this on every release tag and attaches the .dmg to the GitHub Release.


🔌 Using it from Claude (MCP)

The MCP server exposes your library to any MCP client (e.g. Claude Desktop) as tools:

Tool What it does
login_status() Whether there's an active session
login_to_litres(login, password) Log in and persist the session
list_library(limit) List your purchased titles with metadata (authors, narrators, series, dates, DRM flag)
download_book(art_id) Download one title (into LITRES_LIBRARY_DIR when set, else flat LITRES_DOWNLOAD_DIR)
sync_library_now(audio_only) Sync purchased titles into LITRES_LIBRARY_DIR (Audiobookshelf layout)

Install & run (stdio):

.venv/bin/pip install -e ./core -e ./mcp
.venv/bin/playwright install chromium
.venv/bin/bookvault-mcp        # or: python -m bookvault_mcp.server

It speaks the MCP stdio protocol, so you point a client at it rather than running it by hand. Claude Desktop config:

{
  "mcpServers": {
    "bookvault": {
      "command": "/path/to/bookvault/.venv/bin/bookvault-mcp",
      "cwd": "/path/to/bookvault"
    }
  }
}

Prefer to run it in a container (over HTTP)? See Pointing an MCP client at the container. More detail lives in mcp/README.md.


🐳 Running in Docker

Two images are published to the GitHub Container Registry on every release — one for the web app, one for the MCP server — both built on the official Playwright image (Chromium included):

Image Purpose
ghcr.io/mavrovde/bookvault/web The web app
ghcr.io/mavrovde/bookvault/mcp The MCP server

Each release tag (v0.10.0, …) publishes images tagged with that version, plus latest.

Compose — run & control both

docker compose up -d            # start web + mcp (pulls the ghcr images)
# or build locally:  docker compose up -d --build

(Older Docker installs use the hyphenated docker-compose command — same thing.)

Open http://127.0.0.1:8420 and log in. Everyday controls:

docker compose logs -f web      # follow the web app's logs
docker compose stop mcp         # stop just the MCP server
docker compose down             # stop both (the named volume persists)

Both services share one named volume (litres-data at /data), so logging in through the web app also authenticates the MCP server — they read the same saved session. Your library cache and downloads persist there across restarts.

🔒 Localhost-only, on purpose

The containers bind 0.0.0.0 internally, but Compose publishes their ports to 127.0.0.1 only — so nothing beyond your own machine can reach them. This preserves the app's single-user, localhost-only design. Don't change those port bindings to expose it.

🔑 Credentials in a container

There's no OS keychain in a headless container, so the password is not stored — keyring gracefully degrades to session-only. The saved browser session (cookies, on the /data volume) keeps you logged in across restarts for weeks; when it finally lapses you just log in again through the web form. Nothing sensitive is written to the image or the volume.

Prefer the MCP server to bootstrap headlessly from credentials

Set LITRES_LOGIN / LITRES_PASSWORD (e.g. in a local .env that Compose reads). The shared-session route above needs no stored password at all, so this is optional.

Pointing an MCP client at the container

The containerized MCP server speaks streamable-http (not stdio), so point a client at its URL:

{ "mcpServers": { "bookvault": { "url": "http://127.0.0.1:8421/mcp" } } }

For a non-Docker setup the server still defaults to stdio (see above). docker run -i …/mcp with LITRES_MCP_TRANSPORT=stdio also works.


⚙️ Configuration

Copy .env.example to .env to override any defaults. All of it is optional — the app works out of the box.

cp .env.example .env

Credentials in .env are used by the MCP server only (it's headless and bootstraps a first session from them). The web app never reads them — you log in through its page, and the session is saved and reused.

All environment variables (click to expand)
Variable Default Purpose
LITRES_LOGIN / LITRES_PASSWORD — Credentials to bootstrap a session (MCP server only)
LITRES_APP_PORT 8420 Web UI port
LITRES_APP_HOST 127.0.0.1 Web bind host. Leave as-is locally; the Docker image sets 0.0.0.0 and publishes to 127.0.0.1 on the host. Don't set 0.0.0.0 outside a container
LITRES_RELOAD 1 Auto-reload the web server on code changes (dev). The Docker image sets 0
LITRES_MCP_TRANSPORT stdio MCP transport: stdio (client-launched) or streamable-http (the container's networked service)
LITRES_MCP_HOST / LITRES_MCP_PORT 127.0.0.1 / 8421 Bind host/port for the MCP streamable-http transport
LITRES_DOWNLOAD_DIR system Downloads folder Default save folder for a finished zip (web/desktop) and for the MCP server's download_book. In the web/desktop app the folder chosen in the toolbar overrides it
LITRES_SESSION_FILE .litres_session.json Where the browser session (cookies) is cached between runs
LITRES_CACHE_FILE .litres_cache.json Where the library/file-listing cache is stored
LITRES_STATE_FILE .litres_state.json Where the shared UI state (selected books + format prefs) is stored, so every browser sees the same view (web app only)
LITRES_LIBRARY_DIR (unset) Enables the on-disk library sync and sets its root. Unset means the feature is off — nothing is mirrored to disk
LITRES_AUTOSYNC 0 Set to 1 for a periodic background sync into LITRES_LIBRARY_DIR (web app; requires the var above)
LITRES_AUTOSYNC_INTERVAL 21600 (6 h) Seconds between autosync ticks, ±10% jitter. Floored at 15 min — a library only changes when you buy something
LITRES_AUTOSYNC_AUDIO_ONLY 1 Sync only audiobooks (the case Audiobookshelf cares about). 0 syncs ebooks too
LITRES_AUTOSYNC_ON_START 1 Run one sync shortly after the app boots
LITRES_LIBRARY_CACHE_TTL 900 (15 min) How long the cached library listing stays fresh
LITRES_FILES_CACHE_TTL 604800 (7 days) How long a book's cached file listing stays fresh
LITRES_DOWNLOAD_TIMEOUT_MS 300000 (5 min) Per-file download timeout (audiobook bundles can be ~2GB)
LITRES_LOGIN_PAGE_TIMEOUT_MS 30000 (30 s) Playwright timeout for loading the litres.ru login page (login, session restore, cookie re-warm)
LITRES_HEADLESS 1 Set 0 to watch the login flow in a real Chromium window (debugging)
LITRES_LOG_LEVEL INFO Log verbosity: DEBUG / INFO / WARNING / ERROR
LITRES_MAX_RETRIES 3 Retries on a transient anti-bot block (403 / 429 / 503) before giving up
LITRES_RETRY_BASE_DELAY 2.0 First backoff (seconds) on a block, doubled each retry
LITRES_RETRY_MAX_DELAY 30.0 Cap on any single backoff (seconds)
LITRES_SIZE_CHECK_PACE 0.2 Base gap (seconds, jittered up) between live per-book size fetches

.env is gitignored and never committed — see Security & privacy for where your credentials actually live.


🧠 How it works

A quick tour of the design choices that make this reliable. Skip it if you just want to use the app — expand a section if you're curious.

One state machine, on the backend. Everything the app can be doing — reloading the library, sweeping sizes, building the zip, downloading as files, cancelling — is a single state machine in activity/ (idle → refreshing / checking / preparing / downloading / syncing / stopping → idle). Only one activity runs at a time, which falls out naturally from a single dedicated Playwright worker thread. The package is split by domain — state.py owns the machine, and one module per activity owns the work — so a change to the zip build doesn't sit in the same file as the size sweep. The browser is a thin renderer: it POSTs an action, polls GET /activity, and paints whatever state it reports.

State lives on the server, not the browser. The current selection and format preferences sit in prefs.py (GET/POST /prefs, and folded into the /activity poll), and a finished build's per-book results + zip link are kept on the state machine until the next build. So opening the app in another browser shows the same view, and a page reload never loses your selection, the results, or the download link.

🗂 How "I already have this book" is decided

The folder mirror is only useful if running it twice is nearly free, which means answering "do I already have this?" correctly. The obvious approach — compare the file on disk against the size litres.ru's listing advertises — does not work: that number is not the byte length the site serves. Measured across a real 239-book library, 10 files matched their listed size and 147 did not, the delivered file usually being smaller by anything from a couple of hundred bytes to several megabytes. Trusting it would re-download almost the entire library on every run — precisely the request pattern that gets an account anti-bot flagged, and far worse than the stale-file problem it was meant to solve.

So the only trustworthy statement is the one BookVault can make about itself: a finished download records what it actually wrote into a small .bookvault-index.json beside your files (byte length for a single file, track count for an unpacked audiobook). Completeness is checked against that record, which is direct evidence and survives restarts, and it still catches the case that matters — a transfer interrupted part-way leaves something whose length differs from what the completed run recorded.

Two consequences worth knowing:

  • No record means "leave it alone." Books you copied in yourself have nothing to verify against, and re-fetching a library because BookVault lacks a history of it would be destructive. Delete the index and everything present is simply trusted; delete a book and the next run brings it back.
  • The delivered shape is inspected, not assumed. Only the zip_with_mp3 audiobook format is an archive to unpack; the others (mobile_version_mp4 and friends) are a single file, exactly like an ebook — and ebooks such as epub and fb2.zip are themselves zips that must never be unpacked. The rule is "unpack only an audiobook that really is a zip", decided by looking at the bytes.

One definition serves every question about a book's presence — the mirror's own run, the ✓ badge on each cover, the zip build's reuse, and the MCP download_book skip — so they cannot disagree.

🎭 Why Playwright instead of plain HTTP requests

litres.ru's login endpoint rejects plain scripted POSTs with a generic "incorrect credentials" error regardless of the password — the site sets DataDome-style anti-bot cookies (__ddg9_, __ddg1_) that only a real, JS-executing browser can obtain. So LitresClient drives an actual headless Chromium through the real login form.

Being logged in isn't enough either: the API needs several app-level headers (app-id, session-id, client-host, …) that the site's own frontend attaches to every call. Rather than guessing them, the client captures them once from a request the site's own JS fires right after login, and replays them.

Playwright's sync API is tied to whichever thread created it, so session.py funnels every call touching a LitresClient through one dedicated worker thread.

🛡️ Staying under DDoS-Guard's radar

litres.ru sits behind DDoS-Guard, which decides "bot or human" from more than cookies: it fingerprints the TLS handshake (JA3/JA4) and the HTTP request shape, and watches request cadence and IP. As a low-volume personal tool, the goal is simply to not trip those false-positive checks:

  • Downloads carry the same TLS fingerprint as the browser. API calls run inside Chromium, but downloads must stream to disk (audiobook bundles reach ~2GB) over a separate HTTP client. A plain-Python client's TLS fingerprint can be re-challenged even with valid cookies, so download_file uses curl_cffi impersonating Chrome — its JA3/JA4 matches the session that solved the challenge. (Falls back to httpx if curl_cffi isn't available.)
  • Transient blocks are retried, not hammered past. On a 403 / 429 / 503 the client honours Retry-After, backs off with jittered exponential delay, re-warms the __ddg* cookies via a quick page visit, and retries — instead of failing and immediately hitting the next request. A genuine rights-limited 403 carries no DDoS-Guard signature and isn't retried; instead the client automatically tries the subscription download endpoint.
  • No bulk sweeps on load. Opening the app resolves only cached sizes; live per-book fetches happen on an explicit Refresh, with jittered pacing.
  • Keep your IP stable. __ddg9_ encodes your public IP; flipping a VPN mid-session can force a re-challenge.
📦 Project layout
core/                 bookvault-core — shared library (own pyproject.toml)
  bookvault_core/
    client.py         Playwright-driven login + library/file/download calls
    session.py        login/session-restore + the single dedicated Playwright thread
    credentials.py    password storage via the OS keychain (keyring)
    cache.py          disk cache for library + per-book file listings
web/                  bookvault-web — the web app (depends on bookvault-core)
  bookvault_web/
    app.py            FastAPI: library browser, format defaults, activity control
    activity/         the one backend state machine, one module per domain:
      state.py          the machine itself — states, shared state, start/cancel
      library.py        reloading the listing + the paced size sweep
      mirror.py         the loose-file mirror, and "do I already have this?"
      archive.py        the zip build and the finished archive
      abs_sync.py       the Audiobookshelf-shaped on-disk library
    prefs.py          server-side shared UI state (selection + format prefs)
    run.py            starts uvicorn; the `bookvault-web` command
    templates/ static/  HTML + CSS + JS (no build step, no framework)
mcp/                  bookvault-mcp — the MCP server (depends on bookvault-core)
  bookvault_mcp/server.py  MCP tools; the `bookvault-mcp` command
desktop/              bookvault-desktop — native window (depends on bookvault-web)
  bookvault_desktop/app.py  embeds the web app in a pywebview window; `bookvault-desktop`
packaging/macos/      PyInstaller spec + build.sh → BookVault.app / .dmg
tests/                pytest suite — fully mocked, no real Playwright/network
Dockerfile.web        web-app image (Playwright base)
Dockerfile.mcp        MCP-server image
docker-compose.yml    runs + controls both together

Each subproject has its own pyproject.toml and dependencies: installing bookvault-web doesn't pull in the MCP SDK, and vice-versa. Both depend on bookvault-core.


🧪 Development & tests

# editable installs of the core/web/mcp subprojects + dev tooling (pytest, ruff)
# (add -e ./desktop to work on the desktop app -- it also runs its tests)
.venv/bin/pip install -e ./core -e ./web -e ./mcp -e ".[dev]"
.venv/bin/python -m pytest

The whole suite runs offline in under a couple of seconds: LitresClient is either bypassed (pure logic) or replaced with a fake (tests/fakes.py) — no real browser or network call happens. This includes an end-to-end smoke suite (tests/test_e2e_smoke.py) that boots the real server and drives the full login → build → download flow against the fake backend. CI (.github/workflows/lint-test-audit.yml) runs ruff, the test matrix (Python 3.11–3.13), and a dependency-vulnerability audit on every push/PR.

There's also an opt-in live smoke suite (tests/test_smoke_live.py) that hits a running instance over HTTP — deselected by default, run it against a started app with:

.venv/bin/python -m pytest -m live        # defaults to http://127.0.0.1:8420
# or point it elsewhere: BOOKVAULT_BASE_URL=http://127.0.0.1:8420 pytest -m live

And an opt-in browser-layer suite (tests/test_ui.py) that drives the rendered page through Chromium — the layer where a button can be perfectly wired server-side and still do nothing when clicked. It's deselected by default so the everyday suite stays fast and browser-free; CI runs it in its own job.

.venv/bin/playwright install chromium     # one-time
.venv/bin/python -m pytest -m ui

It stays offline like everything else: the app is served against the fake client, and constructing a real one is a test failure — so these can never reach litres.ru.

Contributing? See CONTRIBUTING.md for the setup, the rules that matter (offline tests, the single Playwright worker thread, the deliberate LITRES_* naming), and how AI-assisted PRs are reviewed. This repo checks in its Claude Code configuration — per-area agent roles and skills for the repeatable workflows — so everyone gets the same setup.

HTTP endpoints

The web app is also a small local API, so you can script it. Everything runs on 127.0.0.1:8420 with no authentication — see Localhost-only, on purpose.

Method Path Does
GET / The page itself (login form when logged out)
GET /activity Current state, progress, per-book log, prefs, and which books are already on disk — poll this
GET /library The cached library listing
GET /library/{art_id}/size Resolve one book's size
GET /prefs · POST /prefs Read / update selection, formats, save folder
POST /login · /logout Session
POST /activity/refresh-library Reload the listing from litres.ru
POST /activity/check-sizes Paced per-book size sweep
POST /activity/prepare-zip Build the archive
POST /activity/download-files Download into the save folder as plain files
POST /activity/sync-audiobookshelf Sync the Audiobookshelf-shaped tree (needs LITRES_LIBRARY_DIR)
POST /activity/stop Cancel whatever is running
GET /download/file Stream the finished archive
POST /download/save-copy Put an extra copy of the archive somewhere else
POST /prefs/browse Open the OS folder picker (hidden where no dialog can be drawn)

Only one activity runs at a time: a POST that starts one returns {"started": false} if another is already in flight, rather than queuing.

Writes are same-origin only. State-changing requests from another web page are refused (Sec-Fetch-Site, with an Origin/Host fallback), since the app has no auth and any page you have open could otherwise POST to it. Local scripts and curl send neither header and are allowed — the threat model is a web page you visited, not code already running as you.

curl -s http://127.0.0.1:8420/activity | python3 -m json.tool
curl -s -X POST http://127.0.0.1:8420/activity/prepare-zip \
     -H 'Content-Type: application/json' -d '{}'

⚖️ Legal & fair use

This tool is for making personal backups of books you have fairly bought on litres.ru — and nothing more.

  • ✅ Only your own purchases. It authenticates with your litres.ru account and can only reach titles you have legally bought. It cannot access, list, or download anyone else's library or any book you haven't purchased.
  • ✅ Personal backup / format-shifting. It saves copies of content you already own so you can keep and read them on your own devices.
  • ✅ The site's own endpoints, your own session. It uses litres.ru's normal download endpoints through your logged-in session — the same files the site would give you.
  • 🚫 No DRM circumvention, no piracy. It does not crack DRM, bypass payment, or unlock anything you haven't bought.
  • 🚫 Don't redistribute. The books remain the property of their rights holders — keep your downloads private and for personal use only.

You are responsible for using this tool in line with litres.ru's Terms of Service and the copyright law that applies to you. If in doubt, don't. The author provides this software as-is and accepts no liability for misuse (see License).

Trademarks

BookVault is an independent, unofficial project — not affiliated with, endorsed by, or sponsored by ООО «ЛитРес» (LLC "LitRes"). "LitRes", "ЛитРес", "litres.ru", and "litres.com" are trademarks or registered trademarks of their owner (ООО «ЛитРес», Moscow). BookVault uses the word "litres" only nominatively — to describe what it's compatible with — and uses none of LitRes's logos or branding. Official LitRes sites: litres.com · litres.ru · About LitRes. Full notice: TRADEMARKS.md.


🔒 Security & privacy

  • 🔑 Your password is stored in your OS keychain (keyring), never in a plaintext file — and in Docker it isn't stored at all (session-only).

  • 🍪 Your browser session cookies are cached in a local JSON file so you don't re-login every run. It's gitignored — treat it like being logged in; don't share it.

  • 🚫 .env, .venv/, and the session/cache files are all gitignored.

  • 🏠 All three front-ends (web, desktop, MCP) are single-user and local-only by design — bound to 127.0.0.1 (or published only to it in Docker; the desktop app serves on a private localhost port inside its own process). There's no multi-user support, and none is planned: this is intentionally not built to hold other people's credentials.

  • 🌐 Other websites can't drive the app. Binding to localhost doesn't stop a page you happen to be visiting from POSTing to it, so anything that changes something — starting a build, refreshing, changing the save folder, logging out — is refused unless it came from the app's own page. Reading is unaffected, and non-browser callers (curl, scripts) still work: the risk being closed is a web page, not a program you ran yourself.

  • 📁 The save folder (and any folder you copy an archive to) must resolve to somewhere a library plausibly lives — your home directory, a mounted drive, or the temp dir. Not because you can't be trusted with your own machine, but because that setting is reachable by an unauthenticated POST, and "choose a folder" is a much safer ceiling than "choose any directory on the system".

  • 📦 Release artifacts (installers and Docker images) carry signed build provenance attestations, so you can prove a download came from this repo's CI rather than trusting the link — see Verify your download. The builds themselves are unsigned; provenance is evidence of origin, not a code-signing certificate.

Found a security issue? See SECURITY.md.


⚠️ Known limitations

Tracked as GitHub issues:

  • Stop is near-instant — cancelling interrupts the file currently downloading (polled between streamed chunks; the partial file is discarded). A transfer that stalls without sending any bytes still has to hit its timeout first.
  • Edge cases — the library endpoint's response shape has since been checked against a full JSON Schema generated from a real account (#32), and every field BookVault reads is present in it. What that can't establish is the range of values in an unusual library — podcasts, webtoons, or DRM-restricted items may still need follow-up fixes.
  • Zip filenames on macOS Terminal — the built-in unzip garbles non-Latin (e.g. Cyrillic) names. Extract via Finder or ditto -x -k litres-library.zip dest/.
  • Unsigned builds — the installers are unsigned, so macOS Gatekeeper and Windows SmartScreen will warn on first launch. Provenance attestations let you verify an artifact came from this repo's CI (see Verify your download), but that is not the same as code signing.

📄 License

Free and open source under the MIT License, with attribution. Use it, modify it, and share it freely — the only ask is that any distribution or derivative work visibly credits the original author (Sergii Mavrov) and links back to this repository. It comes with no warranty.

This license covers BookVault's own source code only; it grants no rights in any third-party trademark (see Trademarks and TRADEMARKS.md).

About

Back up your own purchased litres.ru library (books & audiobooks) — as a zip or a plain folder mirror that only fetches what's missing. Local-only, on your machine: desktop app, web app, and MCP server for Claude.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages