Commit Graph

29 Commits

Author SHA1 Message Date
Admin
acbfafb8cd fix(audio): remove speed from TTS/cache/MinIO keys; fix presigned URL host rewrite
- Speed is no longer part of Kokoro generation, in-memory cache keys, or
  MinIO object keys — audio is always generated at 1.0 and playback speed
  is applied client-side via audioEl.playbackRate
- presignAudio() now calls rewriteHost() so chapter audio URLs use the
  public MinIO endpoint (same as presignVoiceSample already did)
- docker-compose.yml: rename MINIO_PUBLIC_ENDPOINT → PUBLIC_MINIO_PUBLIC_URL
  for the ui service so SvelteKit's $env/dynamic/public picks it up
2026-03-04 19:30:46 +05:00
Admin
c8e0cf2813 feat(server): warm voice samples at startup
Add warmVoiceSamples() goroutine launched from ListenAndServe.
It waits up to 30s for Kokoro to become reachable, then generates
missing voice sample clips for all available voices and uploads them
to MinIO (_voice-samples/{voice}.mp3). Already-existing samples are
skipped, so the operation is idempotent on restarts.
2026-03-04 19:10:48 +05:00
Admin
3899a96576 feat(audio): add voice selector, voice samples, MediaSession cover art, and fix speed/voice bug
- Fix speed/voice bug: AudioPlayer no longer accepts speed/voice as props;
  startPlayback() reads audioStore.voice/speed directly instead of overwriting them
- Add GET /api/voices endpoint (Go) proxying Kokoro, cached in-memory
- Add POST /api/audio/voice-samples endpoint (Go) to pre-generate sample clips
  for all voices and store them in MinIO under _voice-samples/{voice}.mp3
- Add GET /api/presign/voice-sample/{voice} endpoint (Go)
- Add SvelteKit proxy routes: /api/voices, /api/presign/voice-sample, /api/audio/voice-samples
- Add presignVoiceSample() helper in minio.ts with proper host rewrite
- Pass book.cover through +page.server.ts -> +page.svelte -> AudioPlayer
- Set navigator.mediaSession.metadata on playback start so cover art,
  book title, and chapter title appear on phone lock screen / notification
2026-03-04 19:05:08 +05:00
Admin
cf0c0dfaaf fix(scraper): replace browserless with direct HTTP and fix presign 404 race
- Fix audio presign 404: MinIO upload is now synchronous before response is sent,
  eliminating the race where presign was called before the file landed in MinIO
- Replace all Browserless usage with direct HTTP client across catalogue, metadata,
  ranking, and browse — novelfire.net pages are server-rendered and don't need a
  headless browser; direct is faster and more reliable
- Harden handleBrowse with 3-attempt retry loop, proper backoff, and full
  browser-like headers to reduce 502s from novelfire.net bot detection
- Remove Browserless env vars (BROWSERLESS_URL/TOKEN/STRATEGY) from main.go;
  add SCRAPER_TIMEOUT as a single timeout knob
- Clean up now-dead rejectResourceTypes var and Browserless-specific WaitFor/
  RejectResourceTypes/GotoOptions fields from scraper calls
2026-03-04 17:32:18 +05:00
Admin
d14644238f fix(scraper): fix ScrapeCatalogue selectors, zero-byte cache skip, and wire ScrapeRanking into runAsync
- ScrapeCatalogue: use li.novel-item (was div), extract href from outer <a> and title from h4.novel-title (was h3), detect next page via rel=next (was class=next)
- handleBrowse/triggerBrowseSnapshot: treat zero-byte MinIO cache entries as misses, skip storing empty SingleFile output
- runAsync: after a successful full-catalogue run, call ScrapeRanking and upsert each result into PocketBase ranking collection
2026-03-04 16:54:00 +05:00
Admin
0d7b985469 fix(audio): return 404 from presign when audio object not yet uploaded
handlePresignAudio now checks AudioExists before presigning; previously it
would generate a valid-looking signed URL for a non-existent object, causing
the browser to get 404 from MinIO directly.

