No description
  • Go 82%
  • HTML 10.6%
  • CSS 6.9%
  • JavaScript 0.5%
Find a file
Arne Skaar Fismen ef082ac3b1 Description reveal and dropcap on the book page
Long descriptions (>1000 chars) are clamped with a one-way reveal:
the clamped paragraph itself is the click target via a hidden
checkbox + sibling selector; after expansion pointer-events are
disabled so it can't be collapsed. Clamp uses max-height + a
mask-image fade (not -webkit-box, which would break ::first-letter
in Safari).

Dropcap on the first letter of the description: float-based,
font-size 5rem × line-height 0.95 so the cap spans exactly three
body lines, with padding-right to give the body copy breathing
room. initial-letter was tried and reverted because Safari's
implementation ignores margin/padding.

Also: drop the .book-marginalia top border (the progress rule above
it already serves as the divider) and collapse the marginalia's top
margin so the rhythm stays consistent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 21:36:50 +00:00
design Description reveal and dropcap on the book page 2026-04-13 21:36:50 +00:00
docs Add openlibrary_url to books 2026-04-13 18:18:06 +00:00
internal Description reveal and dropcap on the book page 2026-04-13 21:36:50 +00:00
.gitignore Foundation: importer pipeline + storage (#1) 2026-04-10 20:32:45 +02:00
go.mod Kobo: progress tracking and kepubify (#8) 2026-04-12 15:24:43 +02:00
go.sum Kobo: progress tracking and kepubify (#8) 2026-04-12 15:24:43 +02:00
main.go Admin API: typed /api/admin/* surface (#13) 2026-04-13 14:28:21 +02:00
README.md htmx: shelve toggle, library search, header sign-in (#14) 2026-04-13 15:42:00 +02:00

books

Personal book server with a Kobo sync endpoint. Imports EPUBs from an inbox directory, stores them in a library, exposes a web UI over OIDC, and pretends to be the Kobo store so a stock-firmware Kobo will treat the shelved subset as "purchased" books and download them.

Status: used in production for one user and one device (a Libra Colour on firmware 4.45.23646). Everything else is speculative.

Running the server

The books binary reads config from environment variables. Minimum set:

BOOKS_DATA_DIR=/opt/books              # sqlite db, inbox/, library/
BOOKS_LISTEN_ADDR=0.0.0.0:8090
BOOKS_PUBLIC_URL=https://books.example.com
BOOKS_OIDC_ISSUER=https://auth.example.com
BOOKS_OIDC_CLIENT_ID=...
BOOKS_OIDC_CLIENT_SECRET=...
BOOKS_OIDC_ALLOWED_SUB=...             # the one user allowed to sign in

BOOKS_PUBLIC_URL is used as the base for every URL the Kobo is handed (api_endpoint, download URLs, covers). It must be publicly reachable from the device.

Build:

CGO_ENABLED=0 go build -o books .

modernc.org/sqlite is pure-Go, so CGO is off and the binary is statically linked. It runs fine on Alpine / musl.

Pointing a Kobo at the server

One-time: register the device on the web UI

  1. Sign in to the books web UI.
  2. Go to Shelf. Under "your kobo", enter a name and click Register.
  3. Copy the generated URL — it looks like https://books.example.com/api/kobo/<token>. It's per-device and acts as the device's credential; anyone with the URL can sync.

Edit Kobo eReader.conf over USB

The setup changes one line in the Kobo's config file. It does not need SSH, a factory reset, or any other Kobo-side modification. Nickel will rewrite every other URL it needs onto the new api_endpoint host automatically on next sync.

  1. Plug the Kobo into a computer via USB. It appears as a mass-storage volume called KOBOeReader.

  2. Open .kobo/Kobo/Kobo eReader.conf in a text editor.

  3. Under the [OneStoreServices] section, replace the line

    api_endpoint=https://storeapi.kobo.com
    

    with

    api_endpoint=https://books.example.com/api/kobo/<token>
    

    where <token> is the value from the web UI registration.

  4. Save the file, eject the volume, and reboot the Kobo.

On next sync the device will talk to books. Every other URL in [OneStoreServices] still points at storeapi.kobo.com, but Nickel rewrites the host at request time, so Cover images, metadata lookups, device auth, analytics, and the book download all land on the books server.

Verifying it worked

From the Kobo home screen, open Settings → Accounts → Sync now. In the books web log you should see:

GET /api/kobo/<token>/v1/initialization
POST /api/kobo/<token>/v1/auth/device
GET /api/kobo/<token>/v1/library/sync
...

and the device download flow (visible in the web log or the packetdump.debug Qt channel on the Kobo, see docs/kobo-api-findings.md).

Internals and protocol notes

See docs/kobo-api-findings.md. It has the full list of every Nickel quirk we've found, the nc :5001 live debug procedure, and the wire format for each endpoint — in particular the gotcha that /v1/library/{id}/metadata must be a JSON array, not a bare object.

Admin API

A token-authenticated JSON API at /api/admin/* for editing book metadata (search, PATCH fields, replace authors, rename authors/series, replace cover/file). See docs/superpowers/specs/2026-04-13-admin-api-design.md for the prose spec and docs/admin-api.openapi.yaml for the OpenAPI 3.1 schema. Authenticate with Authorization: Bearer $BOOKS_ADMIN_TOKEN; if the env var is unset the entire mount returns 404.

Frontend

Server-rendered HTML with htmx for interaction polish (shelve toggle, library search). htmx is vendored at design/htmx.min.js (version recorded in design/htmx.min.js.version). To bump: download the new release artifact from https://github.com/bigskysoftware/htmx/releases, overwrite both files, and verify the existing tests pass.