rockbox

Package Version Hex Docs

Gleam SDK for Rockbox Zig — a typed, pipe-friendly client for the rockboxd GraphQL API.


Table of contents


Installation

gleam add rockbox

rockboxd must be running and reachable. By default the SDK connects to http://localhost:6062/graphql. Start rockboxd with:

rockbox start

Quick start

import gleam/io
import gleam/list
import gleam/option.{None, Some}
import rockbox
import rockbox/library
import rockbox/playback

pub fn main() {
  let client = rockbox.default_client()

  // What's playing right now?
  case playback.current_track(client) {
    Ok(Some(track)) -> io.println("▶ " <> track.title <> " — " <> track.artist)
    Ok(None) -> io.println("Nothing is playing.")
    Error(_) -> io.println("Could not reach rockboxd.")
  }

  // Search the library and play the first hit
  let assert Ok(results) = library.search(client, "dark side")
  case list.first(results.albums) {
    Ok(album) -> {
      let _ =
        playback.play_album(
          client,
          album.id,
          playback.play_options() |> playback.with_shuffle(True),
        )
      Nil
    }
    Error(_) -> Nil
  }
}

Configuration

The Builder pattern lets you override defaults one field at a time:

// Defaults: localhost:6062
let client = rockbox.default_client()

// Custom host and port
let client = rockbox.at(host: "192.168.1.42", port: 6062)

// Fully custom URL (e.g. behind a reverse proxy with TLS)
let client =
  rockbox.new()
  |> rockbox.url("http://192.168.1.42:6062/graphql")
  |> rockbox.connect
SetterDefaultDescription
host(_, value)"localhost"Hostname or IP of rockboxd
port(_, value)6062GraphQL HTTP port
url(_, value)derived from host/portOverride the full HTTP URL (wins over host / port)

Use rockbox.http_url(client) to read back the resolved URL — handy for tests and diagnostics.


API reference

Every function returns Result(value, rockbox/error.Error). Pattern-match or use let assert Ok(x) = … in scripts where a failure should crash.

Playback

import rockbox/playback
import rockbox/types

// Status — typed: Stopped | Playing | Paused | UnknownStatus(Int)
let assert Ok(status) = playback.status(client)

// Toggle
case status {
  types.Playing -> { let _ = playback.pause(client) }
  _ -> { let _ = playback.resume(client) }
}

// Transport
let _ = playback.next(client)
let _ = playback.previous(client)
let _ = playback.stop(client)

// Seek to absolute position (ms)
let _ = playback.seek(client, 90_000)

// Current / next track — Ok(Some(track)) when present, Ok(None) when stopped
let assert Ok(now) = playback.current_track(client)
let assert Ok(next) = playback.next_track(client)

Play helpers

PlayOptions is a small builder for the optional shuffle / position knobs accepted by every play_* shortcut:

let opts =
  playback.play_options()
  |> playback.with_shuffle(True)
  |> playback.with_position(2)

let _ = playback.play_track(client, "/Music/foo.mp3")
let _ = playback.play_album(client, "album-id", opts)
let _ = playback.play_artist(client, "artist-id", opts)
let _ = playback.play_playlist(client, "playlist-id", opts)
let _ = playback.play_directory(client, "/Music/Jazz", True, opts)
let _ = playback.play_liked_tracks(client, opts)
let _ = playback.play_all_tracks(client, opts)

Library

import rockbox/library

// Albums
let assert Ok(albums) = library.albums(client)
let assert Ok(album) = library.album(client, "album-id")    // includes tracks
let assert Ok(liked) = library.liked_albums(client)
let _ = library.like_album(client, "album-id")
let _ = library.unlike_album(client, "album-id")

// Artists
let assert Ok(artists) = library.artists(client)
let assert Ok(artist) = library.artist(client, "artist-id")

// Tracks
let assert Ok(tracks) = library.tracks(client)
let assert Ok(track) = library.track(client, "track-id")
let assert Ok(liked) = library.liked_tracks(client)
let _ = library.like_track(client, "track-id")
let _ = library.unlike_track(client, "track-id")

// Search across artists, albums, tracks, liked
let assert Ok(results) = library.search(client, "radiohead")
results.artists       // List(Artist)
results.albums        // List(Album)
results.tracks        // List(Track)
results.liked_tracks
results.liked_albums

// Trigger a full library rescan
let _ = library.scan(client)

Queue (live playlist)

The live queue lives in rockbox/playlist. For persistent named collections see Saved playlists.

import gleam/option.{None}
import rockbox/playlist
import rockbox/types

let assert Ok(queue) = playlist.current(client)
queue.amount       // total tracks
queue.index        // 0-based position of the currently playing track
queue.tracks       // List(Track)

// Insertion: position is types.Next | types.AfterCurrent | types.Last | types.First
let _ =
  playlist.insert_tracks(
    client,
    ["/Music/a.mp3", "/Music/b.mp3"],
    types.Next,
    None,
  )
