Sync book reading position to Bookhive atproto records from MoonReader
  • Python 86.1%
  • HTML 13.2%
  • Dockerfile 0.7%
Find a file
Brad Wenner a5ccd8e053
All checks were successful
CI / test (push) Successful in 15s
CI / image (push) Successful in 34s
feat: add ARM Docker build
2026-04-18 15:09:10 -07:00
.forgejo/workflows Derive CI registry coordinates from workflow context 2026-04-17 16:01:27 -07:00
.github/workflows feat: add ARM Docker build 2026-04-18 15:09:10 -07:00
src/spacebee Harden before going public 2026-04-17 16:13:30 -07:00
tests Harden before going public 2026-04-17 16:13:30 -07:00
.env.example Resolve PDS from handle; make PDS env optional 2026-04-17 15:06:23 -07:00
.gitignore Initial waggle scaffolding 2026-04-13 13:45:16 -07:00
CLAUDE.md Rename waggle to spacebee and genericize for public use 2026-04-17 14:56:13 -07:00
docker-compose.yml Default compose image to the public GHCR build 2026-04-17 21:27:33 -07:00
Dockerfile Pre-create /data/passthrough in the image so non-root startup works 2026-04-17 16:25:37 -07:00
icon.png Add icon to README 2026-04-17 21:17:24 -07:00
LICENSE Add MIT license 2026-04-17 21:06:22 -07:00
pyproject.toml Add MIT license 2026-04-17 21:06:22 -07:00
README.md update readme 2026-04-17 23:05:34 -07:00
sshot_moonreader.webp update readme 2026-04-17 23:05:34 -07:00
sshot_spacebee.png update readme 2026-04-17 23:05:34 -07:00
uv.lock Rename waggle to spacebee and genericize for public use 2026-04-17 14:56:13 -07:00

spacebee

spacebee

Sync Moon+ Reader reading position to your Atmosphere account and share your progress on bookhive.buzz.

It also serves a simple web page showing your reading status:

🚨 This is vibecoded, I don't know much Python but it works 👍

How to use

Spacebee pretends to be WebDAV but reads and writes from your Atmosphere account. In the Moon Reader app, go to Options->Sync with WebDAV and point it to spacebee:

Running locally

cp .env.example .env
uv sync --extra dev
uv run uvicorn spacebee.main:app --reload --port 8080

Point a test device at http://<wherever>:8080/ as the WebDAV target.

Docker

Copy compose file, fill in a .env, run:

curl -O https://raw.githubusercontent.com/oakbrad/spacebee/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/oakbrad/spacebee/main/.env.example
mv .env.example .env      # then edit with your creds
docker compose up -d

By default it listens on 127.0.0.1:8080.

Configuration

Copy .env.example to .env and fill in:

Var Purpose
BSKY_HANDLE The handle spacebee writes records as
BSKY_APP_PASSWORD An app password for that handle
DAV_USER / DAV_PASSWORD Basic-auth credentials Moon+ Reader will send
PASSTHROUGH_ROOT Local-disk scratch dir for non-.po paths
PDS Optional. If unset, resolved from the handle.

How it works

graph LR
    MR[Moon+ Reader] -->|"WebDAV: PROPFIND / GET / PUT"| SB[spacebee]
    SB -->|"buzz.bookhive.book"| PDS[ATProto PDS]
  • PROPFIND /Books/.Moon+/Cache/ synthesizes a directory listing from your buzz.bookhive.book records that have a bookProgress.moonReader.file field.
  • GET /Books/.Moon+/Cache/{file}.po returns the stored position string verbatim (preserves Moon+ Reader's internal chapter/offset encoding).
  • PUT /Books/.Moon+/Cache/{file}.po parses the .po body, finds the matching bookhive record (or catalog-searches and creates one), and updates bookProgress.{percent,currentChapter,moonReader} on your PDS.

Non-position WebDAV paths (Books/.Moon+/Settings/, backups, etc.) fall through to a local-disk scratch area rooted at $PASSTHROUGH_ROOT.

spacebee also serves a small read-only HTML dashboard at / that renders your bookhive records (currently-reading, finished, etc.). The dashboard and cover-image blob proxy at /blob/{cid} are public; all WebDAV endpoints are gated by HTTP Basic.

Record

{
  "$type": "buzz.bookhive.book",
  "bookProgress": {
    "percent": 11,
    "updatedAt": "2026-04-17T10:01:24.000Z",
    "moonReader": {
      "file": "The Necromancers House - Buehlman Christopher.epub.po",
      "position": "1703297605115*21@0#4826:11.1%",
      "syncedAt": "2026-04-17T10:01:24.000Z"
    },
    "currentChapter": 22
  }
}