- Add AudioExists to Store interface and HybridStore
- Guard handlePresignAudio with AudioExists check, return 404 if missing
- Propagate 404 through presignAudio() in minio.ts (typed error.status=404)
- SvelteKit presign route forwards 404 to the browser instead of 500
2026-03-04 15:21:17 +05:00
Admin
11a846d043 feat(scraper): rewrite browse storage to domain/html+assets structure, populate ranking from snapshot
- Replace BrowsePageKey(genre/sort/status/type/page) with BrowseHTMLKey(domain, page) -> {domain}/html/page-{n}.html
- Add BrowseCoverKey(domain, slug) -> {domain}/assets/book-covers/{slug}.jpg
- Add SaveBrowseAsset/GetBrowseAsset for binary assets in browse bucket
- Rewrite triggerBrowseSnapshot: after storing HTML, parse it, upsert ranking records with MinIO cover keys, fire per-novel cover download goroutines
- Add handleGetCover endpoint (GET /api/cover/{domain}/{slug}) to proxy cover images from MinIO
- handleGetRanking rewrites MinIO cover keys to /api/cover/... proxy URLs
- Update save-browse CLI to use BrowseHTMLKey, populate ranking, and download covers
2026-03-04 15:13:09 +05:00
Admin
555973c053 feat(browse): add SingleFile browse-page snapshot cache via MinIO
- New MinIO bucket 'libnovel-browse' (MINIO_BUCKET_BROWSE env) for storing
  self-contained HTML snapshots of novelfire browse pages
- Store interface gains SaveBrowsePage / GetBrowsePage / BrowsePageKey methods
- handleBrowse is now cache-first: serves from MinIO snapshot when available,
  then fires a background triggerBrowseSnapshot goroutine to populate cache
  on live-fetch (de-duplicated, 90s timeout)
- New 'save-browse' CLI subcommand to bulk-capture pages via SingleFile CLI
- Dockerfile: downloads pinned single-file-x86_64-linux binary (v2.0.83),
  adds gcompat + libstdc++ to Alpine runtime for glibc compatibility
- docker-compose: adds libnovel-browse bucket init and SINGLEFILE_PATH env
- .gitignore: exclude scraper/scraper build artifact
2026-03-04 10:42:26 +05:00
Admin
f80b83309a fix(ui): install prod deps in Docker runtime; add reindex endpoint for chapters_idx
- ui/Dockerfile: copy package-lock.json and run npm ci --omit=dev in the
  runtime stage so marked (and other runtime deps) are available to
  adapter-node at startup — fixes ERR_MODULE_NOT_FOUND for 'marked'
- storage: add ReindexChapters to Store interface and HybridStore — walks
  MinIO objects for a slug, reads chapter titles, upserts chapters_idx
- server: add POST /api/reindex/{slug} to rebuild chapters_idx from MinIO
2026-03-04 00:59:36 +05:00
Admin
0e868506ca feat(server): track scrape jobs in scraping_tasks and expose GET /api/scrape/tasks
runAsync creates a scraping_tasks record on job start, flushes progress
counters via OnProgress, and finalizes status (done/failed/cancelled) on
completion. Adds GET /api/scrape/tasks to list all historical jobs.
Also fixes relative cover URLs in parseBrowsePage.
2026-03-04 00:40:32 +05:00
Admin
7acf04fb9f fix(storage): surface all silent errors with structured logging
- Inject *slog.Logger into HybridStore, PocketBaseStore, and pbClient
- Fix credential defaults in main.go (changeme123 / admin) to match docker-compose
- listOne/listAll/upsert/deleteWhere now return errors on non-2xx HTTP status
- WriteChapter: log warn instead of discarding UpsertChapterIdx error
- MetadataMtime, GetAudioCache, GetProgress: log warn on PocketBase failures
- EnsureCollections: log info/debug/warn per outcome instead of _ = err
- CountChapterIdx: log warn on failure instead of silently returning 0
- server: log warn when SetAudioCache fails after audio generation
- NewHybridStore: add explicit Ping() before EnsureCollections for fast-fail on bad credentials
2026-03-03 22:31:14 +05:00
Admin
af3c487afb feat(e2e): use direct HTTP for chapter scraping, cap TTS at 200 chars
- e2e fixture: replace single contentClient with directClient (plain HTTP)
  for chapter/metadata/ranking + contentClient (Browserless) for urlClient
  only — matches production wiring and is significantly faster
- server: add max_chars field to audio request body; truncates stripped text
  to N runes before sending to Kokoro (used by e2e for quick TTS tests)
- fix: move RankingItem to scraper package to break novelfire→storage import
  cycle; storage.RankingItem is now a type alias for backward compat
- fix: update stale New() call in novelfire integration test (missing args)
- fix: replace removed blob-ranking methods in storage integration test with
  current per-item API (UpsertRankingItem/ListRankingItems/RankingLastUpdated)
- justfile: add test-e2e and e2e tasks
2026-03-03 20:02:15 +05:00
Admin
b8d4d94b18 refactor(ranking): replace blob cache with per-item PocketBase storage
- Replace SetRanking/GetRanking/SetRankingPageHTML/GetRankingPageHTML blob methods
  with WriteRankingItem/ReadRankingItems/RankingFreshEnough per-item operations
