Single-file EPUB vault + web reader for light novels. Turn a folder of EPUBs into a deduplicated, compressed SQLite vault and binge-read in your browser—infinite scroll, highlights, bookmarks, 8 themes. Works with any EPUB.
- Overview
- Features
- Architecture
- Quick Start
- Usage
- The Vault Format
- Web Reader
- HTTP API
- Security
- Tips & Troubleshooting
- License
smoltome.py is a self-contained Python 3 script for reading light novels:
- Ingests EPUB files into a
.vaultarchive—an SQLite-based, content-addressed storage layer with adaptive compression and deduplication. - Serves those books over HTTP with a fully embedded single-page reader (HTML/CSS/JS baked into the Python file).
Handles cover art, inline illustrations, multi-volume series, and EPUBs that mix prose with images. The extraction pipeline preserves chapter spine order, cover position, and embedded illustrations as they appear in the source file. CJK search tokenization works out of the box.
No external dependencies. Requires Python 3.6+.
- EPUB → Markdown extraction with OPF spine ordering and metadata parsing.
- Cover detection for EPUB 2/3
<meta name="cover">tags, with fallback to filename matching (handlesCover.jpg,cover.png, etc. common in fan-translated LNs). - Content-Defined Chunking (CDC) via gear-hash rolling window (64 KB min / 256 KB target / 1 MB max) so small edits to a file don't invalidate downstream deduplication.
- Adaptive compression: samples Shannon entropy to decide whether zlib level-9 will actually save space; high-entropy data (already-compressed images, encrypted blobs) is stored raw.
- BLAKE2b deduplication across all books—identical chunks are stored once. Shared cover art or insert illustrations across volumes stored only once.
- WORM semantics: immutable assets; re-converting the same filename updates the catalog but does not duplicate chunks.
- Global catalog merging: incremental updates preserve previously-ingested books.
- Full-text search with CJK tokenization (Hiragana, Katakana, Unified Ideographs as unigrams).
- Password protection: PBKDF2-HMAC-SHA256 with per-vault salt.
- Zero-dependency HTTP server (
ThreadingHTTPServer) with connection pooling and WAL-tuned SQLite. - 8 built-in themes: default-light, default-dark, sepia, paper, solarised-light, solarised-dark, oled-black, high-contrast.
- Infinite scroll for binge-reading a full volume—chapters append as you scroll, no page breaks.
- Text highlights in 4 colors with optional notes.
- Per-chapter bookmarks with one-click save.
- Reading position auto-persisted (chapter index + scroll offset).
- Progress bar tracking chapter completion.
- In-chapter search with regex-safe highlighting and prev/next navigation.
- Image zoom modal for inline illustrations and maps.
- Zen mode (auto-hiding chrome).
- Mobile swipe gestures (chapter navigation + pull-down settings).
- Sidebar with chapter list, read-state indicators, and bookmark list. Opens via hamburger button in the topbar; closes on click outside or Esc.
| Layer | Technology | Details |
|---|---|---|
| Storage | SQLite + WAL | PRAGMA mmap_size = 512 MB, 64 MB page cache, memory temp store |
| Chunking | Gear-hash CDC | splitmix64 gear table, 14-bit / 19-bit masks for size biasing |
| Compression | Entropy-adaptive zlib | Sampled 2 KB stratified; thresholds at 4.0 and 7.5 bits/byte |
| Hashing | BLAKE2b (256-bit) | Content-addressed dedup in chunks table |
| Paths | WebDAV-style | /project/collection/asset with implicit _GENERAL and _ROOT containers |
| Server | ThreadingHTTPServer |
Daemon threads, reuse_address, basic auth gate |
| Client | Vanilla JS SPA | Embedded in INDEX_HTML, INDEX_CSS, INDEX_JS strings |
| User Data | Sidecar JSON | .notes.json per vault: settings, positions, bookmarks, highlights, search history |
# 1. Grab the script
curl -O https://raw.githubusercontent.com/smolfiddle/smoltome/main/smoltome.py
chmod +x smoltome.py
# 2. Convert all EPUBs in the current directory
python3 smoltome.py convert --vault library.vault --epub-dir .
# 3. Start the reader
python3 smoltome.py read --vault library.vault --port 8080
# 4. Open http://localhost:8080# Directory (top-level only)
python3 smoltome.py convert --vault library.vault --epub-dir ./books
# Recursive scan
python3 smoltome.py convert --vault library.vault --epub-dir ./books --recursive
# Single file
python3 smoltome.py convert --vault library.vault --epub-file ./book.epub
# Password-protect a new vault
python3 smoltome.py convert --vault library.vault --epub-dir . --password "secret"If the script is symlinked or copied to epub2vault, it automatically runs the convert subcommand:
ln -s smoltome.py epub2vault
./epub2vault --vault library.vault --epub-dir .# Serve a specific vault
python3 smoltome.py read --vault library.vault --port 8080
# Auto-scan current directory for any .vault file
python3 smoltome.py read --port 8080
# Bind to all interfaces
python3 smoltome.py read --host 0.0.0.0 --port 8080
# Do not auto-open browser
python3 smoltome.py read --no-browserIf the script is symlinked to vault_reader, it automatically runs the read subcommand.
# Rebuild full-text search index for an existing vault
python3 smoltome.py index --vault library.vault
# With password
python3 smoltome.py index --vault library.vault --password "secret"A Makefile is provided for directory-local workflows:
| Command | What it does |
|---|---|
make convert |
Finds *.epub in cwd and packs them into library.vault |
make serve |
Finds the first *.vault in cwd and launches the reader on port 8080 |
make clean |
Removes __pycache__, .mypy_cache, and *.pyc files |
A .vault file is an SQLite database with the following schema:
vault_properties -- key/value store (password salt/hash)
projects -- top-level namespaces
collections -- directories (nested via parent_id)
assets -- file records with JSON manifests
metadata -- key/value tags per asset (filename, size, root_hash)
chunks -- deduplicated, optionally compressed blob storeWhen a book is ingested, each chapter and image becomes an asset. The manifest tracks:
{
"version": 3,
"filename": "01_chapter_one.md",
"total_size": 15234,
"chunks": ["a1b2c3...", "d4e5f6..."],
"chunk_sizes": [15234],
"chunk_offsets": [0],
"root_hash": "deadbeef...",
"compression": {
"original_size": 15234,
"stored_size": 8912,
"ratio": 1.71,
"compressed_chunks": 1,
"raw_chunks": 0
}
}- Per-book:
/books/{book_id}/catalog.json— metadata, chapter list, image list, cover. - Global:
/catalog_root.json— merged index of all books in the vault. Incremental converts update this without dropping existing entries.
Reader state lives in a sibling .notes.json file (e.g., library.notes.json). It is human-readable, atomically written (temp + os.replace), and contains:
settings— theme, typography, layoutpositions— last-read chapter & scroll per bookbookmarks— per-chapter labeled anchorshighlights— per-chapter color-coded ranges with notessearches— MRU search history (cap 50)
The reader is a responsive SPA that works on desktop and mobile. Choose from 8 themes:
| # | Theme | Best for |
|---|---|---|
| 1 | default-light |
Daytime reading |
| 2 | default-dark |
General dark mode |
| 3 | sepia |
Low eye strain |
| 4 | paper |
Print-like aesthetic |
| 5 | solarised-light |
Code/text clarity |
| 6 | solarised-dark |
Terminal aesthetic |
| 7 | oled-black |
OLED power saving |
| 8 | high-contrast |
Accessibility |
Typography controls include font family (system / serif / mono), font size (12–32 px), line height, margin, alignment, max width, and paragraph indent. The defaults are tuned for long-form prose: a comfortable 18 px size, 1.6 line height, and generous margins that keep line lengths readable even when flipping through dialogue-heavy chapters.
- Chapter mode: Navigate with ← / → toolbar buttons. Each chapter loads independently.
- Infinite scroll: Chapters append automatically as you scroll. A sentinel triggers the next load 300 px before the viewport bottom. An end-of-book card appears at the finish.
- Highlight: Select text → a popup appears with 4 color swatches. Click a highlight to edit its note or delete it.
- Bookmark: Press
bor the toolbar star to save the current scroll position with the chapter title as the label. Right-click (or long-press) a bookmark in the sidebar to delete it.
| Key | Action |
|---|---|
↑ / ↓ |
Scroll one line |
← / → |
Scroll one page |
Shift + ← / Shift + → |
Previous / next chapter |
Space / PgUp / PgDn |
Page scroll |
Home / End |
Chapter start / end |
/ |
Open in-chapter search |
b |
Quick bookmark |
1 – 8 |
Instant theme switch |
, |
Open settings |
? |
Show shortcut overlay |
Esc |
Close panels / toggle zen mode |
Mobile:
- Swipe left → next chapter
- Swipe right → previous chapter
- Swipe down from top edge → open settings
The reader communicates with the backend over a small REST API.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/vaults |
List discovered vaults |
GET |
/api/vault/{v}/books |
List all books |
GET |
/api/vault/{v}/book/{id}/catalog |
Book metadata & chapter index |
GET |
/api/vault/{v}/book/{id}/chapter/{file} |
Raw Markdown chapter |
GET |
/api/vault/{v}/image/{book}/{file} |
Image asset (cached 1 hr) |
GET |
/api/vault/{v}/search?q=... |
Title/author + full-text content search |
GET / PUT |
/api/vault/{v}/settings |
Reader settings |
GET / PUT |
/api/vault/{v}/book/{id}/position |
Reading position |
GET / POST |
/api/vault/{v}/book/{id}/bookmarks |
List / create bookmarks |
DELETE |
/api/vault/{v}/book/{id}/bookmarks/{bid} |
Delete bookmark |
GET / POST |
/api/vault/{v}/book/{id}/highlights |
List / create highlights |
PUT / DELETE |
/api/vault/{v}/book/{id}/highlights/{hid} |
Update / delete highlight |
Password-protected vaults require Authorization: Basic with any username and the vault password.
- Password hashing: PBKDF2-HMAC-SHA256, 100,000 iterations, 16-byte random salt.
- Auth: HTTP Basic Auth over the local reader connection. The password is checked against the vault hash; no username is required.
- WORM: Assets are immutable. The converter will not overwrite primary content; it only adds new chunks and updates catalog pointers.
Cover images not detected
The parser checks EPUB 2/3 <meta name="cover"> tags, then falls back to the first image whose filename contains cover. Most fan-translated and retail light-novel EPUBs name their cover Cover.jpg or cover.png, so this catches them even when the OPF meta tag is missing.
Inline illustrations not showing
The reader rewrites relative <img src="..."> paths to vault API paths automatically. If an image is missing, check that it was listed in the EPUB's OPF manifest and that its MIME type starts with image/.
Converting the same book twice
The global catalog merges entries by book_id. Re-converting updates the catalog but reuses existing deduplicated chunks, so storage cost is minimal—useful when you re-download a corrected volume and want to replace the old copy in-place.
Multi-volume series
Each volume gets its own book_id derived from the filename. Re-converting one volume doesn't touch the others. Infinite scroll works per-book; for cross-volume reading, use the sidebar to switch.
Password prompt during convert
If the vault already has a password, the CLI will prompt up to 3 times. Use --password to provide it non-interactively.
Truncating zero pages After conversion, the tool attempts to trim trailing zero pages from the SQLite file to keep the on-disk size tight. This is safe and purely cosmetic.
Large illustration-heavy books Because images are stored as deduplicated chunks, a series that reuses the same cover or insert art across volumes will only store those bytes once. The vault remains compact even when you archive dozens of volumes.