···11+# Examples
22+33+Each example is a runnable Gleam module. Start `rockboxd` first, then from
44+this directory:
55+66+```sh
77+gleam deps download
88+gleam run -m example_01_basic_playback
99+```
1010+1111+The shared client points at `localhost:6062` — edit `src/helper.gleam` if
1212+your daemon runs elsewhere. Examples that take parameters (search term,
1313+browse path, sleep-timer minutes, …) declare a constant near the top of
1414+the file; tweak it and re-run.
1515+1616+| File | What it shows |
1717+|-----------------------------------|--------------------------------------------------------|
1818+| `example_01_basic_playback.gleam` | Toggle play/pause based on current status |
1919+| `example_02_now_playing.gleam` | Polling-based current-track watcher |
2020+| `example_03_library_search.gleam` | Full-text search across artists/albums/tracks |
2121+| `example_04_queue_management.gleam` | Inspect and modify the live queue |
2222+| `example_05_saved_playlists.gleam` | Persistent named playlists |
2323+| `example_06_smart_playlist.gleam` | Rule-based smart playlist via the `rules` builder DSL |
2424+| `example_07_volume_control.gleam` | Volume up/down |
2525+| `example_08_eq_config.gleam` | Equalizer configuration via the settings patch builder |
2626+| `example_09_browse_filesystem.gleam` | Walk `music_dir` |
2727+| `example_10_devices.gleam` | List Chromecast / AirPlay devices |
2828+| `example_11_bluetooth.gleam` | Bluetooth scan / connect (Linux) |
2929+| `example_12_sleep_timer.gleam` | Polling-based sleep timer |
3030+| `example_13_raw_query.gleam` | Escape hatch for one-off GraphQL queries |
3131+3232+## Differences from the Elixir SDK
3333+3434+The Gleam SDK is request/response-only and doesn't ship the higher-level
3535+abstractions the Elixir SDK has, so a few examples are adapted:
3636+3737+- **02 / now playing** — the Elixir version subscribes to a WebSocket. The
3838+ Gleam version polls `currentTrack` once per second.
3939+- **12 / sleep timer** — the Elixir version uses a `Rockbox.Plugin`
4040+ GenServer. The Gleam version is a simple `process.sleep` loop that bails
4141+ out early if playback was stopped manually.
···11+//// 12 — Sleep timer
22+////
33+//// Polling-based version of the Elixir plugin example: stops playback after
44+//// `minutes` minutes, but bails out early if playback was stopped manually.
55+////
66+//// The Gleam SDK has no plugin/event-bus yet, so this is a straight loop —
77+//// short-circuiting via early return when status flips to Stopped.
88+////
99+//// gleam run -m example_12_sleep_timer
1010+1111+import gleam/erlang/process
1212+import gleam/int
1313+import gleam/io
1414+import helper
1515+import rockbox.{type Client}
1616+import rockbox/playback
1717+import rockbox/types
1818+1919+const minutes: Int = 30
2020+2121+const tick_ms: Int = 1000
2222+2323+pub fn main() {
2424+ let client = helper.client()
2525+ let total_ticks = minutes * 60
2626+2727+ io.println(
2828+ "💤 Sleep timer armed — stopping playback in "
2929+ <> int.to_string(minutes)
3030+ <> " minute(s).",
3131+ )
3232+3333+ loop(client, total_ticks)
3434+}
3535+3636+fn loop(client: Client, ticks_remaining: Int) {
3737+ case ticks_remaining {
3838+ n if n <= 0 -> {
3939+ io.println("💤 Time's up — stopping playback.")
4040+ let _ = playback.stop(client)
4141+ Nil
4242+ }
4343+ _ ->
4444+ case playback.status(client) {
4545+ Ok(types.Stopped) -> {
4646+ io.println("💤 Playback stopped manually — sleep timer cancelled.")
4747+ Nil
4848+ }
4949+ _ -> {
5050+ process.sleep(tick_ms)
5151+ loop(client, ticks_remaining - 1)
5252+ }
5353+ }
5454+ }
5555+}
+69
sdk/gleam/examples/src/example_13_raw_query.gleam
···11+//// 13 — Raw GraphQL escape hatch
22+////
33+//// For operations the SDK doesn't expose directly, drop down to
44+//// `rockbox.query/4` and supply your own decoder.
55+////
66+//// gleam run -m example_13_raw_query
77+88+import gleam/dynamic/decode
99+import gleam/int
1010+import gleam/io
1111+import gleam/json
1212+import gleam/option.{Some}
1313+import helper
1414+import rockbox
1515+1616+pub fn main() {
1717+ let client = helper.client()
1818+1919+ let version_decoder = {
2020+ use v <- decode.field("rockboxVersion", decode.string)
2121+ decode.success(v)
2222+ }
2323+2424+ let assert Ok(version) =
2525+ rockbox.query(
2626+ client,
2727+ "query Version { rockboxVersion }",
2828+ json.object([]),
2929+ version_decoder,
3030+ )
3131+ io.println("rockboxd " <> version)
3232+3333+ let album_decoder = {
3434+ use album <- decode.field(
3535+ "album",
3636+ decode.optional({
3737+ use id <- decode.field("id", decode.string)
3838+ use title <- decode.optional_field("title", "", decode.string)
3939+ use artist <- decode.optional_field("artist", "", decode.string)
4040+ use year <- decode.optional_field("year", 0, decode.int)
4141+ decode.success(#(id, title, artist, year))
4242+ }),
4343+ )
4444+ decode.success(album)
4545+ }
4646+4747+ case
4848+ rockbox.query(
4949+ client,
5050+ "query Album($id: String!) { album(id: $id) { id title artist year } }",
5151+ json.object([#("id", json.string("demo-id-or-use-a-real-one"))]),
5252+ album_decoder,
5353+ )
5454+ {
5555+ Ok(Some(#(id, title, artist, year))) ->
5656+ io.println(
5757+ "album: id="
5858+ <> id
5959+ <> " title="
6060+ <> title
6161+ <> " artist="
6262+ <> artist
6363+ <> " year="
6464+ <> int.to_string(year),
6565+ )
6666+ Ok(_) -> io.println("album: (not found)")
6767+ Error(_) -> io.println("album: query failed")
6868+ }
6969+}
+28
sdk/gleam/examples/src/helper.gleam
···11+//// Shared helpers for the example modules.
22+////
33+//// Edit `client()` to point at a different host/port if your `rockboxd` is
44+//// not running on `localhost:6062`.
55+66+import gleam/int
77+import gleam/string
88+import rockbox.{type Client}
99+1010+pub fn client() -> Client {
1111+ rockbox.new()
1212+ |> rockbox.host("localhost")
1313+ |> rockbox.port(6062)
1414+ |> rockbox.connect
1515+}
1616+1717+/// Format a duration in milliseconds as `M:SS`.
1818+pub fn fmt_ms(ms: Int) -> String {
1919+ let total_secs = ms / 1000
2020+ let mins = total_secs / 60
2121+ let secs = total_secs % 60
2222+ int.to_string(mins) <> ":" <> string.pad_start(int.to_string(secs), 2, "0")
2323+}
2424+2525+/// Pad an integer to a left-aligned width-N string for tabular output.
2626+pub fn pad_int(value: Int, width: Int) -> String {
2727+ string.pad_start(int.to_string(value), width, " ")
2828+}
+257
sdk/gleam/src/rockbox/smart_playlists/rules.gleam
···11+//// Composable builder for smart-playlist rule sets. Pipe-friendly.
22+////
33+//// ```gleam
44+//// import rockbox/smart_playlists
55+//// import rockbox/smart_playlists/rules
66+////
77+//// let r =
88+//// rules.all_of()
99+//// |> rules.where("play_count", rules.Gte, rules.int(10))
1010+//// |> rules.where("last_played", rules.Within, rules.string("30d"))
1111+//// |> rules.sort("play_count", rules.Desc)
1212+//// |> rules.limit(50)
1313+////
1414+//// let input = smart_playlists.new("Most played", rules.to_string(r))
1515+//// let assert Ok(_) = smart_playlists.create(client, input)
1616+//// ```
1717+////
1818+//// `any_of()` swaps the top-level operator from `AND` to `OR`. Mix the two by
1919+//// nesting builders with `where_group`:
2020+////
2121+//// ```gleam
2222+//// let r =
2323+//// rules.all_of()
2424+//// |> rules.where("genre", rules.Eq, rules.string("Rock"))
2525+//// |> rules.where_group(
2626+//// rules.any_of()
2727+//// |> rules.where("year", rules.Gte, rules.int(2000))
2828+//// |> rules.where("year", rules.Lte, rules.int(2010)),
2929+//// )
3030+//// ```
3131+3232+import gleam/json.{type Json}
3333+import gleam/list
3434+import gleam/option.{type Option, None, Some}
3535+3636+// ---------------------------------------------------------------------------
3737+// Types
3838+// ---------------------------------------------------------------------------
3939+4040+/// A composable rule set. Build with `all_of` / `any_of` and chain
4141+/// `where` / `sort` / `limit`. Hand to `to_string` to get the JSON payload
4242+/// the GraphQL `createSmartPlaylist` mutation expects.
4343+pub opaque type Rules {
4444+ Rules(
4545+ operator: GroupOp,
4646+ rules: List(Node),
4747+ sort: Option(Sort),
4848+ limit: Option(Int),
4949+ )
5050+}
5151+5252+/// How sibling rules combine.
5353+pub type GroupOp {
5454+ And
5555+ Or
5656+}
5757+5858+/// Comparison operators understood by the server.
5959+///
6060+/// | Variant | Meaning |
6161+/// |-------------|----------------------------------------|
6262+/// | `Eq` | equals |
6363+/// | `Neq` | not equals |
6464+/// | `Gt` | greater than |
6565+/// | `Gte` | greater than or equal |
6666+/// | `Lt` | less than |
6767+/// | `Lte` | less than or equal |
6868+/// | `Contains` | substring match |
6969+/// | `Within` | duration window (e.g. `"30d"`, `"7d"`) |
7070+pub type Op {
7171+ Eq
7272+ Neq
7373+ Gt
7474+ Gte
7575+ Lt
7676+ Lte
7777+ Contains
7878+ Within
7979+}
8080+8181+/// Sort direction.
8282+pub type SortDir {
8383+ Asc
8484+ Desc
8585+}
8686+8787+/// Wrapped value handed to `where`. Build with `int` / `string` / `bool` /
8888+/// `float` / `null`, or `raw` if you have a `Json` already.
8989+pub opaque type Value {
9090+ Value(Json)
9191+}
9292+9393+type Sort {
9494+ Sort(field: String, dir: SortDir)
9595+}
9696+9797+type Node {
9898+ Leaf(field: String, op: Op, value: Json)
9999+ Group(Rules)
100100+}
101101+102102+// ---------------------------------------------------------------------------
103103+// Value constructors
104104+// ---------------------------------------------------------------------------
105105+106106+/// Wrap an integer (e.g. play counts, years, durations in ms).
107107+pub fn int(value: Int) -> Value {
108108+ Value(json.int(value))
109109+}
110110+111111+/// Wrap a string (e.g. titles, artists, genres, duration windows).
112112+pub fn string(value: String) -> Value {
113113+ Value(json.string(value))
114114+}
115115+116116+/// Wrap a boolean.
117117+pub fn bool(value: Bool) -> Value {
118118+ Value(json.bool(value))
119119+}
120120+121121+/// Wrap a float.
122122+pub fn float(value: Float) -> Value {
123123+ Value(json.float(value))
124124+}
125125+126126+/// JSON `null`.
127127+pub fn null() -> Value {
128128+ Value(json.null())
129129+}
130130+131131+/// Escape hatch for arbitrary JSON values (arrays, nested objects, etc.).
132132+pub fn raw(value: Json) -> Value {
133133+ Value(value)
134134+}
135135+136136+// ---------------------------------------------------------------------------
137137+// Builders
138138+// ---------------------------------------------------------------------------
139139+140140+/// Start a builder where every rule must match (logical AND).
141141+pub fn all_of() -> Rules {
142142+ Rules(operator: And, rules: [], sort: None, limit: None)
143143+}
144144+145145+/// Start a builder where any single rule must match (logical OR).
146146+pub fn any_of() -> Rules {
147147+ Rules(operator: Or, rules: [], sort: None, limit: None)
148148+}
149149+150150+/// Append a rule to the current group.
151151+pub fn where(
152152+ builder: Rules,
153153+ field: String,
154154+ op: Op,
155155+ value: Value,
156156+) -> Rules {
157157+ let Value(json_value) = value
158158+ let leaf = Leaf(field: field, op: op, value: json_value)
159159+ Rules(..builder, rules: list.append(builder.rules, [leaf]))
160160+}
161161+162162+/// Nest another builder underneath this one. Useful for mixing AND / OR.
163163+pub fn where_group(parent: Rules, child: Rules) -> Rules {
164164+ Rules(..parent, rules: list.append(parent.rules, [Group(child)]))
165165+}
166166+167167+/// Set the result ordering.
168168+pub fn sort(builder: Rules, field: String, direction: SortDir) -> Rules {
169169+ Rules(..builder, sort: Some(Sort(field: field, dir: direction)))
170170+}
171171+172172+/// Cap the result count.
173173+pub fn limit(builder: Rules, count: Int) -> Rules {
174174+ Rules(..builder, limit: Some(count))
175175+}
176176+177177+// ---------------------------------------------------------------------------
178178+// Rendering
179179+// ---------------------------------------------------------------------------
180180+181181+/// Encode the builder as a `Json` value — useful if you're already working
182182+/// with JSON values.
183183+pub fn to_json(builder: Rules) -> Json {
184184+ let base = [
185185+ #("operator", json.string(group_op_to_string(builder.operator))),
186186+ #("rules", json.array(builder.rules, node_to_json)),
187187+ ]
188188+189189+ let with_sort = case builder.sort {
190190+ Some(Sort(field, dir)) ->
191191+ list.append(base, [
192192+ #(
193193+ "sort",
194194+ json.object([
195195+ #("field", json.string(field)),
196196+ #("dir", json.string(sort_dir_to_string(dir))),
197197+ ]),
198198+ ),
199199+ ])
200200+ None -> base
201201+ }
202202+203203+ let with_limit = case builder.limit {
204204+ Some(n) -> list.append(with_sort, [#("limit", json.int(n))])
205205+ None -> with_sort
206206+ }
207207+208208+ json.object(with_limit)
209209+}
210210+211211+/// Encode the builder as a JSON string ready for the smart-playlist mutation.
212212+pub fn to_string(builder: Rules) -> String {
213213+ json.to_string(to_json(builder))
214214+}
215215+216216+// ---------------------------------------------------------------------------
217217+// Internal encoding
218218+// ---------------------------------------------------------------------------
219219+220220+fn node_to_json(node: Node) -> Json {
221221+ case node {
222222+ Leaf(field, op, value) ->
223223+ json.object([
224224+ #("field", json.string(field)),
225225+ #("op", json.string(op_to_string(op))),
226226+ #("value", value),
227227+ ])
228228+ Group(child) -> to_json(child)
229229+ }
230230+}
231231+232232+fn group_op_to_string(op: GroupOp) -> String {
233233+ case op {
234234+ And -> "AND"
235235+ Or -> "OR"
236236+ }
237237+}
238238+239239+fn op_to_string(op: Op) -> String {
240240+ case op {
241241+ Eq -> "eq"
242242+ Neq -> "neq"
243243+ Gt -> "gt"
244244+ Gte -> "gte"
245245+ Lt -> "lt"
246246+ Lte -> "lte"
247247+ Contains -> "contains"
248248+ Within -> "within"
249249+ }
250250+}
251251+252252+fn sort_dir_to_string(dir: SortDir) -> String {
253253+ case dir {
254254+ Asc -> "asc"
255255+ Desc -> "desc"
256256+ }
257257+}