BookFetch is a tiny, UI-less acquisition daemon for self-hosters. It reads your Hardcover Want to Read shelf, checks read-only ebook and audiobook libraries, searches Prowlarr, selects a likely release, and submits it to qBittorrent.
Hardcover remains the only request interface. BookFetch is useful when you want one small process to request an ebook, an audiobook, or both without adding a second request UI.
Maturity: BookFetch is early-stage software. Ebook acquisition has been exercised in a real deployment. Paired ebook/audiobook decision logic has been production dry-run validated, and one controlled audiobook acquisition has completed end to end. This does not establish every audiobook or indexer edge case as production-proven.
BookFetch does:
- poll one Hardcover user's Want to Read shelf;
- scan existing ebook and audiobook directories without changing them;
- search configured Prowlarr indexers and apply format-aware fuzzy matching;
- submit matched torrents to qBittorrent with format-specific category/save paths;
- temporarily suppress repeat submissions; and
- optionally call a BookOrbit rescan endpoint after an ebook submission.
BookFetch deliberately does not provide a UI or API, monitor download completion, manage a queue, import or organize files, edit metadata, upgrade releases, manage BookOrbit/Audiobookshelf, or replace a library manager. Your download client and downstream import workflow own everything after submission.
Hardcover Want to Read
|
v
read-only library indexes ---- present? ----> skip as owned
|
v
recent submission history ---- recent? -----> suppress retry temporarily
|
v
Prowlarr search -> format matcher -> qBittorrent submission
|
+-> optional BookOrbit rescan (ebook only)
Only actual downstream library presence permanently satisfies a format.
A successful qBittorrent submission writes temporary retry state; it does not
mean the download completed or the format is owned. If no matching library file
appears, BookFetch can try again after BOOKFETCH_RETRY_SUPPRESSION_SECONDS.
Gate 2 paired acquisition is production validated and complete. A controlled test with The Bright Sword by Lev Grossman proved the audiobook path from a Hardcover Want to Read event through Prowlarr, an exact M4B match, format-specific qBittorrent routing, downstream handoff, and final audiobook-library detection. The already-owned ebook was not reacquired.
One Hardcover event can independently acquire an ebook, an audiobook, or both. Ownership, submission history, and retry decisions are evaluated per format, and only downstream library presence permanently satisfies that format.
Production dogfooding also exposed a fuzzy ebook-ownership false positive for a future title containing a sequel number. Numeric sequel or volume identifiers could be ignored by token-set matching. Fuzzy ebook ownership now requires every numeric token requested in the title to occur in the candidate label. Exact and organized-path matching remain intact.
- Python 3.12+ or Docker with Docker Compose
- a Hardcover account and API token
- a reachable Prowlarr instance with suitable indexers configured
- a reachable qBittorrent Web API
- an existing downstream importer/library workflow for each requested format
- read access to the organized ebook/audiobook library directories
BookFetch currently does not log in to qBittorrent. Its Web API must be reachable from BookFetch without an explicit login (for example, through qBittorrent's trusted-subnet/bypass configuration on a private network). Do not expose an unauthenticated Web API to the public internet.
git clone https://github.com/ShahnurIslam/bookfetch.git
cd bookfetch
cp .env.example .envEdit .env. Never commit that file.
Create/copy a token from https://hardcover.app/account/api and set:
HARDCOVER_TOKEN=your_real_tokenBookFetch authenticates that token and uses the authenticated user's Want to Read shelf; no separate Hardcover user ID is required.
Set URLs reachable from where BookFetch runs, not necessarily browser URLs:
PROWLARR_URL=http://prowlarr:9696
PROWLARR_API_KEY=your_real_api_key
QBITTORRENT_URL=http://qbittorrent:8081
QBITTORRENT_CATEGORY=books
QBITTORRENT_SAVE_PATH=/data/import/downloads/books
QBITTORRENT_AUDIOBOOK_CATEGORY=audiobooks
QBITTORRENT_AUDIOBOOK_SAVE_PATH=/data/import/downloads/audiobooksProwlarr is search-only. BookFetch fetches the torrent through Prowlarr and posts it directly to qBittorrent. The two save paths are interpreted by qBittorrent, so they must be valid paths in the qBittorrent environment. Use stable seed or staging directories—not an organized library directory that another tool may move or rewrite.
For a local Python run, point these variables at existing organized libraries:
MEDIA_BOOKS_PATH=/srv/media/ebooks
MEDIA_AUDIOBOOKS_PATH=/srv/media/audiobooks
BOOKFETCH_MEDIA_POLICY=ebookFor Docker, set host paths instead; Compose mounts them read-only at /books
and /audiobooks:
MEDIA_BOOKS_HOST_PATH=/srv/media/ebooks
MEDIA_AUDIOBOOKS_HOST_PATH=/srv/media/audiobooks
BOOKFETCH_DATA_PATH=./data/bookfetchBOOKFETCH_MEDIA_POLICY accepts:
ebook: evaluate only the ebook library and ebook candidates (default);audiobook: evaluate only the audiobook library and audiobook candidates;both: evaluate each format independently. One may be owned or temporarily suppressed while the other is still searched and submitted.
The library scanner recognizes common ebook files and audiobook files. Matching uses normalized filenames/directory labels plus fuzzy title/author matching. For fuzzy ebook ownership checks, numeric tokens in the requested title must also occur in the candidate label, preventing a numbered sequel or volume from matching an earlier title. Matching can still have false positives or false negatives, so review dry-run output.
BOOKFETCH_RETRY_SUPPRESSION_SECONDS=259200
BOOKFETCH_INTERVAL_SECONDS=21600The defaults are 72 hours between successful-submission retries and six hours
between Docker worker polls. history.json is temporary submission state, not a
library catalog. Library presence always takes precedence and clears applicable
submission state.
Leave BOOKORBIT_USERNAME and BOOKORBIT_PASSWORD empty to disable it. To enable
post-ebook-submission triggers:
BOOKORBIT_URL=http://bookorbit:3000
BOOKORBIT_USERNAME=your_username
BOOKORBIT_PASSWORD=your_password
BOOKORBIT_LIBRARY_ID=1
BOOKORBIT_TRIGGER_MODE=book-dockBOOKORBIT_TRIGGER_MODE is book-dock, library, or both. This trigger only
requests a rescan. It does not copy, finalize, or import a download, and it is not
used for audiobook submissions.
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python main.pyWith no --grab, BookFetch authenticates, scans libraries, searches Prowlarr,
and prints planned matches without submitting anything to qBittorrent. It still
contacts Hardcover and Prowlarr. Confirm the policy, library skips, candidate
matches, categories, and save paths before enabling acquisition.
To perform one real acquisition pass:
python main.py --grabThe public Compose example is deliberately dry-run by default:
cp .env.example .env
# Edit .env, including URLs and host library paths.
docker compose -f docker-compose.yml.example config
docker compose -f docker-compose.yml.example up --buildWatch at least one poll and stop with Ctrl-C. To enable the recurring acquisition
worker, set BOOKFETCH_DRY_RUN=false, then start it detached:
docker compose -f docker-compose.yml.example up -d --buildThe example defines BookFetch only. Connect it to your existing services and use
container-reachable URLs. Inside a container, localhost means that container,
not the Docker host.
BookFetch records a successful qBittorrent API submission and, for ebooks only, may call the optional BookOrbit trigger. It does not watch torrent progress or move any payload. You must provide automation that imports completed ebooks into your ebook library and completed audiobooks into your audiobook library. On a later poll, the organized library file becomes authoritative ownership.
- qBittorrent username/password authentication is not implemented.
- Matching is heuristic; series-heavy titles, alternate editions, Unicode/non-Latin titles, and graphic novels can be missed or mismatched.
- Audiobook matching does not match narrator metadata.
- There is no completion monitoring, failed-torrent cleanup, importing, or queue UI.
- A retry after the suppression window can duplicate a still-running or completed-but- not-yet-imported download because only downstream library presence is authoritative.
- One global media policy applies to every Want to Read item.
- One real audiobook path and paired-format dry runs are production-proven, but broader audiobook/indexer combinations and edge cases remain less exercised.
pip install -r requirements.txt
python -m compileall -q .
pytest tests/ -vBookFetch does not provide indexers or content. Configure only sources you are authorized to use and comply with applicable laws and tracker rules.
MIT — see LICENSE.