let _ =
  playlist.insert_directory(client, "/Music/Ambient", types.Last, None)
let _ = playlist.insert_album(client, "album-id", types.Next)

// Other ops
let _ = playlist.remove_track(client, 2)
let _ = playlist.clear(client)
let _ = playlist.shuffle(client)
let _ = playlist.create(client, "Evening Mix", ["/a.mp3", "/b.mp3"])
let _ = playlist.resume(client)

// Start from a specific position
let opts =
  playlist.start_options()
  |> playlist.at_index(3)
  |> playlist.at_elapsed(0)
let _ = playlist.start(client, opts)

Saved playlists

import gleam/option.{None, Some}
import rockbox/saved_playlists

let assert Ok(lists) = saved_playlists.list(client, None)
let assert Ok(scoped) = saved_playlists.list(client, Some("folder-id"))

let assert Ok(pl) = saved_playlists.get(client, "playlist-id")
let assert Ok(ids) = saved_playlists.track_ids(client, "playlist-id")

// Create
let input =
  saved_playlists.new("Late Night Jazz")
  |> saved_playlists.with_description("Quiet music for working")
  |> saved_playlists.with_folder("folder-id")
  |> saved_playlists.with_tracks(["t1", "t2", "t3"])

let assert Ok(pl) = saved_playlists.create(client, input)

// Update / add / remove
let patch =
  saved_playlists.update("Late Night Jazz (v2)")
  |> saved_playlists.update_description("Updated cover")

let _ = saved_playlists.save(client, pl.id, patch)
let _ = saved_playlists.add_tracks(client, pl.id, ["t4", "t5"])
let _ = saved_playlists.remove_track(client, pl.id, "t1")

// Play / delete
let _ = saved_playlists.play(client, pl.id)
let _ = saved_playlists.delete(client, pl.id)

// Folders
let assert Ok(folders) = saved_playlists.folders(client)
let assert Ok(folder) = saved_playlists.create_folder(client, "Work")
let _ = saved_playlists.delete_folder(client, folder.id)

Smart playlists

Compose rules with the type-safe rockbox/smart_playlists/rules builder instead of hand-writing JSON.

import rockbox/smart_playlists
import rockbox/smart_playlists/rules

let r =
  rules.all_of()
  |> rules.where("play_count", rules.Gte, rules.int(10))
  |> rules.where("last_played", rules.Within, rules.string("30d"))
  |> rules.sort("play_count", rules.Desc)
  |> rules.limit(50)

let input =
  smart_playlists.new("Most played (last 30d)", rules.to_string(r))
  |> smart_playlists.with_description("Top 50 most-played tracks from the last month")

let assert Ok(sp) = smart_playlists.create(client, input)

let assert Ok(ids) = smart_playlists.track_ids(client, sp.id)
let _ = smart_playlists.play(client, sp.id)
let _ = smart_playlists.delete(client, sp.id)

Operators

VariantMeaning
Eqequals
Neqnot equals
Gtgreater than
Gtegreater than or equal
Ltless than
Lteless than or equal
Containssubstring match
Withinduration window (e.g. "30d", "7d")

OR groups and nesting

let either =
  rules.any_of()
  |> rules.where("title", rules.Contains, rules.string("Live"))
  |> rules.where("title", rules.Contains, rules.string("Acoustic"))

let mixed =
  rules.all_of()
  |> rules.where("play_count", rules.Gt, rules.int(0))
  |> rules.where_group(either)

Listening stats

let assert Ok(stats) = smart_playlists.track_stats(client, "track-id")

// Record events manually (e.g. from a scrobbler)
let _ = smart_playlists.record_played(client, "track-id")
let _ = smart_playlists.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 get_volume/1 for the range.

import rockbox/sound

let assert Ok(vol) = sound.get_volume(client)
vol.volume      // current value
vol.min         // lower bound
vol.max         // upper bound

let assert Ok(new_value) = sound.adjust_volume(client, 3)   // +3 steps
let assert Ok(_) = sound.volume_up(client)                  // +1
let assert Ok(_) = sound.volume_down(client)                // -1

Settings

save/2 accepts any subset of fields — only the ones you set are written.

import rockbox/settings
import rockbox/types.{
  CompressorSettings, EqBandSetting, ReplaygainSettings,
}

let assert Ok(current) = settings.get(client)

// Toggle shuffle + repeat
let patch =
  settings.patch()
  |> settings.set_shuffle(True)
  |> settings.set_repeat_mode(1)
let _ = settings.save(client, patch)

// Equalizer
let bands = [
  EqBandSetting(cutoff: 60, q: 7, gain: 3),
  EqBandSetting(cutoff: 200, q: 7, gain: 0),
  EqBandSetting(cutoff: 4000, q: 7, gain: -2),
]
let patch =
  settings.patch()
  |> settings.set_eq_enabled(True)
  |> settings.set_eq_precut(-3)
  |> settings.set_eq_bands(bands)
