Nooky is a small, LAN-only web app that downloads a direct EPUB URL, validates the file, queues it durably, and streams it over SSH to a rooted NOOK Simple Touch when the reader is awake. It uses SQLite from Python's standard library and needs no Redis, Celery, message broker, or JavaScript build pipeline.
- Accepts direct EPUB links or book pages containing explicit EPUB links.
- Streams HTTP and HTTPS downloads to a temporary file with a size limit.
- Resolves DNS before connecting, rejects every non-public answer, pins the connection to the validated IP, and repeats the check for every redirect.
- Validates the ZIP
mimetypeandMETA-INF/container.xmlEPUB entries. - Selects and sanitizes a readable filename without allowing path traversal.
- Uses the legacy algorithms required by the NOOK, but pins its RSA host key.
- Stores queued EPUBs and job state on a persistent Docker volume.
- A dedicated worker checks waiting jobs every 15 seconds, survives restarts, streams books through SSH, and confirms the remote byte count afterward.
- After verification, sends a targeted Android media-scan broadcast for the new EPUB so it appears in the NOOK library without a reboot or storage remount.
- Provides a responsive iPhone UI, confirmation route for Share Sheet shortcuts, a small PWA manifest, upload fallback, status, and health routes.
- Paste a book page or direct
.epubURL into Book page or EPUB URL. Nooky can discover explicit EPUB links on pages from sources such as Project Gutenberg and Standard Ebooks. - Select Send to NOOK. Nooky downloads and validates the EPUB, adds it to the durable queue, clears the form, and shows the book in the list below.
- If the NOOK is asleep or offline, leave the book queued. The worker retries automatically every 15 seconds; no browser tab needs to remain open.
- Watch the book move through Waiting, Sending, and Delivered. After transfer and verification, Nooky asks Android to scan the new file so it appears in the reader's library.
- To send an EPUB already on your device, use Choose EPUB file and Upload instead of pasting a URL.
The reader must be prepared before Nooky can deliver books:
- Complete Phase 4 of The Phoenix Project on a NOOK Simple Touch or Simple Touch with GlowLight.
- Install and configure an SSH server APK on the NOOK. It must start while
the reader is awake, accept key-based authentication for
NOOK_USER(the default isroot), and allow that user to write toNOOK_BOOK_DIR. - Keep the NOOK and Nooky host reachable on the same trusted LAN, with a stable hostname or reserved IP address.
Nooky does not root the reader or install/configure the SSH APK. It assumes those device-side steps are already complete.
Python 3.9 or newer and an OpenSSH client are required.
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements-dev.txt
pytest
flask --app app.web run --debugThe app reads configuration from the environment. Set NOOK_HOST to your
reader's LAN hostname or address:
| Variable | Default |
|---|---|
NOOK_HOST |
nook.local |
NOOK_PORT |
22 |
NOOK_USER |
root |
NOOK_BOOK_DIR |
/sdcard/My Files/Books |
SSH_KEY_PATH |
/run/secrets/nook_ssh_key |
SSH_KNOWN_HOSTS_PATH |
/run/secrets/nook_known_hosts |
MAX_EPUB_SIZE_MB |
100 |
DOWNLOAD_TIMEOUT_SECONDS |
60 |
QUEUE_DB_PATH |
/data/nooky.db |
SPOOL_DIR |
/data/books |
QUEUE_POLL_SECONDS |
15 |
NOOK_REFRESH_LIBRARY |
true |
Do this from a trusted machine on the same LAN. First collect only the RSA key:
ssh-keyscan -T 10 -t rsa nook.local > nook_known_hosts
ssh-keygen -lf nook_known_hostsDo not trust the scan by itself. Compare the displayed SHA256 fingerprint
with the fingerprint obtained from the NOOK itself or with a previously trusted
connection. If the old device cannot display it directly, make the first
connection while physically controlling the device and verify that the key is
stable through a second independent LAN path. Nooky deliberately uses
StrictHostKeyChecking=yes; a changed key stops delivery.
For a non-default SSH port, the known-hosts entry must use [host]:port. You
can generate that form with ssh-keyscan -p PORT HOST.
The commands below send the files to the Swarm manager. Keep both source files outside the repository and protect the private key locally.
docker secret create nook_ssh_key ~/.ssh/nook_rsa
docker secret create nook_known_hosts ./nook_known_hostsSwarm mounts secrets as read-only mode 0444. The container entrypoint copies
the private key—not the known-hosts file—to private temporary storage with mode
0600, because OpenSSH refuses a group/world-readable private key. It vanishes
with the container.
- Change the image name and Traefik host rule in
docker-stack.yml. - Ensure the external
traefik-publicoverlay network exists and Traefik can route to it. - Build and push from a machine that can publish to your registry.
- Deploy on a Swarm manager.
docker build -t ghcr.io/your-user/nooky:latest .
docker push ghcr.io/your-user/nooky:latest
docker stack deploy -c docker-stack.yml nookyKeep the site LAN-only (or behind authenticated VPN/reverse-proxy access). Traefik exposes only the web service. Do not publish port 22 and do not forward the NOOK's SSH port through your router. If Traefik uses a different network or certificate resolver, adjust the labels to match your homelab.
For a standalone Docker host using an external proxy network, use
docker-compose.yml. It defaults to the placeholder nooky.example.com and
Traefik's cloudflare certificate resolver. Set NOOKY_HOSTNAME to your own
internal hostname before deployment.
Create a protected project directory on the host and place the two secret files
under its secrets directory:
mkdir -p /opt/stacks/nooky/secrets
chmod 700 /opt/stacks/nooky/secrets
cp ~/.ssh/nook_rsa /opt/stacks/nooky/secrets/nook_ssh_key
cp ./nook_known_hosts /opt/stacks/nooky/secrets/nook_known_hosts
chmod 600 /opt/stacks/nooky/secrets/*
cd /opt/stacks/nooky
docker compose up -d --buildThe container starts briefly as root only to copy the read-only key and host-pin
secrets to mode 0600, then permanently drops to the unprivileged nooky user
before starting the web application.
Create an iOS Shortcut that accepts URLs from the Share Sheet and opens:
https://nooky.home.example/send?url=<URL-ENCODED-SHARED-URL>
Nooky always shows a confirmation page before fetching a URL through this route. In Safari, use Share → Add to Home Screen for an app-like launcher.
GET /— delivery formPOST /send-url— discover/fetch, validate, transfer, and verifyPOST /upload— manual fallbackGET /send?url=…— Share Sheet confirmationGET /status— NOOK reachability and free spaceGET /jobs/<id>— queued delivery statusGET /health— container liveness
Nooky rejects URL credentials and all schemes except HTTP(S). It rejects a hostname if any DNS result is loopback, private, link-local, multicast, reserved, unspecified, or otherwise non-global. The validated IP is used for the actual socket while TLS still verifies the requested hostname, preventing a DNS-rebinding gap between validation and connection. Redirects receive the same checks. The NOOK address is used only by the fixed SSH integration and cannot be selected by a submitted URL.
The app is intentionally unauthenticated because it is designed for a trusted LAN. Add authentication at Traefik or VPN level before making it reachable from any untrusted network.
