rockbox-sdk#
Async Python SDK for Rockbox Zig — a typed, batteries-included
client for the GraphQL API exposed by rockboxd.
import asyncio
from rockbox_sdk import RockboxClient, PlaybackStatus
async def main():
async with RockboxClient(host="localhost") as client:
track = await client.playback.current_track()
if track:
print(f"Now: {track.title} — {track.artist}")
if await client.playback.status() == PlaybackStatus.PAUSED:
await client.playback.resume()
asyncio.run(main())
Highlights#
- Async-first — built on
httpx+websockets. Useawaiteverywhere. - Domain-namespaced API —
client.playback.*,client.library.*,client.sound.*, … - Typed responses — every reply is a Pydantic model with snake_case fields.
- Real-time events —
connect()opens a WebSocket and forwardstrack:changed/status:changed/playlist:changedto listeners. - Builder API —
RockboxClient.builder().host(...).port(...).build(). - Plugin system — Jellyfin-style install/uninstall lifecycle.
- Python-friendly — context manager, decorator listeners, dataclass inputs.
Install#
uv add rockbox-sdk
# or
pip install rockbox-sdk
Requires Python 3.10+ and a running rockboxd (default port 6062).
Try it in the REPL#
The SDK is async-first. The recommended REPL is IPython — await works at
the top level, and you get tab-completion on models, inline docs with ?, and
%timeit for benchmarking:
uv run ipython
In [1]: from rockbox_sdk import RockboxClient, PlaybackStatus
In [2]: client = RockboxClient(host="localhost", port=6062)
In [3]: await client.playback.status()
Out[3]: <PlaybackStatus.PLAYING: 1>
In [4]: track = await client.playback.current_track()
In [5]: track.title, track.artist
Out[5]: ('Money', 'Pink Floyd')
In [6]: await client.sound.get_volume()
Out[6]: VolumeInfo(volume=-12, min=-74, max=6)
In [7]: await client.library.search("daft punk")
In [8]: await client.aclose()
You can also test offline — models, enums, and the builder don't need a server:
In [1]: from rockbox_sdk import RockboxClient, Track, InsertPosition
In [2]: Track.model_validate({"title": "Money", "albumArt": "x.jpg"}).album_art
Out[2]: 'x.jpg'
In [3]: RockboxClient.builder().host("nas.local").build()._config.resolve_http_url()
Out[3]: 'http://nas.local:6062/graphql'
If you prefer the stdlib REPL, python -m asyncio also supports top-level
await, or wrap each call in asyncio.run(...) in a plain python session:
uv run python -m asyncio
>>> from rockbox_sdk import RockboxClient
>>> client = RockboxClient()
>>> await client.playback.status()
Subscriptions (await client.connect()) keep firing in the background between
prompts in both REPLs.
Configure#
from rockbox_sdk import RockboxClient
# Direct kwargs
client = RockboxClient(host="192.168.1.42", port=6062)
# Or fluent builder
client = (
RockboxClient.builder()
.host("nas.local")
.port(6062)
.timeout(15)
.build()
)
# Or full URL override
client = RockboxClient(
http_url="http://nas.local:6062/graphql",
ws_url="ws://nas.local:6062/graphql",
)
Always call await client.aclose() when you're done — or use it as an
async context manager:
async with RockboxClient() as client:
...
Domains#
| Namespace | What it does |
|---|---|
client.playback |
Transport (play/pause/seek), play helpers |
client.library |
Albums, artists, tracks, search, likes, scan |
client.playlist |
The active queue (insert/remove/shuffle/start) |
client.saved_playlists |
Persistent playlists & folders |
client.smart_playlists |
Rule-based playlists & listening stats |
client.sound |
Volume control |
client.settings |
Global EQ / replaygain / crossfade / shuffle / … |
client.system |
Version, runtime info |
client.browse |
Filesystem & UPnP browser |
client.devices |
Cast / source device discovery |
client.bluetooth |
Bluetooth pairing & scanning (Linux only) |
Real-time events#
from rockbox_sdk import RockboxClient, TRACK_CHANGED, STATUS_CHANGED
async with RockboxClient() as client:
await client.connect() # opens the WebSocket
@client.on(TRACK_CHANGED)
async def on_track(track):
print(f"▶ {track.title} — {track.artist}")
@client.on(STATUS_CHANGED)
def on_status(raw_status):
print(f"◐ status = {raw_status}")
await asyncio.Event().wait() # run forever
Convenience wrappers exist (client.on_track_changed(...),
client.on_status_changed(...), client.on_playlist_changed(...)).
Plugins#
A plugin is anything matching the RockboxPlugin protocol — a name, version,
install(context), and optionally uninstall():
from rockbox_sdk import RockboxClient, PlaybackStatus, RockboxPlugin
class SleepTimer:
name = "sleep-timer"
version = "1.0.0"
description = "Stop playback after N minutes"
def __init__(self, minutes: int) -> None:
self.minutes = minutes
self._task: asyncio.Task | None = None
def install(self, ctx):
async def fire():
await asyncio.sleep(self.minutes * 60)
await ctx.query("mutation { hardStop }")
self._task = asyncio.create_task(fire())
@ctx.events.on("status:changed")
def cancel_on_stop(status: int):
if status == PlaybackStatus.STOPPED and self._task:
self._task.cancel()
def uninstall(self):
if self._task:
self._task.cancel()
async with RockboxClient() as client:
await client.connect()
await client.use(SleepTimer(30))
Raw GraphQL escape hatch#
data = await client.query(
"query Volume { volume { volume min max } }"
)
Examples#
See examples/ for runnable scripts mirroring the TypeScript SDK examples:
01-basic-playback.py02-now-playing.py03-library-search.py04-queue-management.py05-volume-control.py06-plugin-sleep-timer.py
Run with:
uv run python examples/01_basic_playback.py
License#
MIT License. See LICENSE for details.