- Add 24h staleness gate in ScrapeRanking to skip re-scraping fresh data
- Add GET /api/ranking endpoint returning []RankingItem sorted by rank
- Remove RankingPageCacher interface and rankingCacheAdapter adapter
- Update integration tests to use new per-item upsert semantics
- Include e2e test suite (scraper/internal/e2e/)
2026-03-03 19:37:49 +05:00
Admin
f265d9d020 feat(scraper): add GET /api/browse and GET /api/scrape/status endpoints
- GET /api/browse fetches novelfire.net catalogue page and parses it with golang.org/x/net/html;
  returns JSON {novels, page, hasNext} with per-novel slug/title/cover/rank/rating/chapters/url.
  Supports page, genre, sort, status query params.
- GET /api/scrape/status returns {"running": bool} for polling job state from the UI
2026-03-03 14:03:19 +05:00
Admin
38e400a4c7 feat(scraper): remove HTMX UI — delete ui.go, extract helpers.go, drop goldmark dependency 2026-03-02 22:03:36 +05:00
Admin
cb90771248 refactor(scraper): rename /ui/audio*, /ui/chapter-text routes to /api/ namespace 2026-03-02 22:01:52 +05:00
Admin
33e2a4dc01 feat: add MinIO presign endpoints to scraper API and SvelteKit presign helper 2026-03-02 21:35:27 +05:00
Admin
da4a182f85 Step 9: add session cookie + /api/progress GET/POST/DELETE endpoints 2026-03-02 14:47:58 +05:00
Admin
18e76c9668 steps 6-8: wire HybridStore into orchestrator, server, and main
- Add storage/hybrid.go: HybridStore composing PocketBase + MinIO backends
- Rewrite orchestrator to accept storage.Store instead of *writer.Writer
- Replace *writer.Writer with storage.Store in server.go and ui.go
- Wire audio cache, reading progress, chapter reads/writes through store
- Add rankingCacheAdapter in main.go to bridge context-free RankingPageCacher
  interface to HybridStore's context-aware methods
2026-03-02 14:44:02 +05:00
Admin
7589866965 Fetch voices from Kokoro API at runtime; replace select with styled voice card grid
Some checks failed
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Build (push) Has been cancelled
2026-03-02 10:18:43 +05:00
Admin
db59d55f42 Fix summary overlay scrolling and paragraph formatting on mobile
Some checks failed
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Build (push) Has been cancelled
- Make overlay scroll within the viewport instead of being clipped (overflow-y-auto on backdrop, items-start alignment)
- Split summary text into paragraphs in the zoom overlay (newline-split with sentence-boundary fallback)
- Add Summary heading in overlay for context
- Remove old merge-poller JS; simplify generateAudio to use data.url directly (Kokoro backend rewrite cleanup)
2026-03-01 23:37:05 +05:00
Admin
fb5d22eb8d Move scrape form to dedicated /scrape page, add '+ Add' link in home header
Some checks failed
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Build (push) Has been cancelled
- Extract scrape form and autocomplete JS from homeTmpl into new scrapeTmpl
- Add handleScrape GET handler serving /scrape with ranking autocomplete
- Register GET /scrape route in server.go
- Replace inline scrape form on home page with '+ Add' flat button in header
- handleHome no longer loads ranking items (only needed on /scrape)
2026-03-01 22:26:50 +05:00
Admin
26a46a4d31 Add chunked TTS audio generation and ranking search autocomplete on home page
Some checks failed
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Build (push) Has been cancelled
- Split chapter TTS into up to 10 paragraph-aligned parts; part 0 is
  generated synchronously so playback starts immediately, parts 1-9 and
  the final merge happen in a background goroutine
- New routes: GET /ui/audio/{slug}/{n}/status (merge poll) and
  GET /ui/audio-file/{slug}/{n}/part/{p} (serve individual part)
- JS polls status every 3 s and seamlessly swaps audio.src to the merged
  file once ready, preserving playback position proportionally
- Home page scrape form replaced with a ranking-search autocomplete:
  type a title/author to see matching ranking items (cover + metadata),
  click or keyboard-select to inject the source URL, or paste a raw URL
  directly; ranking data is embedded as JSON at page render time
2026-03-01 21:03:18 +05:00
Admin
81e5d015b4 feat: server-side TTS audio generation, audio queue panel, and home screen fix
Some checks failed
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Build (push) Has been cancelled
- Add POST /ui/audio/{slug}/{n} endpoint: calls Kokoro-FastAPI server-side,
  writes MP3 atomically to disk, deduplicates concurrent requests via
  in-flight channel map, wraps route with 10-min TimeoutHandler
