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.