Skip to content

feat: ai reading recommendations (read next and discover) through openrouter - #53

Merged
audemed44 merged 2 commits into
mainfrom
feat/ai-recommendations
Oct 13, 2026
Merged

audemed44 merged 2 commits into
mainfrom
feat/ai-recommendations

Conversation

@audemed44

Copy link
Copy Markdown
Owner

What

AI recommendations through OpenRouter, in two lists:

  • Read next: picks from the unread books in the library. The candidates are the next unread book of each series or web serial, plus unread standalones; DNF'd series and books turned down are left out. The top picks also show as an Up Next row on Home.
  • Discover: books and/or web serials you don't have yet. It can use OpenRouter's web search server tool (Exa, limited to royalroad.com, reddit.com, goodreads.com, scribblehub.com and novelupdates.com; up to 6 searches). Picks you already own (matched on normalised title or main title, series names and followed serials) and titles turned down earlier are dropped. Books link to a Z-Library search (the in-app search when an account is set, otherwise the site); Royal Road serials link to the fiction page found by search, or to a Royal Road title search.

New For You page (sidebar, or More on phones) with Read next / Discover / Saved tabs. Each pick has a reason and feedback buttons (save, read it, not for me, doesn't exist) with undo. Feedback is kept per title and fed into later runs.

What the model is told

Works grouped by series or web serial: finished/in progress counts, DNF, reading hours, ratings, when last read, genres and tags. Also genre and tag reading-hour totals, the followed web serials, and feedback lists. Reviews and highlights are only sent when "Share reviews and highlights" is on (off by default).

On a copy of the live library this comes to about 9k tokens for Read next and 6k for Discover, before search results.

Backend

  • internal/openrouter: small stdlib client for chat completions (structured outputs with require_parameters, web search server tool, usage and cost, citations) and the model list.
  • internal/recommend: builds the profile and prompts, reads answers leniently (code fences, chatty text), and matches titles against the library.
  • internal/server/recommendations.go: refreshes run in the background (one per kind at a time) and are recorded in recommendation_runs with tokens, searches and cost. The last good run's picks stay visible when a newer run fails. Runs left running by a restart are marked failed on start. An optional weekly refresh runs from the background loop. If the provider rejects structured output (400 or 404), the request is retried without it.
  • Migration 0003_recommendations.sql: tables for runs, picks and feedback.
  • Settings (stored in the settings table): model, web search, share notes, weekly refresh, discover kinds.

Config

Variable
OPENROUTER_API_KEY OpenRouter key; the feature is off without it (SHELFLOOM_OPENROUTER_API_KEY wins if both are set)
SHELFLOOM_OPENROUTER_MODEL Default model until one is picked in Settings (google/gemini-3.8-flash)
SHELFLOOM_OPENROUTER_BASE_URL API address, for a proxy or test server

Testing

  • Go tests: the OpenRouter client against a fake server (request shape, usage, errors); prompt and profile building, parsing and title matching; the full flow against a fake OpenRouter (candidates, privacy switch, owned and turned-down filtering, feedback, fallback without structured output, failed and interrupted runs, settings, model list).
  • Vitest: the For You page (lists, links, feedback and undo, refresh, error and disabled states) and the settings section.
  • Ran a test instance on a copy of the live database with a fake OpenRouter: both lists fill, owned titles (Pale Lights, A Practical Guide to Evil, Beware of Chicken) are dropped from Discover, and the pages were checked at desktop and phone widths.
  • Not tested with a real OpenRouter key (by design: secrets stay out of testing). The first real refresh is the first live check of the web search tool.

Two lists, refreshed on demand (or weekly) in the background:

- Read next: picks from the unread books in the library (the next
  unread book of each series, plus standalones).
- Discover: titles the reader doesn't have, books and/or web serials,
  optionally using OpenRouter's web search (Exa, limited to Royal Road,
  Reddit, Goodreads, Scribble Hub and Novel Updates).

The taste profile is built from the library: finished/started works
grouped by series or web serial, ratings, reading time, last read,
DNFs, genre/tag hours, followed serials and feedback on earlier picks.
Reviews and highlights are only sent when allowed in settings.

Feedback (not interested, already read, doesn't exist, saved) is kept
per title and fed into later runs. Runs record tokens, searches and
cost. The key comes from OPENROUTER_API_KEY (or
SHELFLOOM_OPENROUTER_API_KEY); the feature is off without it.
A For You page (More menu on phones) with three tabs: Read next (unread
library books, with covers and a Read button), Discover (books and web
serials to find, with a Royal Road or Z-Library link) and Saved. Each
pick shows the model's reason; feedback buttons (save, read it, not for
me, doesn't exist) hide the pick, with an undo, and shape the next
refresh. A refresh runs in the background and the page polls until it
is done; the last run's tokens, web searches and cost show below the
list, and a failed run's error shows above the previous picks.

Home gets an Up Next row with the read next picks. Settings gets a
Recommendations section: the model (with OpenRouter's price for it),
web search for Discover, sharing reviews and highlights (off by
default) and weekly refreshes. The Z-Library search opens with the
pick's title and author when Shelfloom has an account.
@audemed44
audemed44 merged commit 688bf27 into main Oct 13, 2026
2 checks passed
@audemed44
audemed44 deleted the feat/ai-recommendations branch October 13, 2026 04:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant