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
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.