Files
libnovel/AGENTS.md
Admin 7413313100
All checks were successful
CI / Scraper / Lint (push) Successful in 10s
CI / Scraper / Test (push) Successful in 14s
Release / Scraper / Test (push) Successful in 18s
CI / Scraper / Lint (pull_request) Successful in 18s
Release / UI / Build (push) Successful in 23s
CI / Scraper / Test (pull_request) Successful in 15s
CI / UI / Build (pull_request) Successful in 32s
Release / Scraper / Docker (push) Successful in 55s
CI / Scraper / Docker Push (pull_request) Has been skipped
CI / UI / Docker Push (pull_request) Has been skipped
CI / Scraper / Docker Push (push) Successful in 1m5s
Release / UI / Docker (push) Successful in 1m12s
iOS CI / Build (push) Successful in 4m18s
iOS CI / Build (pull_request) Successful in 4m25s
iOS CI / Test (push) Successful in 8m11s
iOS CI / Test (pull_request) Successful in 8m21s
fix: update integration_test.go to match server.New signature (version, commit args)
2026-03-14 14:25:46 +05:00

10 KiB

libnovel Project

Go web scraper for novelfire.net with TTS support via Kokoro-FastAPI. Structured data in PocketBase, binary blobs (chapters, audio, browse snapshots) in MinIO. SvelteKit frontend.

Architecture

scraper/
├── cmd/scraper/main.go               # Entry point: run | refresh | serve | save-browse
├── internal/
│   ├── orchestrator/orchestrator.go  # Catalogue walk → per-book metadata goroutines → chapter worker pool
│   ├── browser/                      # BrowserClient interface + direct HTTP (production) + Browserless variants
│   ├── novelfire/scraper.go          # novelfire.net scraping (catalogue, metadata, chapters, ranking)
│   ├── server/                       # HTTP API server (server.go + 6 handler files)
│   │   ├── server.go                 # Server struct, route registration, ListenAndServe
│   │   ├── handlers_scrape.go        # POST /scrape, /scrape/book, /scrape/book/range; job status/tasks
│   │   ├── handlers_browse.go        # GET /api/browse, /api/search, /api/cover — MinIO-cached browse pages
│   │   ├── handlers_preview.go       # GET /api/book-preview, /api/chapter-text-preview — live scrape, no store writes
│   │   ├── handlers_audio.go         # POST /api/audio, GET /api/audio-proxy, voice samples, presign
│   │   ├── handlers_progress.go      # GET/POST/DELETE /api/progress
│   │   ├── handlers_ranking.go       # GET /api/ranking, /api/cover
│   │   └── helpers.go                # stripMarkdown, hardcoded voice list fallback
│   ├── storage/                      # Persistence layer (PocketBase + MinIO)
│   │   ├── store.go                  # Store interface — single abstraction for server + orchestrator
│   │   ├── hybrid.go                 # HybridStore: routes structured data → PocketBase, blobs → MinIO
│   │   ├── pocketbase.go             # PocketBase REST admin client (7 collections, auth, schema bootstrap)
│   │   ├── minio.go                  # MinIO client (3 buckets: chapters, audio, browse)
│   │   └── coverutil.go              # Best-effort cover image downloader → browse bucket
│   └── scraper/
│       ├── interfaces.go             # NovelScraper interface + domain types (BookMeta, ChapterRef, etc.)
│       └── htmlutil/htmlutil.go      # HTML parsing helpers (NodeToMarkdown, ResolveURL, etc.)

Key Concepts

  • Orchestrator: Catalogue stream → per-book goroutines (metadata + chapter list) → shared chapter work channel → N worker goroutines (chapter text). Scrape jobs tracked in PocketBase scraping_tasks.
  • Storage: HybridStore implements the Store interface. PocketBase holds structured records (books, chapters_idx, ranking, progress, audio_cache, app_users, scraping_tasks). MinIO holds blobs (chapter markdown, audio MP3s, browse HTML snapshots, cover images).
  • Browser Client: Production uses NewDirectHTTPClient (plain HTTP, no Browserless). Browserless variants (content/scrape/cdp) exist in browser/ but are only wired for the save-browse subcommand.
  • Preview: GET /api/book-preview/{slug} scrapes metadata + chapter list live without persisting anything — used when a book is not yet in the library. On first visit, metadata and chapter index are auto-saved to PocketBase in the background.
  • Server: 24 HTTP endpoints. Async scrape jobs (mutex, 409 on concurrent), in-flight dedup for audio generation, MinIO-backed browse page cache with mem-cache fallback.

Commands

# Build
cd scraper && go build -o bin/scraper ./cmd/scraper

# Full catalogue scrape (one-shot)
./bin/scraper run

# Single book
./bin/scraper run --url https://novelfire.net/book/xxx

# Re-scrape a book already in the DB (uses stored source_url)
./bin/scraper refresh <slug>

# HTTP server
./bin/scraper serve

# Capture browse pages to MinIO via SingleFile CLI (requires SINGLEFILE_PATH + BROWSERLESS_URL)
./bin/scraper save-browse

# Tests (unit only — integration tests require live services)
cd scraper && go test ./... -short

# All tests (requires MinIO + PocketBase + Browserless)
cd scraper && go test ./...

Environment Variables

Scraper (Go)

