A Garmin Connect IQ watch app for listening to your Audiobookshelf library, with playback progress kept in sync between the watch and your phone automatically.
There's no separate "companion phone app" to install - syncing between your watch and phone happens two ways, both driven by your Audiobookshelf server itself:
- Setup (watch ⇄ phone). You configure the server URL and API key from the Garmin Connect Mobile app on your phone (Settings for this watch app → see "Setup" below). That's the one place all configuration happens; the watch just receives it.
- Listening progress (watch ⇄ server ⇄ any other Audiobookshelf app). While you listen on the watch, it pushes your position back to the server. Whenever the watch's sync runs, it also pulls the latest position from the server. Since Audiobookshelf's official phone app and web player read/write that exact same position, listening on your phone and then picking up on the watch (or vice versa) resumes from the right place either way - the same mechanism the official apps already use to sync with each other.
Downloading books and audio playback itself go through Garmin's Music widget, using the same system framework Spotify/Amazon Music's Garmin apps use (an "audio content provider app") rather than a custom home screen - see "Using it on the watch" below.
- Generate an API key on your Audiobookshelf server: Settings → Users
→ API Keys → add one for your account, with no expiry. (Requires
Audiobookshelf ~v2.26+. On an older server, use your account's
non-expiring login token from
POST /logininstead - same field.) - Install ShelfSync on your watch (see "Before you build" below - this repo is source, not a built app).
- Open the Garmin Connect Mobile app → Garmin Devices → your watch →
installed apps → ShelfSync → App Settings, and enter:
- Server URL - e.g.
https://abs.example.com(must be reachable from your phone/watch - a local-network-only address won't work unless you're on that network or have a VPN) - API Key - from step 1
- Server URL - e.g.
ShelfSync doesn't have its own icon in the watch's app list - like Spotify/Amazon Music on Garmin, it's reached through the Music glance/ widget:
- Open Music on the watch → select ShelfSync as the source.
- Choose manage content (wording varies by device/firmware) to browse your library and check off books to download. Unchecking a downloaded book removes it from the watch.
- Choose play to pick which downloaded book is active, which starts playback and hands you Garmin's normal music-player controls (play/ pause, next/previous track, skip forward/back).
Downloads and progress sync both happen through the same "Sync" action in the Music widget - the OS decides when to run it (typically triggered by opening the widget, charging, or connecting to WiFi, same as any other music-provider app).
Audiobookshelf items are one or more audio files. ShelfSync treats each
file as one track, so Next/Previous on the watch moves between files.
For a library split into per-chapter files (common for most audiobooks),
that's real chapter navigation. For a book stored as a single .m4b file,
there's only one track, and no finer-grained seek - Garmin's audio content
provider API doesn't expose an absolute-seek call, so partway through a
single-file book you can't jump to an arbitrary timestamp beyond what
"skip forward/back" gives you. If your library has single-file audiobooks
and you want per-chapter navigation on the watch, re-encode/split them by
chapter server-side (a common Audiobookshelf library-management step).
- No playback speed control. Garmin's audio-content-provider framework
(
Toybox.Media) doesn't expose a speed-control API as far as could be confirmed against the current SDK/samples - unlike Audiobookshelf's own phone/web app. If a later Connect IQ SDK adds this, it isn't wired up yet. - One audiobook library. If your server has more than one library with
mediaType: book, only the first one found is browsable for now. - Unsupported audio formats are skipped. The Connect IQ Media API only documents MP3, M4A/M4B, WAV, and AAC/ADTS. FLAC/OGG tracks are silently dropped rather than downloaded (a whole book in an unsupported format never finishes syncing - it's simply skipped, not stuck retrying forever).
- Absolute resume-to-timestamp isn't guaranteed within a track. See "What 'chapter' navigation means" above - the watch resumes at the right file, matched against the latest known progress, but not necessarily the exact second, because the OS (not this app) owns a track's internal playback position.
This was written and reviewed without access to a Connect IQ SDK/simulator in the environment it was authored in - it has not been compiled or run on-device. Before you rely on it:
- Install the Connect IQ SDK and the VS Code Monkey C extension (or Eclipse-based tooling).
- Regenerate
manifest.xml's<iq:products>list. The device ids in this repo are a placeholder set of watches that have historically supported music-provider apps (fenix 7 family, epix 2, venu 2/3, some Forerunners) - device support for this app type and the exact id strings both depend on your installed SDK version. Use the SDK's project properties / device picker to regenerate this list rather than trusting it as-is, or the build will fail on an unknown device id. - Build and run in the simulator against a real (or locally-run) Audiobookshelf server, then sideload to a physical watch to verify downloads and playback - the simulator's music-provider support is limited for testing actual audio playback.
- Re-check the things flagged as unverified in the code comments (see
AbsContentIterator.mc,AbsSyncDelegate.mc) against the currentToybox.MediaAPI docs for your SDK version, since Garmin's docs site wasn't reachable while this was written and the implementation is based on reading Garmin's own official sample app (garmin/connectiq-apps/audio-provider/monkeymusic) source directly rather than the rendered API reference.
Once you've built it (see "Before you build" above), getting it onto a
physical watch doesn't require the Connect IQ Store - IQ apps install by
copying a compiled .prg file straight onto the device over USB.
There's no Bluetooth equivalent for a local build: Garmin Connect Mobile's Bluetooth install path only talks to the Connect IQ Store's servers, not to arbitrary files on your computer. The only way to get Bluetooth installs for a non-public build is to register a (free) Connect IQ developer account, upload the app to the Store dashboard as unpublished/in review, and add your own Garmin account as a tester - after that one-time upload, Connect Mobile installs and updates it over Bluetooth like any Store app. Worth it only if you're iterating without physical access to a cable; otherwise USB below is simpler.
-
Get a developer key (one-time, any Connect IQ project needs this to produce a signed build):
- In VS Code with the Monkey C extension: Command Palette →
Monkey C: Generate a Developer Key, save it somewhere outside this
repo (e.g.
~/.garmin/developer_key.der), and point the extension at it (monkeyC.developerKeyPathin VS Code settings, or it'll prompt you the first time you build). - Or via OpenSSL directly:
openssl genrsa -out developer_key.pem 4096 openssl pkcs8 -topk8 -inform PEM -outform DER -in developer_key.pem -out developer_key.der -nocrypt
Keep this key - you'll reuse the same one for every future build of this app; a different key produces a different app id and Garmin Connect Mobile will treat it as a brand new, separately-installed app.
- In VS Code with the Monkey C extension: Command Palette →
Monkey C: Generate a Developer Key, save it somewhere outside this
repo (e.g.
-
Regenerate the device list in
manifest.xmlfor your specific watch model first (see "Before you build" step 2) - building against an unknown/unsupported device id fails outright. -
Build for your device:
- VS Code: Command Palette → Monkey C: Build for Device, pick your
watch model. Output lands at
bin/<app>.prg. - CLI:
monkeyc -d <deviceId> -f monkey.jungle -o bin/ShelfSync.prg -y /path/to/developer_key.der(monkeycis in the SDK'sbin/directory - add it to yourPATH, or use the full path).
- VS Code: Command Palette → Monkey C: Build for Device, pick your
watch model. Output lands at
-
Copy it to the watch:
- Plug the watch in with a USB cable. It mounts as a normal drive
named
GARMIN. - Copy
bin/ShelfSync.prgintoGARMIN/GARMIN/APPS/on that drive. - On Windows, if Garmin Express is installed and running, it will
usually offer to install a
.prgautomatically when you double-click it with the watch connected - the manual copy above always works too and doesn't need Garmin Express at all. - Safely eject the drive, then unplug. The watch will index the new app - this can take a few seconds, occasionally a restart.
- Plug the watch in with a USB cable. It mounts as a normal drive
named
-
Configure it: open Garmin Connect Mobile on your phone → your watch → Connect IQ apps (or "More" → the app list) → ShelfSync → App Settings → enter your Audiobookshelf server URL and API key (see "Setup" above). This works the same for a sideloaded app as a Store-installed one - Connect Mobile reads the settings straight off the installed app package on the watch over Bluetooth.
-
Use it: on the watch, open the Music widget/glance and select ShelfSync as the source (see "Using it on the watch" above).
Debugging tip: System.println() output (used throughout this
codebase for error logging) is only visible in the Connect IQ Simulator's
console, not on a physical device - there's no on-device log viewer.
Reproduce a problem in the simulator against a real or locally-run
Audiobookshelf server first if something isn't working, since a sideloaded
build on hardware is effectively a black box otherwise.
manifest.xml App metadata, permissions, target devices
monkey.jungle Build config
resources/
strings/strings.xml All user-facing text
settings/{properties,settings}.xml Phone-configured settings (server URL, API key)
drawables/ Launcher/provider icon
source/
ShelfSyncApp.mc App entry point (AudioContentProviderApp)
AbsContentDelegate.mc Playback event hook -> progress push
AbsContentIterator.mc Per-book track playlist for the player
AbsSyncDelegate.mc Download/delete + progress sync on each OS-triggered sync
ConfigureSyncView.mc / ConfigureSyncMenuDelegate.mc "Manage content" (choose books to download)
ConfigurePlaybackView.mc / ConfigurePlaybackMenu(.mc/Delegate.mc) "Choose what to play"
AbsApi.mc Audiobookshelf REST client
ProgressSync.mc Bidirectional progress push/pull logic
HttpClient.mc / JsonResponseHandler.mc / AudioDownloadHandler.mc Shared networking
LocalStore.mc / AppConstants.mc On-device state (object store)
AppSettings.mc Phone-configured settings reader
ItemMeta.mc Defensive Audiobookshelf JSON parsing
ErrorView.mc / ErrorMessages.mc
Verified by reading the live advplyr/audiobookshelf server source
(v2.36.0) rather than the (now-unmaintained) hosted API docs:
- Auth:
POST /api/api-keys(admin) to mint a long-lived key; bearer token on every request. GET /api/libraries,GET /api/libraries/:id/items,GET /api/items/:id?expanded=1for browsing.GET /api/me/progressfor the current progress of every item (mediaProgress[]).POST /api/session/local-allfor reporting local progress - the same "offline-first" endpoint Audiobookshelf's own apps use to report playback sessions recorded while offline. The server only applies an entry if its own copy isn't already newer (PlaybackSessionManager.syncLocalSessioncomparesupdatedAtbefore writing), so a push can never clobber a fresher value written by another device in the meantime. Chosen over the simpler-lookingPATCH /api/me/progress/*endpoints for two reasons: those are a blind overwrite with no reconciliation at all, and Connect IQ'sCommunicationsmodule doesn't document an HTTP PATCH method (GET/POST/PUT/DELETE only).