let _ = settings.save(client, patch)

// Compressor
let patch =
  settings.patch()
  |> settings.set_compressor(CompressorSettings(
    threshold: -24,
    makeup_gain: 3,
    ratio: 2,
    knee: 0,
    release_time: 100,
    attack_time: 5,
  ))
let _ = settings.save(client, patch)

// Replaygain
let patch =
  settings.patch()
  |> settings.set_replaygain(ReplaygainSettings(
    noclip: True, type_: 1, preamp: 0,
  ))
let _ = settings.save(client, patch)

System

import rockbox/system

let assert Ok(version) = system.version(client)
let assert Ok(status) = system.status(client)

status.runtime          // seconds since boot
status.topruntime       // peak runtime
status.resume_index     // last queued position

Browse (filesystem)

import gleam/option.{None, Some}
import rockbox/browse
import rockbox/types

let assert Ok(entries) = browse.entries(client, None)                   // music_dir root
let assert Ok(entries) = browse.entries(client, Some("/Music/Pink Floyd"))

list.each(entries, fn(e) {
  let icon = case types.is_directory(e) {
    True -> "[dir] "
    False -> "      "
  }
  io.println(icon <> e.name)
})

let assert Ok(dirs) = browse.directories(client, Some("/Music"))
let assert Ok(files) = browse.files(client, Some("/Music/Pink Floyd/The Wall"))

Devices

import rockbox/devices

let assert Ok(devices) = devices.list(client)
let assert Ok(device) = devices.get(client, "device-id")

// Connect — switches the active PCM output sink to this device
let _ = devices.connect(client, "chromecast-id")
let _ = devices.disconnect(client, "chromecast-id")

Bluetooth

Linux only — backed by BlueZ. Calls return a GraphQLError on non-Linux hosts.

import gleam/option.{None, Some}
import rockbox/bluetooth

let assert Ok(devices) = bluetooth.devices(client)
let assert Ok(found) = bluetooth.scan(client, Some(10))   // 10 second scan
let _ = bluetooth.connect(client, "AA:BB:CC:DD:EE:FF")
let _ = bluetooth.disconnect(client, "AA:BB:CC:DD:EE:FF")

Error handling

import rockbox/error
import rockbox/playback

case playback.current_track(client) {
  Ok(track) -> echo track
  Error(error.NetworkError(reason)) -> io.println("offline: " <> reason)
  Error(error.HttpError(status, _)) -> io.println("http " <> int.to_string(status))
  Error(error.GraphQLError(messages)) ->
    list.each(messages, fn(m) { io.println("server: " <> m) })
  Error(error.DecodeError(reason)) -> io.println("decode: " <> reason)
}
VariantWhen raised
NetworkErrorDNS, refused connection, TLS, etc.
HttpErrorServer returned a non-2xx HTTP response.
GraphQLErrorServer returned a populated errors array.
DecodeErrorResponse body could not be decoded into the expected shape.

Raw GraphQL queries

For operations not yet covered by a dedicated function, drop down to rockbox.query/4 (or rockbox.execute/3 for fire-and-forget mutations) and supply your own decoder.

import gleam/dynamic/decode
import gleam/json

let version_decoder = {
  use v <- decode.field("rockboxVersion", decode.string)
  decode.success(v)
}

let assert Ok(version) =
  rockbox.query(
    client,
    "query Version { rockboxVersion }",
    json.object([]),
    version_decoder,
  )

// Mutation — use execute when you don't care about the response body
let _ =
  rockbox.execute(
    client,
    "mutation Seek($t: Int!) { fastForwardRewind(newTime: $t) }",
    json.object([#("t", json.int(120_000))]),
  )

The GraphiQL explorer is available at http://localhost:6062/graphiql while rockboxd is running.


Module map

DomainModule
Client constructorrockbox
Transport controlsrockbox/playback
Library / searchrockbox/library
Live queuerockbox/playlist
Saved playlistsrockbox/saved_playlists
Smart playlistsrockbox/smart_playlists
Smart-playlist rulesrockbox/smart_playlists/rules
Volumerockbox/sound
Settingsrockbox/settings
System inforockbox/system
Filesystem browserrockbox/browse
Output devicesrockbox/devices
Bluetoothrockbox/bluetooth
Domain typesrockbox/types
Errorsrockbox/error

Development

gleam test    # run the test suite
gleam docs build

Runnable examples live in examples/. Start rockboxd, then:

cd examples
gleam run -m example_01_basic_playback
gleam run -m example_06_smart_playlist

See examples/README.md for the full list.

Further documentation is on HexDocs.


License

MIT License. See LICENSE for details.

Search Document