Simple web UI to search prowlarr
  • JavaScript 83.8%
  • CSS 10.4%
  • HTML 3.6%
  • Shell 2.2%
Find a file
2026-07-09 21:39:11 -07:00
deploy Fix qBittorrent watch-folder pickup, accessible success color, better import naming 2026-07-08 23:14:29 -07:00
html Update color scheme to match Kavita 2026-07-09 21:39:11 -07:00
src Fix qBittorrent watch-folder pickup, accessible success color, better import naming 2026-07-08 23:14:29 -07:00
.gitignore Fix qBittorrent watch-folder pickup, accessible success color, better import naming 2026-07-08 23:14:29 -07:00
CHANGELOG.md Update color scheme to match Kavita 2026-07-09 21:39:11 -07:00
env.example Add booksearch v0.1.0: Prowlarr ebook search, qBittorrent watch-folder bridge, and Kavita library importer 2026-07-08 01:41:33 -07:00
LICENSE Add booksearch v0.1.0: Prowlarr ebook search, qBittorrent watch-folder bridge, and Kavita library importer 2026-07-08 01:41:33 -07:00
package-lock.json Release v0.2.0 2026-07-08 23:36:08 -07:00
package.json Release v0.2.0 2026-07-08 23:36:08 -07:00
prowlar-api.json Initial commit before planning and development. 2026-07-08 00:47:47 -07:00
README.md Fix qBittorrent watch-folder pickup, accessible success color, better import naming 2026-07-08 23:14:29 -07:00

BookSearch

A small self-hosted web app that searches Prowlarr for ebooks and sends matches straight to a qBittorrent watch folder, then automatically copies finished downloads into a library folder (e.g. Kavita).

It's designed to sit alongside an existing site on the same nginx server block (e.g. a ReadMeABook instance) without interfering with it — it only adds new location blocks, never touching the existing one.

