This guide explains the project for someone who is not familiar with Rust. It covers just enough Rust to read the code, then tours how the pieces fit. If you know Python or JavaScript, the analogies in §2 will get you oriented fast.
TL;DR: there is one engine library (
libgen-core) that does all the real work — parsing book lists, searching Library Genesis, picking the right match, downloading files politely, and persisting everything so it can resume. Every user interface (the command-line harnesses, and a native Mac desktop app) is a thin shell around that one library, driving it through a single command/event surface (the orchestrator).
what the user sees what does the work
┌──────────────────────┬──────────────┐ ┌────────────────────────┐
│ Tauri desktop app │ CLI harnesses│──▶│ libgen-core (engine) │
│ (app/src-tauri + ui) │ (libgen) │ │ one Rust library │
└──────────────────────┴──────────────┘ └────────────────────────┘
libgen-core— the engine. No user-interface code lives here. (folder:crates/core)libgen-cli— small command-line programs ("harnesses") that call the engine so we can build and test each feature on its own, headlessly. (folder:crates/cli)app/src-tauri— the native desktop app's Rust backend (Tauri);app/uiis the self-contained web UI it ships, andapp/ui-mockis the clickable design prototype (plain HTML/CSS/JS) used to agree on the look & flows.
A ratatui terminal UI could be added later against the same engine; none exists yet.
The whole thing is a Cargo workspace: one repo containing several packages
("crates") that build together. See the top-level Cargo.toml.
The data flows in a pipeline, tied together and persisted by the orchestrator:
parse ──▶ search ──▶ match ──▶ queue ──▶ download ──▶ files on disk
(read a (ask libgen (is this (schedule (fetch the (renamed, in
list) mirrors) the right downloads file, verify per-list /
book?) politely) it) per-group folders)
store (SQLite) records every step so a quit/crash resumes on the next launch.
You don't need to write Rust to follow along. Here are the constructs you'll see, each mapped to something you already know.
| Rust | What it means | Like in JS/Python |
|---|---|---|
crate |
a package / library | an npm package / Python module |
mod foo; |
declares a module (a file foo.rs) |
import ./foo |
pub |
public (visible outside the module) | export |
struct Foo { a: u16 } |
a record with named fields | a class with only data / a dataclass / a TS interface |
enum Status { Queued, Done } |
a type that is exactly one of several variants | a union type / Python Enum, but more powerful |
fn name(x: u16) -> bool |
a function: takes a u16, returns a bool |
function name(x): boolean |
Vec<T> |
a growable list of T |
Array / list |
Option<T> |
either Some(value) or None |
nullable / T | undefined |
Result<T> |
either Ok(value) or Err(error) |
success-or-throw, but returned as a value |
String / &str |
owned text / a borrowed text slice | a string (don't overthink the distinction at first) |
async fn + .await |
a function that does I/O without blocking | async/await (same idea) |
impl Foo { ... } |
methods attached to Foo |
methods inside a class body |
trait Bar |
a shared interface several types can implement | an interface / abstract base class |
? (e.g. read(x)?) |
"if this errored, return the error now" | try/catch that auto-rethrows |
#[derive(...)] |
auto-generate boilerplate (e.g. JSON, equality) | decorators / mixins |
Three ideas worth a second look because they shape this codebase:
- Errors are values, not exceptions. Functions return
Result<T>. The?operator means "unwrap the success, or bubble the error up." Solet s = read(path)?;reads like normal code but quietly propagates failures. We use theanyhowcrate for easy error handling. Option<T>instead of null. A missing year isNone, a present one isSome(2019). The compiler forces us to handle the "missing" case, so there are no surprise null crashes.enumcarries data.RequestStatus::Failed { error: String }is one variant that also holds an error message. This is how we model a state machine cleanly (see §4).
Serde is the JSON library. #[derive(Serialize, Deserialize)] on a struct means "this
type can be turned into/from JSON automatically." That's why our model types round-trip to
JSON for the CLI and the GUI for free.
kwire/
├── Cargo.toml # workspace: lists the crates + shared dependency versions
├── rust-toolchain.toml # pins the Rust compiler version
├── mirrors.toml # editable list of libgen SEARCH mirrors (no hardcoding in code)
├── DESIGN.md # the design rationale (read for "why")
├── PLAN.md # phased build plan
├── docs/CODEBASE.md # this file
│
├── crates/core/ # THE ENGINE (libgen-core)
│ └── src/
│ ├── lib.rs # lists the modules; the crate's front door
│ ├── model.rs # ⭐ the shared data types — start here (see §4)
│ ├── parse.rs # Markdown/JSON reading list → model
│ ├── search.rs # query libgen mirrors (record/replay) → candidates
│ ├── matching.rs # score, format-rank, keep diverse variations; auto vs. ask
│ ├── download.rs # per-mirror resolvers (md5→URL) + resumable, md5-verified fetch
│ ├── queue.rs # per-host download scheduling, retry/failover, pause/cancel
│ ├── store.rs # SQLite persistence (schema v2; resume-on-launch)
│ ├── naming.rs # pure filename/foldering (template, sanitize, collisions)
│ └── orchestrator.rs # ties the pipeline together behind a command/event API
│ └── tests/ # headless integration tests + golden files
│
├── crates/engine/ # UI-AGNOSTIC DRIVER (libgen-engine) — shared by both frontends
│ └── src/
│ ├── engine.rs # the concurrency driver loop (spawn_with) + EngineEmitter trait
│ ├── viewmodel.rs # JSON-friendly projection of the library (ViewModel/ViewVariation)
│ ├── state.rs # AppState/Config/Library shared between desktop + TUI
│ └── legs.rs # ⭐ shared LegTracker: per-leg download state (see docs/LEG_LIFECYCLE.md)
│
├── crates/tui/ # THE TUI FRONTEND (libgen-tui, binary `kwire`)
│ └── src/
│ ├── app.rs # AppState + input/event handling (holds the LegTracker)
│ ├── ui.rs # ratatui render (the Activity pane renders per-leg "· alt copy" rows)
│ └── theme.rs # the "quiet" truecolor palette + shared widgets
│
├── crates/cli/ # THE HARNESSES (libgen-cli, binary named `libgen`)
│ └── src/
│ ├── main.rs # wires up the subcommands
│ ├── cmd_parse.rs # `libgen parse-list <file>`
│ ├── cmd_query.rs # `libgen query-books <input>`
│ ├── cmd_download.rs # `libgen download-books <md5…> --site … | --mock …`
│ └── cmd_run.rs # `libgen run-list <file>` — the whole pipeline
│
├── fixtures/ # sample inputs + recorded responses + golden outputs
│ ├── jeremy_public_domain_list.md / .json # a real sample list (Markdown / JSON)
│ ├── avery_public_domain_list.md / .json # a second sample list
│ ├── expected/ # golden normalized parser outputs
│ ├── search/ # recorded mirror responses for replay tests
│ └── ipfs/ # captured libgen.li pages for the IPFS resolver tests
│
└── app/
├── src-tauri/ # native desktop app backend (Tauri) → .app / .dmg
├── ui/ # the self-contained web UI the app ships
└── ui-mock/ # clickable design prototype + headless UI tests
If you read one file, read crates/core/src/model.rs. Everything else moves these
types around.
crates/core/src/model.rs defines the shared vocabulary. In plain language:
DownloadList— a whole reading list. Has atitle, somesettings, and a list ofgroups. Maps to one destination folder on disk.Group— a named batch of books (e.g. "Batch 1 — Lift-Off"). Can containsubgroups, which become subfolders. This is how sub-grouping works.BookInput— what you asked for: title, authors, and optional isbn/year/ publisher/edition/language/format preference. The optional fields areOption<...>.BookRequest— one tracked item: yourBookInputplus its livestatus, thecandidateswe found (kept for swapping), which one isselected, and a stable, persistedseq(its sequence number, assigned once so inserting a book later never renumbers files).Candidate— one search result from a mirror: has anmd5(a unique fingerprint libgen gives every file — we use it to fetch and to verify the download), title, authors, format, size, ascorethe matcher assigns, and its own optionaljob.DownloadJob— progress for one download: itsJobState, host, attempts, bytes done, resume offset, whether the md5 verified, the final path.
Downloads are tracked per candidate, not per book. Each Candidate carries its own
job; a candidate is "requested for download" exactly when it has one. That's how several
variations of one book can be in different states at once — e.g. the epub Done while
the same book's pdf is still Downloading.
BookRequest::acquisition() rolls those per-variation states up into a small summary
(requested / done / active / failed / paused / cancelled) — that's what a book row shows
(e.g. "Downloading 1/2"), and the book's overall RequestStatus is derived from it. By
default a Matched book auto-requests just its single best variation ("one best copy");
run-list --all-formats requests one best per preferred format instead.
RequestStatus (per book) and JobState (per variation) are each an enum — always
exactly one state, moving between them:
per book: queued → querying ─┬→ matched ───────────► (auto-request best variation)
├→ needs_selection ─(you pick)→ ready
└→ not_found
ready → downloading → verifying → done / failed / paused / cancelled
per variation (JobState): pending → resolving → downloading → verifying → done
(also: failed, paused [resumable], cancelled)
matched= we're confident, download automatically.needs_selection= ambiguous, the UI asks you to choose (the candidate modal).failed= retried with backoff; if it keeps failing it waits for a manual retry.pausedkeeps the partial.part+ resume offset so it can continue;cancelleddoes not. Interrupted downloads also resume automatically on the next launch (the orchestrator re-pends in-flight jobs from the persisted store).
ListSettings holds the knobs that drive this: format_pref (default [epub, pdf] —
friendly to both Kindle and iPad), auto_threshold / near_threshold (how confident is
"confident enough"), the filename template, whether sequence numbers reset per group, and
keep_top (how many variations to keep for swapping).
Each engine module is a stage in the pipeline. Their public entry points:
| Module | Key entry point | Input → Output |
|---|---|---|
parse |
parse_auto / parse_markdown / parse_json |
text → DownloadList |
search |
SearchClient::search (replay/live transport) |
BookInput → Vec<Candidate> |
matching |
evaluate |
candidates + settings → MatchOutcome (status + kept variations) |
download |
resolver_for_site → Resolver::resolve, then download_with_client_cancellable |
an md5 → a file on disk (verified) |
queue |
Scheduler::run |
many jobs → downloaded politely, per host, with retry/failover/pause |
store |
Store::open / insert_list / update_request |
model ⇄ SQLite (resumes on launch) |
naming |
destinations_for_variations |
book + variations → sanitized destination paths |
orchestrator |
Orchestrator (query_all, start_downloads, pause/cancel/resume …) |
a list → driven end-to-end, persisted, with events |
Why "per-host queues"? (queue.rs) Libgen files come from several download hosts.
Instead of one global download limit, each host gets its own small queue with its own
speed limit and retry budget. That way we're polite to each server, and a slow/blocked host
can't hold up downloads from the others. On repeated failure the resolver chain fails over
to an alternate mirror and re-enqueues onto that host's queue. See DESIGN.md §8.
Why several download sites? (download.rs) resolver_for_site builds a per-mirror
resolver that turns an md5 into a real download URL: the libgen.li family (libgen.li,
libgen.vg, libgen.la) does an ads.php → get.php hop on a shared CDN; libgen.pw /
randombook.org use an independent CDN via a by-id JSON lookup; ipfs maps the md5
to an IPFS content id (via libgen.li) and serves it from public gateways — the most
independent lane. download::ALL_SITES lists everything --site accepts. (library.lol is
dead and removed; Anna's Archive is not used.)
The harnesses let us exercise one feature at a time, no GUI needed. The binary is
libgen. From the repo root:
# Parse a reading list and print the normalized model as JSON (no network):
cargo run -p libgen-cli -- parse-list fixtures/jeremy_public_domain_list.md
# Search mirrors for a book and print ranked candidates (uses recorded fixtures offline):
cargo run -p libgen-cli -- query-books some_book.json --replay fixtures/search
# Resolve + download a file by md5 (resumable, md5-verified) from a chosen site:
cargo run -p libgen-cli -- download-books <md5> --site libgen.li --out downloads
# …other sites: --site libgen.vg|libgen.la|libgen.pw|randombook.org|ipfs
# Run the WHOLE pipeline (parse → query → match → plan filenames). Dry run by
# default; add --site/--mock to actually download, --all-formats for epub+pdf,
# --db <path> to persist and --resume to continue interrupted downloads later:
cargo run -p libgen-cli -- run-list fixtures/jeremy_public_domain_list.md --replay fixtures/search-p libgen-cli means "run the package named libgen-cli." Everything after -- is passed
to our program.
cargo build # compile everything
cargo test # run ALL tests (headless, offline by design)
cargo test -p libgen-core # just the engine's tests
cargo fmt # auto-format (run before committing)
cargo clippy # lint for common mistakesCurrent status: the engine, the CLI harnesses, and the Tauri app are implemented and
tested (115 tests via cargo test, cargo clippy --all-targets clean). The app/ui-mock/
prototype is openable with open app/ui-mock/index.html.
Testing philosophy (important): everything is validated headlessly — no clicking, no live servers required.
- Parsing is checked with golden files: we parse a fixture and compare to a saved
expected JSON in
fixtures/expected/. If behavior changes intentionally, regenerate the golden (UPDATE_GOLDEN=1 cargo test). - Search is tested with record/replay: real HTTP responses are saved once (incl.
the captured libgen.li pages in
fixtures/ipfs/for the IPFS resolver), then replayed so tests are deterministic and need no network. - Downloads are tested against a local mock HTTP server (so we can simulate resumes, corruption, rate limits, failover, and pause/cancel without touching real mirrors).
- Orchestration / store are tested with an in-memory SQLite store plus the replay search client and mock resolvers, end to end.
The native app is app/src-tauri (a Rust Tauri backend) over the
self-contained web UI in app/ui. The backend exposes the engine to the UI as Tauri
commands (commands.rs, called from JS via invoke(...)), translating the UI's flat
book ids to the orchestrator's (group_path, book_index) tree positions (bridge.rs).
Run it from app/src-tauri with cargo tauri dev; bundle a .app/.dmg with
cargo tauri build.
The UI is tested headlessly without a browser-clicking session: app/ui/headless-test.mjs
loads the page over file:// through headless Chrome (DevTools Protocol, no extra deps),
asserts the JS ran with zero console errors, and exercises the core interactions (incl. the
per-variation rows and format ranking). app/ui-mock/ has the same for the design
prototype. The real engine path is exercised at runtime via cargo tauri dev.
- Cargo / crate / workspace — Rust's build tool / a package / a multi-package repo.
- md5 — a short fingerprint of a file. Libgen gives one per file; we use it both as the download key and to confirm the file arrived intact.
- search mirror — one of several interchangeable libgen websites we search. They go up
and down, so the list lives in
mirrors.tomland is editable without recompiling. - download site (
--site) — a mirror we download from; each needs a bespoke resolver, so the set lives in code (download::ALL_SITES): the libgen.li family, libgen.pw / randombook.org, and ipfs. - variation — one specific copy/format of a book (one
Candidatewith its ownjob); several variations of a book can download independently and be in different states. - golden file — a saved "expected output" that a test compares against.
- record/replay — save real network responses once, then replay them in tests.
- harness — a small standalone program that drives one feature for development/testing.
- resolve — turn an md5 into an actual downloadable URL (libgen needs an extra hop).
- backoff / jitter — wait longer after each retry, with a little randomness, to be polite and avoid thundering-herd retries.
Keep this file current as modules get implemented. If you add a public function or change the model, update §4/§5 so the next newcomer stays oriented.