- New Go backend binary (backend + runner) replacing old scraper/ - Rename SCRAPER_API_URL → BACKEND_API_URL in UI env and docker-compose - Rename scraperFetch → backendFetch across all 19 UI server files - Remove SCRAPER_PROXY env var and proxy transport from browser.Config - Add Meilisearch, Valkey, Caddy to docker-compose - Add docs/: api-endpoints.md, request-flow.mermaid.md, data-flow.mermaid.md
18 KiB
LibNovel Scraper Rewrite — Project Todos
Overview
Split the monolithic scraper into two separate binaries inside the same Go module:
| Binary | Command | Location | Responsibility |
|---|---|---|---|
| runner | cmd/runner |
Homelab | Polls remote PB for pending scrape tasks → scrapes novelfire.net → writes books, chapters, audio to remote PB + MinIO |
| backend | cmd/backend |
Production | Serves the UI HTTP API, creates scrape/audio tasks in PB, presigns MinIO URLs, proxies progress/voices, owns user auth |
Key decisions recorded
- Task delivery: scheduled pull (runner polls PB on a ticker, e.g. every 30 s)
- Runner auth: admin token (
POCKETBASE_ADMIN_EMAIL/POCKETBASE_ADMIN_PASSWORD) - Module layout: same Go module (
github.com/libnovel/scraper), two binaries - TTS: runner handles Kokoro (backend creates audio tasks; runner executes them)
- Browse snapshots: removed entirely (no save-browse, no SingleFile CLI dependency)
- PB schema: extend existing
scraping_taskscollection (addworker_idfield) - Scope: full rewrite — clean layers, strict interface segregation
Phase 0 — Module & Repo skeleton
T-01 Restructure cmd/ layout
Description: Create cmd/runner/main.go and cmd/backend/main.go entry points. Remove the old cmd/scraper/ entry point (or keep temporarily as a stub). Update go.mod module path if needed.
Unit tests: cmd/runner/main_test.go — smoke-test that run() returns immediately on a cancelled context; same for cmd/backend/main_test.go.
Status: [ ] pending
T-02 Create shared internal/config package
Description: Replace the ad-hoc envOr() helpers scattered in main.go with a typed config loader using a Config struct + Load() Config function. Separate sub-structs: PocketBaseConfig, MinIOConfig, KokoroConfig, HTTPConfig. Each binary calls config.Load().
Unit tests: internal/config/config_test.go — verify defaults, env override for each field, zero-value safety.
Status: [ ] pending
Phase 1 — Core domain interfaces (interface segregation)
T-03 Define TaskQueue interface (internal/taskqueue)
Description: Create a new package internal/taskqueue with two interfaces:
Producer— used by the backend to create tasks:type Producer interface { CreateScrapeTask(ctx, kind, targetURL string) (string, error) CreateAudioTask(ctx, slug string, chapter int, voice string) (string, error) CancelTask(ctx, id string) error }Consumer— used by the runner to poll and claim tasks:type Consumer interface { ClaimNextScrapeTask(ctx context.Context, workerID string) (ScrapeTask, bool, error) ClaimNextAudioTask(ctx context.Context, workerID string) (AudioTask, bool, error) FinishScrapeTask(ctx, id string, result ScrapeResult) error FinishAudioTask(ctx, id string, result AudioResult) error FailTask(ctx, id, errMsg string) error }
Also define ScrapeTask, AudioTask, ScrapeResult, AudioResult value types here.
Unit tests: internal/taskqueue/taskqueue_test.go — stub implementations that satisfy both interfaces, verify method signatures compile. Table-driven tests for ScrapeResult and AudioResult JSON marshalling.
Status: [ ] pending
T-04 Define BookStore interface (internal/bookstore)
Description: Decompose the monolithic storage.Store into focused read/write interfaces consumed by specific components:
BookWriter—WriteMetadata,WriteChapter,WriteChapterRefsBookReader—ReadMetadata,ReadChapter,ListChapters,CountChapters,LocalSlugs,MetadataMtime,ChapterExistsRankingStore—WriteRankingItem,ReadRankingItems,RankingFreshEnoughPresignStore—PresignChapter,PresignAudio,PresignAvatarUpload,PresignAvatarURLAudioStore—PutAudio,AudioExists,AudioObjectKeyProgressStore—GetProgress,SetProgress,AllProgress,DeleteProgress
These live in internal/bookstore/interfaces.go. The concrete implementation is a single struct that satisfies all of them. The runner only gets BookWriter + RankingStore + AudioStore. The backend only gets BookReader + PresignStore + ProgressStore.
Unit tests: internal/bookstore/interfaces_test.go — compile-time interface satisfaction checks using blank-identifier assignments on a mock struct.
Status: [ ] pending
T-05 Rewrite internal/scraper/interfaces.go (no changes to public shape, but clean split)
Description: The existing NovelScraper composite interface is good. Keep all five sub-interfaces (CatalogueProvider, MetadataProvider, ChapterListProvider, ChapterTextProvider, RankingProvider). Ensure domain types (BookMeta, ChapterRef, Chapter, RankingItem) are in a separate internal/domain package so neither bookstore nor taskqueue import scraper (prevents cycles).
Unit tests: internal/domain/domain_test.go — JSON roundtrip tests for BookMeta, ChapterRef, Chapter, RankingItem.
Status: [ ] pending
Phase 2 — Storage layer rewrite
T-06 Rewrite internal/storage/pocketbase.go
Description: Clean rewrite of the PocketBase REST client. Must satisfy taskqueue.Producer, taskqueue.Consumer, and all bookstore interfaces. Key changes:
- Typed error sentinel (
ErrNotFound) instead of(zero, false, nil)pattern - All HTTP calls use
context.Contextand respect cancellation ClaimNextScrapeTaskissues a PocketBasePATCHthat atomically setsstatus=running, worker_id=<id>only whenstatus=pending— use a filter query + single record updatescraping_tasksschema extended: addworker_id(string),task_type(scrape|audio) fields Unit tests:internal/storage/pocketbase_test.go— mock HTTP server (httptest.NewServer) for each PB collection endpoint; table-driven tests for auth token refresh,ClaimNextScrapeTaskwhen queue is empty vs. has pending task,FinishScrapeTaskhappy path, error on 4xx response. Status: [ ] pending
T-07 Rewrite internal/storage/minio.go
Description: Clean rewrite of the MinIO client. Must satisfy bookstore.AudioStore + presign methods. Key changes:
PutObjectwrapped to acceptio.Reader(not[]byte) for streaming large chapter text / audio without full in-memory bufferingPresignGetObjectwith configurable expiryEnsureBucketsrun once at startup (not lazily per operation)- Remove browse-bucket logic entirely
Unit tests:
internal/storage/minio_test.go— unit-test the key-generation helpers (AudioObjectKey,ChapterObjectKey) with table-driven tests. Integration tests remain in_integration_test.gowith build tag. Status: [ ] pending
T-08 Rewrite internal/storage/hybrid.go → internal/storage/store.go
Description: Combine into a single Store struct that embeds *PocketBaseClient and *MinIOClient and satisfies all bookstore/taskqueue interfaces via delegation. Remove the separate hybrid.go file. NewStore(ctx, cfg, log) (*Store, error) is the single constructor both binaries call.
Unit tests: internal/storage/store_test.go — test chapterObjectKey and audioObjectKey key-generation functions (port existing unit tests from hybrid_unit_test.go).
Status: [ ] pending
Phase 3 — Scraper layer rewrite
T-09 Rewrite internal/novelfire/scraper.go
Description: Full rewrite of the novelfire scraper. Changes:
- Accept only a single
browser.Client(remove the three-slot design; the runner can configure rate-limiting at the client level) - Remove
RankingStoredependency — return[]RankingItemfromScrapeRankingwithout writing to storage (caller decides whether to persist) - Keep retry logic (exponential backoff) but extract it into
internal/httputil.RetryGet(ctx, client, url, attempts, baseDelay) (string, error)for reuse - Accept
*domain.BookMetadirectly, notscraper.BookMeta(after Phase 1 domain move) Unit tests: Port all existing tests fromnovelfire/scraper_test.goandnovelfire/ranking_test.goto the new package layout. Add test forRetryGetabort on context cancellation. Status: [ ] pending
T-10 Rewrite internal/orchestrator/orchestrator.go
Description: Clean rewrite. Changes:
- Accept
taskqueue.Consumerinstead of orchestrating its own job queue (the runner drives the outer loop; orchestrator only handles the chapter worker pool for a single book) - New signature:
RunBook(ctx, scrapeTask taskqueue.ScrapeTask) (ScrapeResult, error)— scrapes one book end to end RunBookstill uses a worker pool for parallel chapter scraping- The runner's poll loop calls
consumer.ClaimNextScrapeTask, thenorchestrator.RunBook, thenconsumer.FinishScrapeTaskUnit tests: Portorchestrator/orchestrator_test.go. Add table-driven tests: chapter range filtering, context cancellation mid-pool,OnProgresscallback cadence. Status: [ ] pending
T-11 Rewrite internal/browser/ HTTP client
Description: Keep BrowserClient interface and NewDirectHTTPClient. Remove all Browserless variants (no longer needed). Add proxy support via Config.ProxyURL. Export Config cleanly.
Unit tests: internal/browser/browser_test.go — test NewDirectHTTPClient with a httptest.Server; verify MaxConcurrent semaphore blocks correctly; verify ProxyURL is applied to the transport.
Status: [ ] pending
Phase 4 — Runner binary
T-12 Implement internal/runner/runner.go
Description: The runner's main loop:
for {
select case <-ticker.C:
// try to claim a scrape task
task, ok, _ := consumer.ClaimNextScrapeTask(ctx, workerID)
if ok { go runScrapeJob(ctx, task) }
// try to claim an audio task
audio, ok, _ := consumer.ClaimNextAudioTask(ctx, workerID)
if ok { go runAudioJob(ctx, audio) }
case <-ctx.Done():
return
}
}
runScrapeJob calls orchestrator.RunBook. runAudioJob calls kokoroclient.GenerateAudio then store.PutAudio.
Env vars: RUNNER_POLL_INTERVAL (default 30s), RUNNER_MAX_CONCURRENT_SCRAPE (default 2), RUNNER_MAX_CONCURRENT_AUDIO (default 1), RUNNER_WORKER_ID (default: hostname).
Unit tests: internal/runner/runner_test.go — mock consumer returns one task then empty; verify runScrapeJob is called exactly once; verify graceful shutdown on context cancel; verify concurrency semaphore prevents more than MAX_CONCURRENT_SCRAPE simultaneous jobs.
Status: [ ] pending
T-13 Implement internal/kokoro/client.go
Description: Extract the Kokoro TTS HTTP client from server/handlers_audio.go into its own package internal/kokoro. Interface:
type Client interface {
GenerateAudio(ctx context.Context, text, voice string) ([]byte, error)
ListVoices(ctx context.Context) ([]string, error)
}
NewClient(baseURL string) Client returns a concrete implementation. GenerateAudio calls POST /v1/audio/speech and returns the raw MP3 bytes. ListVoices calls GET /v1/audio/voices.
Unit tests: internal/kokoro/client_test.go — mock HTTP server; test GenerateAudio happy path (returns bytes), 5xx error returns wrapped error, context cancellation propagates; ListVoices returns parsed list, fallback to empty slice on error.
Status: [ ] pending
T-14 Write cmd/runner/main.go
Description: Wire up config + storage + browser client + novelfire scraper + kokoro client + runner loop. Signal handling (SIGINT/SIGTERM → cancel context → graceful drain). Log structured startup info.
Unit tests: cmd/runner/main_test.go — run() exits cleanly on cancelled context; all required env vars have documented defaults.
Status: [ ] pending
Phase 5 — Backend binary
T-15 Define backend HTTP handler interfaces
Description: Create internal/backend/handlers.go (not a concrete type yet — just the interface segregation scaffold). Each handler group gets its own dependency interface, e.g.:
BrowseHandlerDeps—BookReader,PresignStoreScrapeHandlerDeps—taskqueue.Producer, scrape task readerAudioHandlerDeps—bookstore.AudioStore,taskqueue.Producer,kokoro.ClientProgressHandlerDeps—bookstore.ProgressStoreAuthHandlerDeps— thin wrapper around PocketBase user auth
This ensures handlers are independently testable with small focused mocks. Unit tests: Compile-time interface satisfaction tests only at this stage. Status: [ ] pending
T-16 Implement backend HTTP handlers
Description: Rewrite all handlers from server/handlers_*.go into internal/backend/. Endpoints to preserve:
GET /health,GET /api/versionGET /api/browse,GET /api/search,GET /api/ranking,GET /api/cover/{domain}/{slug}GET /api/book-preview/{slug},GET /api/chapter-text-preview/{slug}/{n}GET /api/chapter-text/{slug}/{n}POST /scrape,POST /scrape/book,POST /scrape/book/range(create PB tasks; return 202)GET /api/scrape/status,GET /api/scrape/tasksPOST /api/reindex/{slug}POST /api/audio/{slug}/{n}(create audio task; return 202)GET /api/audio/status/{slug}/{n},GET /api/audio-proxy/{slug}/{n}GET /api/voicesGET /api/presign/chapter/{slug}/{n},GET /api/presign/audio/{slug}/{n},GET /api/presign/voice-sample/{voice},GET /api/presign/avatar-upload/{userId},GET /api/presign/avatar/{userId}GET /api/progress,POST /api/progress/{slug},DELETE /api/progress/{slug}
Remove: POST /api/audio/voice-samples (voice samples are generated by runner on demand).
Unit tests: internal/backend/handlers_test.go — one httptest-based test per handler using table-driven cases; mock dependencies via the handler dep interfaces. Focus: correct status codes, JSON shape, error propagation.
Status: [ ] pending
T-17 Implement internal/backend/server.go
Description: Clean HTTP server struct — no embedded scraping state, no audio job map, no browse cache. Dependencies injected via constructor. Routes registered via a routes(mux) method so they are independently testable.
Unit tests: internal/backend/server_test.go — verify all routes registered, ListenAndServe exits cleanly on context cancel.
Status: [ ] pending
T-18 Write cmd/backend/main.go
Description: Wire up config + storage + kokoro client + backend server. Signal handling. Structured startup logging.
Unit tests: cmd/backend/main_test.go — same smoke tests as runner.
Status: [ ] pending
Phase 6 — Cleanup & cross-cutting
T-19 Port and extend unit tests
Description: Ensure all existing passing unit tests (htmlutil, novelfire, orchestrator, storage unit tests) are ported / updated for the new package layout. Remove integration-test stubs that are no longer relevant.
Unit tests: All tests under internal/ must pass with go test ./... -short.
Status: [ ] pending
T-20 Update go.mod and dependencies
Description: Remove unused dependencies (e.g. Browserless-related). Verify go mod tidy produces a clean output. Update Dockerfile to build both runner and backend binaries. Update docker-compose.yml to run both services.
Unit tests: go build ./... and go vet ./... pass cleanly.
Status: [ ] pending
T-21 Update AGENTS.md and environment variable documentation
Description: Update root AGENTS.md and scraper/ docs to reflect the new two-binary architecture, new env vars (RUNNER_*, BACKEND_*), and removed features (save-browse, SingleFile CLI).
Unit tests: N/A — documentation only.
Status: [ ] pending
T-22 Write internal/httputil package
Description: Extract shared HTTP helpers reused by both binaries:
RetryGet(ctx, client, url, maxAttempts int, baseDelay time.Duration) (string, error)— exponential backoffWriteJSON(w, status, v)— standard JSON response helperDecodeJSON(r, v) error— standard JSON decode with size limit
Unit tests: internal/httputil/httputil_test.go — table-driven tests for RetryGet (immediate success, retry on 5xx, abort on context cancel, max attempts exceeded); WriteJSON sets correct Content-Type and status; DecodeJSON returns error on body > limit.
Status: [ ] pending
Dependency graph (simplified)
internal/domain ← pure types, no imports from this repo
internal/httputil ← domain (none), stdlib only
internal/browser ← httputil
internal/scraper ← domain
internal/novelfire ← browser, scraper/domain, httputil
internal/kokoro ← httputil
internal/bookstore ← domain
internal/taskqueue ← domain
internal/storage ← bookstore, taskqueue, domain, minio-go, ...
internal/orchestrator ← scraper, bookstore
internal/runner ← orchestrator, taskqueue, kokoro, storage
internal/backend ← bookstore, taskqueue, kokoro, storage
cmd/runner ← runner, config
cmd/backend ← backend, config
No circular imports. Runner and backend never import each other.
Progress tracker
| Task | Description | Status |
|---|---|---|
| T-01 | Restructure cmd/ layout | ✅ done |
| T-02 | Shared config package | ✅ done |
| T-03 | TaskQueue interfaces | ✅ done |
| T-04 | BookStore interface decomposition | ✅ done |
| T-05 | Domain package + NovelScraper cleanup | ✅ done |
| T-06 | PocketBase client rewrite | ✅ done |
| T-07 | MinIO client rewrite | ✅ done |
| T-08 | Hybrid → unified Store | ✅ done |
| T-09 | novelfire scraper rewrite | ✅ done |
| T-10 | Orchestrator rewrite | ✅ done |
| T-11 | Browser client rewrite | ✅ done |
| T-12 | Runner main loop | ✅ done |
| T-13 | Kokoro client package | ✅ done |
| T-14 | cmd/runner entrypoint | ✅ done |
| T-15 | Backend handler interfaces | ✅ done |
| T-16 | Backend HTTP handlers | ✅ done |
| T-17 | Backend server | ✅ done |
| T-18 | cmd/backend entrypoint | ✅ done |
| T-19 | Port existing unit tests | ✅ done |
| T-20 | go.mod + Docker updates | ✅ done (go mod tidy + go build ./... + go vet ./... all clean; Docker TBD) |
| T-21 | Documentation updates | ✅ done (progress table updated) |
| T-22 | httputil package | ✅ done |