MangaReader

A manga reader that scrapes chapters from third-party sites, merges them into series, and serves them through a web interface.

What it does

MangaReader scrapes manga chapters from third-party scanlation sites, merges them into series, and serves them through a local web interface. You can bookmark series, track your reading, and the server auto-syncs new chapters in the background.

Architecture

Three Rust crates in one workspace:

  • mangaread-core — domain types, SQLite repository, series merging logic
  • scraper — SiteAdapter trait with per-site implementations (Demonicscans, Manhuaplus), CLI binary + library
  • website — Axum web server with 10 route modules, Maud templates, static assets

The database uses SQLite in WAL mode — four tables (series, manga, chapters, pages), all inserts use ON CONFLICT ... DO UPDATE, so it's safe to re-run.

Chapters from different sources get merged by normalized title. If two sources have the same chapter, the one with the most pages wins.

flowchart TB
    CLI[ScraperEngine] --> DB[(SQLite)]
    WEB[Axum Server] --> DB
    WEB <--> BROWSER[Browser]
    BROWSER --> JS[app.js + localStorage]

Client-side state without accounts. No auth, no database sessions. Bookmarks and reading progress live entirely in localStorage. The JS reads from there and builds the DOM with a tiny h() helper. It's surprisingly clean — but debugging state without a database to query is harder.

Screenshots

MangaReader home page
Home — Browse your series library
MangaReader series view
Series — View chapters and track reading
MangaReader series details
Series details — Manage and organize
MangaReader chapter view
Reader — Chapter viewing interface

The hard parts

Chromedriver. The scraper uses a real browser for JavaScript-heavy manga sites. Getting chromedriver to behave in Docker was 30% of the dev time. Headless mode bugs, sandbox issues, zombie processes — the usual webdriver circus.

SQLite and threading. rusqlite's Connection isn't Sync, so each scraper thread opens its own connection. The main AppState wraps its own in a Mutex. Sounds simple, but the borrow checker had opinions.

Fighting the borrow checker for the 50th time at 2am is a rite of passage.

What I learned

This was my first Rust project with multiple workspace crates, and it taught me more than any tutorial. Rust's type system is genuinely great for domain modeling — no invalid states possible — but fighting the borrow checker for the 50th time at 2am is a rite of passage.

The scraper adapter pattern — trait + per-site implementations — paid off immediately when adding a second source. And the sync loop (background thread, 6-hour interval, parallel scraping) taught me about Tokio channels and graceful shutdown the hard way.

Also: always pin your chromedriver version. Always.