How it works

  1. Search — the UI queries Prowlarr, scoped server-side to a single ebook category, and shows results as a vertical list (title, author — parsed from the release title when the indexer embeds it as a "... by AUTHOR" suffix — indexer, size, age, seeders), capped at the top 8 results with the first marked as suggested.
  2. Request — clicking Download fetches the chosen release's torrent file from Prowlarr server-side and writes it into WATCHED_DIR, where qBittorrent picks it up automatically.
  3. Import — a background poll loop watches DOWNLOAD_DIR for finished downloads and copies the ebook file into LIBRARY_DIR, named using the LIBRARY_DIR_FORMAT template, for your library manager to pick up on its next scan. Naming prefers the {title, author} already parsed from the release at request time (matched to the download via the torrent's own internal name) over re-deriving it from the downloaded folder name, which is frequently just the torrent's bare internal name with no usable title/author shape.

The Prowlarr API key never reaches the browser, and the app has no login of its own — it's meant to run behind an nginx location block with HTTP Basic Auth, bound to 127.0.0.1 only.

Requirements

  • Node.js 22+
  • A running Prowlarr instance with at least one ebook-capable indexer configured
  • qBittorrent with a watched folder for adding torrents automatically
  • A library manager that periodically rescans a folder (Kavita, Calibre-Web, etc.) — optional, the importer just copies files into a folder

Configuration

Copy env.example to .env and fill in real values. .env is gitignored — never commit it.

Key Description
PROWLARR_URL Base URL of your Prowlarr instance
API_TOKEN Prowlarr API key
WEB_DIR_BASE Deploy target for the app (backend + config)
WEB_DIR_HTML Deploy target for the static frontend (served directly by nginx)
PORT Port the Node app listens on (default 3031, 127.0.0.1 only)
CATEGORY_ID Newznab/Torznab category to search (default 7020, Books/EBook)
LOG_DIR Directory for the app's log file (default /var/log/booksearch)
WATCHED_DIR qBittorrent's watched folder — fetched .torrent files are written here
DOWNLOAD_DIR qBittorrent's completed-download folder — polled for finished downloads
LIBRARY_DIR Destination library folder
LIBRARY_DIR_FORMAT Folder naming template for imports, using {TITLE}/{AUTHOR} tokens
IMPORTER_POLL_INTERVAL_MS How often the importer checks DOWNLOAD_DIR (default 60000)

Local development

npm install
npm start

The server binds to 127.0.0.1:$PORT only — there's no auth at this layer (that's nginx's job in production), so test it directly:

curl http://127.0.0.1:3031/api/health
curl "http://127.0.0.1:3031/api/search?q=some+title"

API

Route Description
GET /api/search?q= Search Prowlarr, returns simplified results with an opaque id per release
POST /api/request { id } Fetch that release's torrent file and write it into WATCHED_DIR
GET /api/health Liveness check

Deployment

Runs as a systemd service behind nginx, on the same host/subdomain as an existing site.

  • deploy/booksearch.service — systemd unit template. Fill in <YOUR_USER>, <ABSOLUTE_PATH_TO_NODE>, and the ReadWritePaths placeholders for your host before installing — left as placeholders on purpose so this tracked file doesn't bake in machine-specific paths. Runs as a real login account (User=<YOUR_USER>), not a dedicated system user — pick a user on your host that already owns WATCHED_DIR/LIBRARY_DIR and can reach your Node install; a locked-down system user needs ProtectHome/directory permissions that get in the way of an nvm-managed Node binary under /home.
  • deploy/booksearch.logrotate — logrotate config for the file-based log (copy to /etc/logrotate.d/)
  • deploy/deploy.sh — syncs this repo into WEB_DIR_BASE/WEB_DIR_HTML (per .env) and restarts the service

nginx configuration

The app has no authentication or TLS of its own — nginx is the only thing standing between it and the internet. Add an upstream and two new location blocks to your existing server block; do not touch its current location / (that's whatever site you're already hosting on the subdomain).

upstream BOOKSEARCH {
    server 127.0.0.1:3031;   # match PORT in .env
}
    location = /booksearch {
        return 301 /booksearch/$is_args$args;
    }

    location /booksearch/api/ {
        auth_basic           "BookSearch";
        auth_basic_user_file /etc/nginx/.htpasswd_booksearch;

        proxy_pass              http://BOOKSEARCH/api/;
        proxy_set_header        Host $host;
        proxy_set_header        X-Real-IP $remote_addr;
        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header        X-Forwarded-Proto $scheme;
        proxy_redirect          off;
        proxy_http_version      1.1;

        proxy_connect_timeout   10s;
        proxy_read_timeout      75s;   # > the backend's own download timeout
        client_max_body_size    64k;   # POST body is just {id}
    }

    location /booksearch/ {
        alias /var/www/booksearch/html/;   # match WEB_DIR_HTML in .env
        index index.html;

        auth_basic           "BookSearch";
        auth_basic_user_file /etc/nginx/.htpasswd_booksearch;
    }

A few details that are easy to get wrong:

  • The bare /booksearch redirect is required, not cosmetic. alias-based prefix locations only match requests that include the trailing slash — a request to /booksearch (no slash) doesn't match location /booksearch/ at all and falls through to whatever location / on that subdomain proxies to, not this app. The location = /booksearch { return 301 /booksearch/; } exact-match block catches that case and 301s to the trailing-slash form (preserving any query string via $is_args$args).
  • alias, not root, on the static block — alias replaces the whole matched /booksearch/ prefix; root would look for a nonexistent .../html/booksearch/... subfolder and 404 everything.
  • Keep the trailing /api/ on proxy_pass — since both the location and the proxy_pass URL end in /, nginx strips /booksearch/api/ and forwards the rest to /api/... on the Node app, matching its routes.
  • Same auth_basic_user_file and realm string on both blocks — otherwise the browser's cached Basic Auth credentials from loading the page won't reattach to its fetch() calls to the API.
  • Create the credentials file once: sudo htpasswd -c /etc/nginx/.htpasswd_booksearch <user>.
  • Validate and reload before it's live: sudo nginx -t && sudo systemctl reload nginx.

Project layout

src/        Express backend + library importer (one process, one systemd unit)
html/       Static frontend, served directly by nginx (no build step)
deploy/     systemd unit, logrotate config, deploy script

License

GPLv3 — see LICENSE.

Copyright (C) 2026 kurt

Acknowledgments

Built with the assistance of Claude (Sonnet 5, model claude-sonnet-5) via Claude Code.