A Garmin Audio Content Provider app that downloads and plays audiobooks from a self-hosted Audiobookshelf (ABS) server on a Garmin Tactix 8 (and other music-capable Garmin watches). Log in on the watch, browse your whole library, download a book, and listen offline — with two-way progress sync.
Status (build
b39): compiles clean across the supported watch package and is exercised end-to-end in the Forerunner 965 simulator. Login, Continue Listening, tail-only downloads, part-to-part playback, whole-book progress, completion, and two-way ABS progress sync are covered by the current test flow.Since b35: a long-book playback crash fixed, on-watch login no longer requires a system keyboard (vívoactive 4 / 4s, Venu / Venu D), an optional reverse-proxy auth header, podcast libraries, prebuilt multi-arch sidecar images, and live progress pushes that no longer clobber another device's newer position. The unit suite (
monkeyc -t) is 27 tests, plus 11 sidecar tests (npm test).b37/b38 add per-book playback speed (1.0x–2.0x), chosen from a downloaded book's playback menu. The watch keeps the previous encoding when there is room, so switching back to a speed you already have is instant. Requires an updated sidecar — the watch asks for
fmt=m4a3, which older ones reject.
A real ABS library is mostly single files of 200 MB – 1 GB (a 25-hour book is one 1 GB file). A watch can't download a file that big in one request, and it can't accept an "item detail" response large enough to even list a many-file book. So WatchShelf puts a tiny Node + ffmpeg sidecar in front of ABS, and the watch talks to only the sidecar:
- lean lists (
/libraries,/continue,/list,/authors,/series,/collections,/files) so nothing overflows the watch; /transcodecuts a small on-demand AAC/M4A chunk out of any file via HTTP Range (it never downloads the whole gigabyte), with a real container so the native player shows a position/time indicator;/coverserves the book's cover for menu thumbnails and player album art;/login+/progressproxy to ABS, so Audiobookshelf itself never has to be exposed to the internet — it can stay fully private.
The sidecar ships as a prebuilt multi-arch image —
ghcr.io/jedibrooker/watchshelf:latest (amd64 + arm64) — if you'd rather drop it
into an existing Docker stack than build it.
You expose the sidecar at any HTTPS URL and enter that URL on the watch. One command installs the sidecar and walks you through exposing it:
bash -c "$(curl -fsSL https://raw.githubusercontent.com/JediBrooker/WatchShelf/main/sidecar/install.sh)"
Or do it by hand — a plain, step-by-step guide (including why the sidecar is needed at all) for each of Cloudflare Tunnel, nginx, Apache, Caddy, and Traefik is in sidecar/GETTING_STARTED.md.
If you'd rather the sidecar didn't answer strangers at all, the watch can send a secret header on every request for your proxy to require — optional, set on-watch under Downloaded → Proxy header, with per-proxy snippets in the same guide.
Watch (WatchShelf, Monkey C)
| on-watch login: SIDECAR URL + username + password
|
|-- login: POST {sidecar}/login -> ABS token (stored; password discarded)
|-- browse: GET {sidecar}/{libraries|continue|list|authors|series|collections}
|-- open: GET {sidecar}/files?item -> files + saved progress (lean)
|-- queue: split only the unlistened suffix into ~3-min chunks
|-- sync: GET {sidecar}/transcode?item&file&start&end -> M4A chunk
| GET {sidecar}/cover?item -> cover art
|-- play: native media player -> Bluetooth
|-- progress: POST {sidecar}/progress -> sidecar PATCHes ABS
v
Sidecar (Node + ffmpeg) -> Audiobookshelf (internal)
- The watch only ever talks to the sidecar. ABS can stay on a private network.
- Two ways to set up. From the Connect IQ Store, fill
WatchShelf URLandAudiobookshelf API keyin Garmin Connect → WatchShelf → Settings and the watch never asks for anything. Sideloaded, there are no phone settings, so log in on the watch instead — a system keyboard where the device has one, and a character wheel onvivoactive4/4sandvenu/venud, which don't. A first run offers the choice; a re-login keeps the URL and only asks for what's missing. Only the ABS token is stored, never the password. - Chunks are ~3-min, 96 kbps mono AAC in a real M4A container (~2 MB) — the
container is what lets the native player show a position/time indicator. Tune in
source/Chunks.mc. - Playback speed is selected per book (1.0x–2.0x); progress stays on the Audiobookshelf source timeline.
- State lives in
Application.Storageas chunk params (not URLs) to stay small. - Playback is restricted to the selected book. The native player still owns its
part-local time bar, while the scrolling artist line shows
% of book.
CLI only, no editor — see BUILD.md.
make build # -> bin/WatchShelf.prg for the Tactix 8 Solar (fenix8solar51mm)
Sideload bin/WatchShelf.prg to the watch's GARMIN/APPS/ (the tactix 8 is MTP on
macOS — use OpenMTP or Android File Transfer; power-cycle the watch if a client can't
see it). WatchShelf appears under the watch's Music / audio providers. Open it →
Log in (enter your sidecar URL, then your ABS username + password) → Browse
library → Continue listening / All books / By author / By series / By collection
→ pick a book → its unlistened tail downloads in chunks.
Podcast libraries show up alongside audiobooks with no extra setup: the sidecar presents each show where authors normally appear and each episode as a book, so browsing, downloading and two-way progress sync all work as they do for books. Episodes must already be downloaded in ABS (or auto-download enabled for the show) before the watch can fetch them.
- Long books = many chunks (a 25-hour book → ~50). Sync is slow but works; each chunk is a small independent download.
- Garmin's native elapsed/total bar is scoped to the current downloaded part. WatchShelf adds whole-book percentage to the player metadata but cannot replace that native bar.
- Sideloaded installs have no phone settings (a Connect IQ limitation), so their setup is typed on the watch. Entering a URL, username and password on the character wheel is roughly 359 button presses — which is why a Store install, where Garmin Connect can supply the same values, is worth preferring.