rockbox_ex
Idiomatic Elixir SDK for Rockbox Zig — a fully typed
GraphQL client for rockboxd with real-time WebSocket subscriptions, a
plugin behaviour, and a builder DSL for smart playlists.
- Pipe-friendly — every API function takes the client as its first arg.
- Builder-friendly — smart-playlist rules and partial settings updates
compose with
|>. - Tagged tuples or bangs —
name/N → {:ok, value} | {:error, exception}, with a matchingname!/Nthat raises. - Real-time events as messages —
Rockbox.subscribe(:track_changed)and receive{:rockbox, :track_changed, %Rockbox.Track{}}. - Plugins — implement
Rockbox.Pluginand install withRockbox.use_plugin/2.
Table of contents
- Installation
- Quick start
- Configuration
- API reference
- Real-time events
- Plugins
- Error handling
- Raw GraphQL queries
Installation
def deps do
[
{:rockbox_ex, "~> 0.1"}
]
endrockboxd must be running and reachable. By default the SDK connects to
http://localhost:6062/graphql. Start rockboxd with:
rockbox startQuick start
client = Rockbox.new()
# Optional: open the WebSocket so subscribers receive events
{:ok, _pid} = Rockbox.connect(client)
# What's playing right now?
case Rockbox.Playback.current_track(client) do
{:ok, %Rockbox.Track{} = t} -> IO.puts("▶ #{t.title} — #{t.artist}")
{:ok, nil} -> IO.puts("Nothing is playing.")
end
# Search the library
{:ok, results} = Rockbox.Library.search(client, "dark side")
album = List.first(results.albums)
# Play it shuffled
:ok = Rockbox.Playback.play_album(client, album.id, shuffle: true)
# React to track changes
:ok = Rockbox.subscribe(:track_changed)
receive do
{:rockbox, :track_changed, track} ->
IO.puts("Now: #{track.title}")
end
# Tear down when done
Rockbox.disconnect(client)Configuration
# Defaults: localhost:6062
client = Rockbox.new()
# Custom host and port
client = Rockbox.new(host: "192.168.1.42", port: 6062)
# Fully custom URLs (useful behind a reverse proxy)
client = Rockbox.new(
http_url: "https://music.home/graphql",
ws_url: "wss://music.home/graphql"
)| Option | Type | Default | Description |
|---|---|---|---|
:host | String.t() | "localhost" | Hostname or IP of rockboxd |
:port | non_neg_integer() | 6062 | GraphQL HTTP/WS port |
:http_url | String.t() | http://{host}:{port}/graphql | Override the full HTTP URL |
:ws_url | String.t() | ws://{host}:{port}/graphql | Override the full WebSocket URL |
:headers | [{String.t(), String.t()}] | [] | Extra HTTP request headers |
:timeout | non_neg_integer() | 15_000 | HTTP request timeout (ms) |
API reference
Every function comes in two flavors:
name/N → {:ok, value} | {:error, exception}— forwith/casepipelines.name!/N → value— raisesRockbox.Error(or a subclass) on failure.
Playback
# Status — returns an atom: :stopped | :playing | :paused
{:ok, :playing} = Rockbox.Playback.status(client)
# Toggle
case Rockbox.Playback.status!(client) do
:playing -> Rockbox.Playback.pause(client)
_ -> Rockbox.Playback.resume(client)
end
# Transport
:ok = Rockbox.Playback.next(client)
:ok = Rockbox.Playback.previous(client)
:ok = Rockbox.Playback.stop(client)
# Seek to absolute position (ms)
:ok = Rockbox.Playback.seek(client, 90_000)
# Current / next track — returns nil when stopped
{:ok, %Rockbox.Track{title: t}} = Rockbox.Playback.current_track(client)
{:ok, _next} = Rockbox.Playback.next_track(client)
# Play helpers — single-call shortcuts
:ok = Rockbox.Playback.play_track(client, "/Music/foo.mp3")
:ok = Rockbox.Playback.play_album(client, "album-id", shuffle: true)
:ok = Rockbox.Playback.play_artist(client, "artist-id", shuffle: true)
:ok = Rockbox.Playback.play_playlist(client, "playlist-id")
:ok = Rockbox.Playback.play_directory(client, "/Music/Jazz", recurse: true, shuffle: true)
:ok = Rockbox.Playback.play_liked_tracks(client, shuffle: true)
:ok = Rockbox.Playback.play_all_tracks(client, shuffle: true)Rockbox.Track exposes a couple of helpers:
Rockbox.Track.format_length(track) # "4:32"
Rockbox.Track.format_elapsed(track) # "1:14"
Rockbox.Track.progress(track) # 0.27 (0.0–1.0)Library
# Albums
{:ok, albums} = Rockbox.Library.albums(client)
{:ok, album} = Rockbox.Library.album(client, "album-id") # full track list
{:ok, liked} = Rockbox.Library.liked_albums(client)
:ok = Rockbox.Library.like_album(client, "album-id")
:ok = Rockbox.Library.unlike_album(client, "album-id")
# Artists
{:ok, artists} = Rockbox.Library.artists(client)
{:ok, artist} = Rockbox.Library.artist(client, "artist-id")
# Tracks
{:ok, tracks} = Rockbox.Library.tracks(client)
{:ok, track} = Rockbox.Library.track(client, "track-id")
{:ok, liked} = Rockbox.Library.liked_tracks(client)
:ok = Rockbox.Library.like_track(client, "track-id")
:ok = Rockbox.Library.unlike_track(client, "track-id")
# Search across artists, albums, tracks, liked
{:ok, results} = Rockbox.Library.search(client, "radiohead")
results.artists # [%Rockbox.Artist{}, ...]
results.albums # [%Rockbox.Album{}, ...]
results.tracks # [%Rockbox.Track{}, ...]
results.liked_tracks
results.liked_albums
# Trigger a full library rescan
:ok = Rockbox.Library.scan(client)Queue (live playlist)
The queue is the live playback list — what plays right now. For persistent named collections see Saved playlists.
{:ok, queue} = Rockbox.Queue.current(client)
queue.amount # total tracks
queue.index # 0-based position of the currently playing track
queue.tracks # [%Rockbox.Track{}, ...]
Rockbox.Playlist.current_track(queue) # convenience helper
# Insertion: position is :next | :after_current | :last | :first
:ok = Rockbox.Queue.insert_tracks(client, ["/Music/a.mp3", "/Music/b.mp3"], :next)
:ok = Rockbox.Queue.insert_directory(client, "/Music/Ambient", :last)
:ok = Rockbox.Queue.insert_album(client, "album-id", :next)
# Other ops
:ok = Rockbox.Queue.remove_track(client, 2)
:ok = Rockbox.Queue.clear(client)
:ok = Rockbox.Queue.shuffle(client)
:ok = Rockbox.Queue.create(client, "Evening Mix", ["/a.mp3", "/b.mp3"])
:ok = Rockbox.Queue.resume(client)
# Pipe-friendly chaining with bang variants
client
|> tap(&Rockbox.Queue.clear!/1)
|> tap(&Rockbox.Queue.insert_tracks!(&1, ["/Music/a.mp3"], :last))
|> Rockbox.Queue.shuffle!()Saved playlists
{:ok, lists} = Rockbox.SavedPlaylists.list(client)
{:ok, lists} = Rockbox.SavedPlaylists.list(client, "folder-id")
{:ok, pl} = Rockbox.SavedPlaylists.get(client, "playlist-id")
{:ok, ids} = Rockbox.SavedPlaylists.track_ids(client, "playlist-id")
# Create
{:ok, pl} =
Rockbox.SavedPlaylists.create(client,
name: "Late Night Jazz",
description: "Quiet music for working",
folder_id: "folder-id", # optional
track_ids: ["t1", "t2", "t3"] # optional
)
# Update / add / remove
:ok = Rockbox.SavedPlaylists.update(client, pl.id, name: "Late Night Jazz (v2)")
:ok = Rockbox.SavedPlaylists.add_tracks(client, pl.id, ["t4", "t5"])
:ok = Rockbox.SavedPlaylists.remove_track(client, pl.id, "t1")
# Play / delete
:ok = Rockbox.SavedPlaylists.play(client, pl.id)
:ok = Rockbox.SavedPlaylists.delete(client, pl.id)
# Folders
{:ok, folders} = Rockbox.SavedPlaylists.folders(client)
{:ok, folder} = Rockbox.SavedPlaylists.create_folder(client, "Work")
:ok = Rockbox.SavedPlaylists.delete_folder(client, folder.id)Smart playlists
Use the Rockbox.SmartPlaylist.Rules builder — pipe-friendly, type-safe.
alias Rockbox.SmartPlaylist.Rules
rules =
Rules.all_of()
|> Rules.where(:play_count, :gte, 10)
|> Rules.where(:last_played, :within, "30d")
|> Rules.sort(:play_count, :desc)
|> Rules.limit(50)
|> Rules.to_json()
{:ok, sp} =
Rockbox.SmartPlaylists.create(client,
name: "Most played (last 30d)",
description: "Top 50 most-played tracks from the last month",
rules: rules
)
{:ok, ids} = Rockbox.SmartPlaylists.track_ids(client, sp.id)
:ok = Rockbox.SmartPlaylists.play(client, sp.id)
:ok = Rockbox.SmartPlaylists.delete(client, sp.id)
# OR groups
or_rules =
Rules.any_of()
|> Rules.where(:title, :contains, "Live")
|> Rules.where(:title, :contains, "Acoustic")
# Mixed AND/OR via where_group/2
mixed =
Rules.all_of()
|> Rules.where(:play_count, :gt, 0)
|> Rules.where_group(or_rules)
|> Rules.to_json()Listening stats
{:ok, stats} = Rockbox.SmartPlaylists.track_stats(client, "track-id")
# Record events manually (e.g. from a scrobbler plugin)
:ok = Rockbox.SmartPlaylists.record_played(client, "track-id")
:ok = Rockbox.SmartPlaylists.record_skipped(client, "track-id")Sound
Volume is adjusted in firmware-defined steps. The number of steps per dB
varies by hardware target — always inspect volume/1 for the range.
{:ok, %Rockbox.Volume{volume: v, min: lo, max: hi}} = Rockbox.Sound.volume(client)
{:ok, new_value} = Rockbox.Sound.adjust(client, 3) # +3 steps
{:ok, _} = Rockbox.Sound.up(client) # +1
{:ok, _} = Rockbox.Sound.down(client) # -1Settings
update/2 accepts any subset of fields — only the ones you pass are written.
{:ok, settings} = Rockbox.Settings.get(client)
# Toggle shuffle + repeat
:ok = Rockbox.Settings.update(client, shuffle: true, repeat_mode: 1)
# Equalizer
:ok =
Rockbox.Settings.update(client,
eq_enabled: true,
eq_precut: -3,
eq_band_settings: [
%{cutoff: 60, q: 7, gain: 3},
%{cutoff: 200, q: 7, gain: 0},
%{cutoff: 4000, q: 7, gain: -2}
]
)
# Compressor
:ok =
Rockbox.Settings.update(client,
compressor_settings: %{
threshold: -24, makeup_gain: 3, ratio: 2,
knee: 0, release_time: 100, attack_time: 5
}
)
# Replaygain
:ok =
Rockbox.Settings.update(client,
replaygain_settings: %{noclip: true, type: 1, preamp: 0}
)System
{:ok, version} = Rockbox.System.version(client)
{:ok, status} = Rockbox.System.status(client)
status.runtime # seconds since boot
status.topruntime # peak runtime
status.resume_index # last queued positionBrowse (filesystem)
{:ok, entries} = Rockbox.Browse.entries(client) # music_dir root
{:ok, entries} = Rockbox.Browse.entries(client, "/Music/Pink Floyd")
for e <- entries do
icon = if Rockbox.Entry.directory?(e), do: "📁", else: "🎵"
IO.puts("#{icon} #{e.name}")
end
{:ok, dirs} = Rockbox.Browse.directories(client, "/Music")
{:ok, files} = Rockbox.Browse.files(client, "/Music/Pink Floyd/The Wall")Devices
{:ok, devices} = Rockbox.Devices.list(client)
{:ok, device} = Rockbox.Devices.get(client, "device-id")
# Connect — switches the active PCM output sink to this device
:ok = Rockbox.Devices.connect(client, "chromecast-id")
:ok = Rockbox.Devices.disconnect(client, "chromecast-id")Bluetooth
Linux only — backed by BlueZ. Calls return a Rockbox.GraphQLError on
non-Linux hosts.
{:ok, devices} = Rockbox.Bluetooth.devices(client)
{:ok, found} = Rockbox.Bluetooth.scan(client, 10) # 10 second scan
:ok = Rockbox.Bluetooth.connect(client, "AA:BB:CC:DD:EE:FF")
:ok = Rockbox.Bluetooth.disconnect(client, "AA:BB:CC:DD:EE:FF")Real-time events
Open the WebSocket once with Rockbox.connect/1. The connection is supervised
and auto-reconnects with exponential backoff (capped at 30 s). Subscribers
receive plain Erlang messages, so they integrate with receive blocks and
GenServer.handle_info/2.
client = Rockbox.new()
{:ok, _pid} = Rockbox.connect(client)
:ok = Rockbox.subscribe(:track_changed)
:ok = Rockbox.subscribe([:status_changed, :playlist_changed]) # multiple
:ok = Rockbox.subscribe(:all) # catch-all
receive do
{:rockbox, :track_changed, %Rockbox.Track{} = track} ->
IO.puts("▶ #{track.title} — #{track.artist}")
{:rockbox, :status_changed, status} ->
IO.puts("status → #{status}") # :stopped | :playing | :paused
{:rockbox, :playlist_changed, %Rockbox.Playlist{} = queue} ->
IO.puts("queue is now #{queue.amount} tracks")
end
Rockbox.unsubscribe(:track_changed)
Rockbox.disconnect(client)Event map
| Event | Payload |
|---|---|
:track_changed | %Rockbox.Track{} |
| :status_changed | :stopped | :playing | :paused |
| :playlist_changed | %Rockbox.Playlist{} |
| :ws_open | nil |
| :ws_close | nil |
| :ws_error | Exception.t() |
Subscribers are auto-removed when their process exits — no manual cleanup needed.
Inside a GenServer
defmodule MyApp.NowPlaying do
use GenServer
def start_link(client), do: GenServer.start_link(__MODULE__, client, name: __MODULE__)
@impl true
def init(client) do
Rockbox.connect(client)
Rockbox.subscribe([:track_changed, :status_changed])
{:ok, %{client: client, track: nil, status: :stopped}}
end
@impl true
def handle_info({:rockbox, :track_changed, track}, state),
do: {:noreply, %{state | track: track}}
def handle_info({:rockbox, :status_changed, status}, state),
do: {:noreply, %{state | status: status}}
endPlugins
Plugins are the recommended way to bolt on cross-cutting features — scrobbling, desktop notifications, analytics, sleep timers — without forking the SDK.
Writing a plugin
defmodule MyApp.LastFmScrobbler do
@behaviour Rockbox.Plugin
@impl true
def name, do: "lastfm-scrobbler"
@impl true
def version, do: "1.0.0"
@impl true
def description, do: "Scrobble played tracks to Last.fm"
@impl true
def install(ctx) do
{:ok, pid} = MyApp.LastFmScrobbler.Worker.start_link(ctx.client)
{:ok, %{worker: pid}}
end
@impl true
def uninstall(%{worker: pid}) do
if Process.alive?(pid), do: GenServer.stop(pid)
:ok
end
end
defmodule MyApp.LastFmScrobbler.Worker do
use GenServer
def start_link(client), do: GenServer.start_link(__MODULE__, client)
@impl true
def init(client) do
Rockbox.Events.subscribe(:track_changed)
{:ok, %{client: client, current: nil, started_at: 0}}
end
@impl true
def handle_info({:rockbox, :track_changed, track}, state) do
now = System.monotonic_time(:millisecond)
# Submit the previous track if it played for more than 30 s
if state.current && now - state.started_at > 30_000 do
submit_scrobble(state.current)
end
{:noreply, %{state | current: track, started_at: now}}
end
defp submit_scrobble(_track), do: :ok # talk to the Last.fm API here
endInstalling
client = Rockbox.new()
{:ok, _} = Rockbox.connect(client)
:ok = Rockbox.use_plugin(client, MyApp.LastFmScrobbler)
# Inspect what's installed
for entry <- Rockbox.installed_plugins() do
IO.puts("#{entry.module.name()} v#{entry.module.version()}")
end
:ok = Rockbox.unuse_plugin("lastfm-scrobbler") # by name
:ok = Rockbox.unuse_plugin(MyApp.LastFmScrobbler) # or by module
The install/1 callback receives %{client: client}. Return {:ok, state};
the state is passed back to uninstall/1 so resources can be cleaned up.
Error handling
case Rockbox.Playback.play(client) do
:ok ->
:ok
{:error, %Rockbox.NetworkError{} = e} ->
Logger.error("rockboxd unreachable: #{Exception.message(e)}")
{:error, %Rockbox.GraphQLError{errors: errors}} ->
for %{message: msg} <- errors, do: Logger.error("graphql: #{msg}")
{:error, %Rockbox.Error{} = e} ->
Logger.error("rockbox: #{Exception.message(e)}")
end
# …or use the bang variant inside a try/rescue
try do
Rockbox.Playback.play!(client)
rescue
e in Rockbox.NetworkError -> Logger.error("offline: #{e.message}")
e in Rockbox.GraphQLError -> Logger.error("server: #{e.message}")
end| Exception | When raised |
|---|---|
Rockbox.NetworkError | HTTP request fails or returns a non-2xx status |
Rockbox.GraphQLError |
Server returns { "errors": [...] } in the response body |
Rockbox.Error | Base exception — rescue this to catch any SDK failure |
Raw GraphQL queries
For operations not yet covered by a dedicated function, drop down to
Rockbox.query/3. Variables can be a map or keyword list — snake_case keys
are converted to camelCase before being sent.
{:ok, %{"rockboxVersion" => v}} =
Rockbox.query(client, "query { rockboxVersion }")
{:ok, %{"album" => album}} =
Rockbox.query(
client,
"query Album($id: String!) { album(id: $id) { id title artist year } }",
id: "abc-123"
)
# Mutation
:ok = Rockbox.query(client, "mutation Seek($t: Int!) { fastForwardRewind(newTime: $t) }", t: 120_000) |> elem(0) == :ok
The GraphiQL explorer is available at http://localhost:6062/graphiql while
rockboxd is running.
Module map
| Domain | Module |
|---|---|
| Client constructor | Rockbox, Rockbox.Client |
| Transport controls | Rockbox.Playback |
| Library / search | Rockbox.Library |
| Live queue | Rockbox.Queue |
| Saved playlists | Rockbox.SavedPlaylists |
| Smart playlists | Rockbox.SmartPlaylists |
| Smart-playlist rules | Rockbox.SmartPlaylist.Rules |
| Volume | Rockbox.Sound |
| Settings | Rockbox.Settings |
| System info | Rockbox.System |
| Filesystem browser | Rockbox.Browse |
| Output devices | Rockbox.Devices |
| Bluetooth | Rockbox.Bluetooth |
| Real-time events | Rockbox.Events |
| Plugin behaviour | Rockbox.Plugin, Rockbox.Plugins |
| Errors | Rockbox.Error, Rockbox.NetworkError, Rockbox.GraphQLError |
Development
mix deps.get
mix test
mix docs # generates HTML docs in doc/
Examples live in examples/. Start rockboxd, then:
mix run examples/01_basic_playback.exs
mix run --no-halt examples/02_now_playing.exsLicense
MIT License. See LICENSE for details.