← Back to Projects Web Application

MangaRead

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

What it does

MangaRead scrapes manga chapters from third-party scanlation sites, merges them into unified series entries, and serves everything through a local web interface. You can bookmark series, track reading progress, 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 with WAL mode. Four tables: series, manga, chapters, pages. All inserts use ON CONFLICT ... DO UPDATE so the whole thing is safe to re-run.

Chapters from different sources get linked to one unified series 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]

The hard parts

Chromedriver. The scraper uses a real browser to render JavaScript-heavy manga sites. Getting chromedriver to behave consistently in Docker was easily 30% of the development time. Headless mode, sandbox issues, zombie processes — the usual webdriver horror show.

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

Client-side state without accounts. No authentication, no database sessions. Bookmarks and reading progress live entirely in localStorage. This means the JavaScript reads from localStorage and builds the DOM via a tiny h() helper. It's surprisingly clean but debugging state is harder when you can't just query a database.

What I learned

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

The scraper adapter pattern (trait + per-site implementations) taught me that good abstractions pay off immediately when you add a second source. The sync loop design (background thread, 6-hour interval, parallel scraping) taught me about Tokio channels and graceful shutdown.

Also: always pin your chromedriver version. Always.