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
Browse your series library
MangaReader series view
View chapters and track reading
MangaReader series details
Manage and organize
MangaReader chapter view
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.