an audible watchdog and local database for audiobook library changes 📚 and listening progress 🎧.
auklet watches your audible account, records every library and progress change as an event in a local sqlite database, and can rebuild its current state from the audible api at any time. the database is the journal; audible is the source of truth.
It is the audiobook sibling of spike (the Spotify watchdog), and can optionally push its data to a Journey server as audiobook.* items.
Requires Bun (uses bun:sqlite; no native build).
bun install
cp .env.example .env # set AUDIBLE_LOCALE to your marketplace (de, com, co.uk, …)
bun cli.js login # interactive: open the URL, sign in, paste the redirect URL back
bun index.js # start the watchdogThen open http://127.0.0.1:8899/browse.
Audible has no public API, so auklet authenticates the same way the mobile apps
do: PKCE device registration (audible-api-ts).
bun cli.js login prints an Amazon sign-in URL — open it in a browser, sign in
(handling any CAPTCHA/OTP there), and you'll be redirected to a blank/"error"
page. Copy that page's full URL from the address bar and paste it back. The
resulting credentials are saved to db/audible-auth.json and auto-refreshed.
Audible only exposes state (your current library and per-book progress), not a play-history feed. So auklet polls and diffs snapshots over time, recording change history into an append-only journal while treating Audible as the rebuildable source of truth.
Events — every poll re-fetches the full library and diffs it against the local set:
| kind | when |
|---|---|
library-added 📚 |
a book enters your library (first sync backfills with Audible's own purchase/added date) |
library-removed |
a book leaves your library |
Listen sessions 🎧 — each poll appends an immutable progress_snapshots row
for any book whose position advanced (position comes from duration − time_remaining, falling back to percent × duration). deriveSessions then
stitches consecutive snapshots into listen sessions — "listened to this book
from time A to time B, N seconds of audio" — splitting on gaps longer than
SESSION_GAP_S. Sessions are pure derived state and can be rebuilt from the
snapshots at any time (derive-sessions --rebuild). A book's pre-existing
progress at first sight is a baseline and is never counted as a listen.
Historical backfill — library only reports the current position, but
Audible's stats endpoints hold real history, which auklet backfills on startup
(and via auklet backfill): /1.0/stats/status/finished gives per-book finish
dates (recorded as exact, back-dated finish markers), and /1.0/stats/aggregates
gives Audible's monthly listening totals (whole account) going back to the
account's start — the activity tab. audible-api-ts doesn't wrap these, so
auklet signs the requests itself (reusing the device key).
The history keeps provenance honest: exact "✓ finished" dates from Audible
vs. ~-marked listening estimated from progress polls — never conflated.
Item IDs are deterministic (audiobook.book from audible|<asin>,
audiobook.listen from audible|<endedAt>|<asin>), so restarts and full
re-syncs upsert rather than duplicate.
The daemon is the single executor; the CLI is a thin HTTP client (except
login, which talks to Audible directly).
auklet login authenticate with audible
auklet sync-library refresh library from audible (rebuild current state)
auklet derive-sessions [--rebuild] (re)build listen sessions from snapshots
auklet backfill import historical finish dates + listening time (audible stats)
auklet activity monthly listening time, years back (audible stats)
auklet verify [--strict] [--deep] consistency + integrity checks
auklet journey-sync [--full] push books, listens, library events + covers to journey
auklet hydrate download cover art for new books
auklet stats library / finished / listening totals, top authors
auklet books [--q] [--sort author|series|progress|recent|title]
auklet sessions [--month] [--q]
auklet events [--kind] [--month] the library change log
AUKLET_URL (default http://127.0.0.1:8899) targets a remote daemon.
Journey is opt-in. Run journey-sync manually when JOURNEY_URL,
JOURNEY_TOKEN, and JOURNEY_CLIENT_ID are configured, or set
JOURNEY_ENABLED=true to let the daemon push automatically after each poll.
Without JOURNEY_ENABLED=true, auklet still hydrates covers and runs locally
without contacting Journey.
When enabled, auklet pushes to a Journey server as audiobook.book,
audiobook.listen, and audiobook.library_event items, uploading cover blobs
first.
Auklet owns the journey.audiobooks module (journey/ — manifest + schemas).
Register it once per server so pushes are accepted:
JOURNEY_SETUP_TOKEN=<server admin token> bun cli.js journey-registerOrdering doesn't matter: pushes before registration are rejected with the
retryable unknown_schema reason and drain on the next sync afterwards.
Re-run journey-register after schema changes — it upserts, and the server
refuses upgrades that would orphan a schema version still used by stored items.
Pushes are idempotent (deterministic IDs, cursor-based), so re-running is safe.
Docker Compose (recommended). Authenticate once (writes into the bind-mounted db/), then bring it up:
docker compose run --rm auklet bun cli.js login
docker compose up -d --buildsystemd — see deploy/auklet.service.
See .env.example. Key knobs: AUDIBLE_LOCALE (marketplace),
AUKLET_TZ (bucketing), PROGRESS_INTERVAL_S (poll cadence), SESSION_GAP_S
(session split threshold).