Variable Description Default
LOG_LEVEL debug|info|warn|error info
SCRAPER_HTTP_ADDR HTTP listen address :8080
SCRAPER_WORKERS Chapter goroutines NumCPU
SCRAPER_TIMEOUT Per-request HTTP timeout (seconds) 90
KOKORO_URL Kokoro-FastAPI TTS base URL https://kokoro.kalekber.cc
KOKORO_VOICE Default TTS voice af_bella
MINIO_ENDPOINT MinIO S3 API host:port localhost:9000
MINIO_PUBLIC_ENDPOINT Public MinIO endpoint for presigned URLs ""
MINIO_ACCESS_KEY MinIO access key admin
MINIO_SECRET_KEY MinIO secret key changeme123
MINIO_USE_SSL TLS for internal MinIO connection false
MINIO_PUBLIC_USE_SSL TLS for public presigned URL endpoint true
MINIO_BUCKET_CHAPTERS Chapter markdown bucket libnovel-chapters
MINIO_BUCKET_AUDIO Audio MP3 bucket libnovel-audio
MINIO_BUCKET_BROWSE Browse HTML + cover image bucket libnovel-browse
POCKETBASE_URL PocketBase base URL http://localhost:8090
POCKETBASE_ADMIN_EMAIL PocketBase admin email admin@libnovel.local
POCKETBASE_ADMIN_PASSWORD PocketBase admin password changeme123
BROWSERLESS_URL Browserless WS endpoint (save-browse only) http://localhost:3030
SINGLEFILE_PATH SingleFile CLI binary path (save-browse only) single-file

UI (SvelteKit)

Variable Description Default
AUTH_SECRET HMAC signing secret for auth tokens dev_secret_change_in_production
SCRAPER_API_URL Internal URL of the Go scraper http://localhost:8080
POCKETBASE_URL PocketBase base URL http://localhost:8090
POCKETBASE_ADMIN_EMAIL PocketBase admin email admin@libnovel.local
POCKETBASE_ADMIN_PASSWORD PocketBase admin password changeme123
PUBLIC_MINIO_PUBLIC_URL Browser-visible MinIO URL (presigned links) http://localhost:9000

Docker

docker-compose up -d  # Starts: minio, minio-init, pocketbase, pb-init, scraper, ui

Services:

Service Port(s) Role
minio 9000 (S3 API), 9001 (console) Object storage
minio-init One-shot bucket creation then exits
pocketbase 8090 Structured data store
pb-init One-shot PocketBase collection bootstrap then exits
scraper 8080 Go scraper HTTP API
ui 5252 → internal 3000 SvelteKit frontend

Kokoro and Browserless are external services — not in docker-compose.

HTTP API Endpoints (Go scraper)

Method Path Description
GET /health Liveness probe
POST /scrape Enqueue full catalogue scrape
POST /scrape/book Enqueue single-book scrape {url}
POST /scrape/book/range Enqueue range scrape {url, from, to?}
GET /api/scrape/status Current scrape job status
GET /api/scrape/tasks All scrape task records
GET /api/browse Browse novelfire catalogue (MinIO-cached)
GET /api/search Search local + remote ?q=
GET /api/ranking Ranking list
GET /api/cover/{domain}/{slug} Proxy cover image from MinIO
GET /api/book-preview/{slug} Live metadata + chapter list (no store write)
GET /api/chapter-text-preview/{slug}/{n} Live chapter text (no store write)
POST /api/reindex/{slug} Rebuild chapters_idx from MinIO
GET /api/chapter-text/{slug}/{n} Chapter text (markdown stripped)
POST /api/audio/{slug}/{n} Trigger Kokoro TTS generation
GET /api/audio-proxy/{slug}/{n} Proxy generated audio
POST /api/audio/voice-samples Pre-generate voice samples
GET /api/voices List available Kokoro voices
GET /api/presign/chapter/{slug}/{n} Presigned MinIO URL for chapter
GET /api/presign/audio/{slug}/{n} Presigned MinIO URL for audio
GET /api/presign/voice-sample/{voice} Presigned MinIO URL for voice sample
GET /api/progress Get reading progress (session-scoped)
POST /api/progress/{slug} Set reading progress
DELETE /api/progress/{slug} Delete reading progress

Code Patterns

  • log/slog for structured logging throughout
  • Context-based cancellation on all network calls and goroutines
  • Worker pool pattern in orchestrator (buffered channel + WaitGroup)
  • Single async scrape job enforced by mutex; 409 on concurrent requests; job state persisted to scraping_tasks in PocketBase
  • Store interface decouples all persistence — pass it around, never touch MinIO/PocketBase clients directly outside storage/
  • Auth: custom HMAC-signed token (userId:username:role.<sig>) in libnovel_auth cookie; signed with AUTH_SECRET

AI Context Tips

  • Primary files to modify: orchestrator.go, server/handlers_*.go, novelfire/scraper.go, storage/hybrid.go, storage/pocketbase.go
  • To add a new scrape source: implement NovelScraper from internal/scraper/interfaces.go
  • To add a new API endpoint: add handler in the appropriate handlers_*.go file, register in server.go ListenAndServe()
  • Storage changes: update Store interface in store.go, implement on HybridStore (hybrid.go) and PocketBaseStore/MinioClient as needed; update mock in orchestrator_test.go
  • Skip: scraper/bin/ (compiled binary), MinIO/PocketBase data volumes

iOS App

See ios/AGENTS.md for full iOS/SwiftUI conventions.

Documentation Tools

This project has two MCP-backed documentation tools available. Use them proactively:

  • context7 — Live Apple SwiftUI/Swift docs, Go stdlib, SvelteKit, and any other library docs. Use before implementing anything non-trivial in Swift/SwiftUI. Example: use context7 to look up NavigationStack.
  • gh_grep — Search real-world code on GitHub for implementation patterns. Example: use gh_grep to find examples of background URLSession in Swift.