- Add GET /ui/audio-file/{slug}/{n} endpoint: serves cached MP3 with 1-day
  cache headers
- Add AudioDir/AudioPath helpers to writer.go
- Rewrite JS TTS: remove all MSE/blob/stream code; use plain fetch to
  generate endpoint then set audio.src to returned URL
- Add AbortController to generateAudio; abort on stop() and navigation
- Keep voice/speed controls disabled until setPlaying() fires
- Reset prefetchFired on voice/speed change
- Upgrade prefetch from fire-and-forget to promise chain updating queue badge
- Add sticky bottom audio queue panel: always-visible scrubber with timestamps,
  expandable rows for Now/Next/Dbg with color-coded state badges
- Fix iOS Reader Mode: header->nav aria-hidden, audio outside nav, role=main,
  visible h1 in content area, hover-only paragraph highlight
- Fix home screen Available books hidden: stop hiding cards in #books-grid
  when they appear in Continue Reading; Available always shows all books
2026-03-01 20:44:03 +05:00
Admin
e9f880f7f7 fix: make ranking refresh async and use JS-rendered client for ScrapeRanking
Two bugs caused the 'Refresh Rankings' button to silently fail in production:

1. ScrapeRanking was using the plain HTTP client (s.client) instead of the
   browserless content client (s.urlClient). The /ranking page requires
   JavaScript rendering, so a plain fetch returned HTML without any novel
   entries. Now uses s.urlClient so the page is fully rendered before scraping.

2. handleRankingRefresh was synchronous, holding the HTTP connection open for
   up to 60 s while scraping. Reverse proxies and HTMX timeouts closed the
   connection before the scrape finished. Rewritten to the same async pattern
   used for book scraping: POST /ranking/refresh returns immediately with a
   polling badge; the browser polls GET /ui/ranking/status every 3 s; when
   the goroutine finishes the status endpoint sends HX-Redirect to /ranking.
2026-03-01 16:52:03 +05:00
Admin
bcdef02997 feat: paginate chapter list and track reading progress in localStorage
Chapter list:
- Book page now shows the first 50 chapters only; a 'Load more' button
  (HTMX, GET /books/{slug}/chapters-page?page=N) appends the next page
  without a full navigation, replacing itself with the next load-more
  button or disappearing when all chapters are loaded.

Reading progress:
- Visiting a chapter saves {slug: chapterN} to localStorage under
  'reading_progress'.
- The book page reads that key on load and, if a saved chapter exists,
  highlights the saved chapter row with an amber dot and shows a
  'Resume — Chapter N' button that navigates directly to it.
- Progress dots are also re-applied after each Load More via
  hx-on::after-request.
2026-03-01 16:39:26 +05:00
Admin
7879a51fe3 feat: add Kokoro TTS, ranking page, direct HTTP strategy, and chapter-number fix
- Add Kokoro-FastAPI TTS integration to the chapter reader UI:
  - Browser-side MSE streaming with paragraph-level click-to-start
  - Voice selector, speed slider, auto-next with prefetch of the next chapter
  - New GET /ui/chapter-text endpoint that strips Markdown and serves plain text

- Add ranking page (novelfire /ranking scraper, WriteRanking/ReadRankingItems
  in writer, GET /ranking + POST /ranking/refresh + GET /ranking/view routes)
  with local-library annotation and one-click scrape buttons

- Add StrategyDirect (plain HTTP client) as a new browser strategy; the
  default strategy is now 'direct' for chapter fetching and 'content'
  for chapter-list URL retrieval (split via BROWSERLESS_URL_STRATEGY)

- Fix chapter numbering bug: numbers are now derived from the URL path
  (/chapter-N) rather than list position, correcting newest-first ordering

- Add 'refresh <slug>' CLI sub-command to re-scrape a book from its saved
  source_url without knowing the original URL

- Extend NovelScraper interface with RankingProvider (ScrapeRanking)

- Tune scraper timeouts: wait-for-selector reduced to 5 s, GotoOptions
  timeout set to 60 s, content/scrape client defaults raised to 90 s

- Add cover extraction fix (figure.cover > img rather than bare img.cover)

- Add AGENTS.md and .aiignore for AI tooling context

- Add integration tests for browser client and novelfire scraper (build
  tag: integration) and unit tests for chapterNumberFromURL and pagination
2026-03-01 12:25:16 +05:00
Admin
e6e6f7dc4d feat: add exponential backoff, some UI elements to see the resut of a scrape 2026-02-26 18:51:32 +05:00
Admin
d68ea71239 feat: initial commit 2026-02-26 12:56:25 +05:00