rockbox 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 13e4b57d472b2417316bad50917e45c1976a80db4f42a556172f1dd599871e88
4
+ data.tar.gz: dde0b0010b7ae247cdcff0d118899586c33b95e4fee949ee4231135561149432
5
+ SHA512:
6
+ metadata.gz: f3b1a4dd4254a20ff12c3aaa119a4955d5ddbca8872f130b351787673f5b558d355db951e3e5167e0f1acc09aa2d5e410bfe926f94953e89d2ef3db9c527bca8
7
+ data.tar.gz: 8869e601d30924d78273673dbfdfc97ca6d1a3d059e42e293bb7307090f1a13d58fa887c1cb26e83f21bb16c2dd79426d5d74b8c77dcd9c372dd145984c159b6
data/README.md ADDED
@@ -0,0 +1,473 @@
1
+ # rockbox
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/rockbox?style=flat-square&logo=rubygems&logoColor=white&color=E9573F)](https://rubygems.org/gems/rockbox)
4
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.0-CC342D?style=flat-square&logo=ruby&logoColor=white)](https://www.ruby-lang.org/)
5
+ [![Downloads](https://img.shields.io/gem/dt/rockbox?style=flat-square&logo=rubygems&logoColor=white&color=blue)](https://rubygems.org/gems/rockbox)
6
+ [![License](https://img.shields.io/badge/license-MIT-green?style=flat-square)](./LICENSE)
7
+ [![GraphQL](https://img.shields.io/badge/GraphQL-client-E10098?style=flat-square&logo=graphql&logoColor=white)](https://graphql.org/)
8
+ [![WebSocket](https://img.shields.io/badge/WebSocket-realtime-4353FF?style=flat-square&logo=socketdotio&logoColor=white)](https://github.com/enhancv/websocket-client-simple)
9
+ [![Plugins](https://img.shields.io/badge/plugins-extensible-8A2BE2?style=flat-square&logo=rubygems&logoColor=white)](#plugin-system)
10
+ [![GitHub](https://img.shields.io/badge/github-rockbox--zig-181717?style=flat-square&logo=github&logoColor=white)](https://github.com/tsirysndr/rockbox-zig)
11
+
12
+ Ruby SDK for [Rockbox Zig](https://github.com/tsirysndr/rockbox-zig) — a builder-friendly, block-friendly GraphQL client with real-time event subscriptions and a plugin system.
13
+
14
+ ```ruby
15
+ require "rockbox"
16
+
17
+ client = Rockbox::Client.build do |c|
18
+ c.host = "localhost"
19
+ c.port = 6062
20
+ end
21
+
22
+ client.on(:track_changed) { |t| puts "▶ #{t.title} — #{t.artist}" }
23
+ client.connect
24
+
25
+ results = client.library.search("dark side")
26
+ client.playback.play_album(results.albums.first.id, shuffle: true)
27
+ ```
28
+
29
+ ---
30
+
31
+ ## Table of contents
32
+
33
+ - [Installation](#installation)
34
+ - [Quick start](#quick-start)
35
+ - [Configuration](#configuration)
36
+ - [API reference](#api-reference)
37
+ - [Playback](#playback)
38
+ - [Library](#library)
39
+ - [Playlist (queue)](#playlist-queue)
40
+ - [Saved playlists](#saved-playlists)
41
+ - [Smart playlists](#smart-playlists)
42
+ - [Sound](#sound)
43
+ - [Settings](#settings)
44
+ - [System](#system)
45
+ - [Browse (filesystem)](#browse-filesystem)
46
+ - [Devices](#devices)
47
+ - [Bluetooth](#bluetooth)
48
+ - [Real-time events](#real-time-events)
49
+ - [Plugin system](#plugin-system)
50
+ - [Error handling](#error-handling)
51
+ - [Raw GraphQL queries](#raw-graphql-queries)
52
+
53
+ ---
54
+
55
+ ## Installation
56
+
57
+ ```sh
58
+ gem install rockbox
59
+ ```
60
+
61
+ Or with Bundler:
62
+
63
+ ```ruby
64
+ # Gemfile
65
+ gem "rockbox"
66
+ ```
67
+
68
+ `rockboxd` must be running and reachable. The SDK targets **Ruby 3.0+** and connects to `http://localhost:6062/graphql` by default.
69
+
70
+ ---
71
+
72
+ ## Quick start
73
+
74
+ ```ruby
75
+ require "rockbox"
76
+
77
+ client = Rockbox::Client.new
78
+
79
+ # Optional — start WebSocket subscriptions for real-time events.
80
+ client.connect
81
+
82
+ # What's playing?
83
+ if (track = client.playback.current_track)
84
+ puts "Now playing: #{track.title} — #{track.artist}"
85
+ end
86
+
87
+ # Search the library.
88
+ results = client.library.search("dark side")
89
+ puts "Found #{results.albums.size} albums and #{results.tracks.size} tracks"
90
+
91
+ # Play an album with shuffle.
92
+ client.playback.play_album(results.albums.first.id, shuffle: true)
93
+
94
+ # React to track changes.
95
+ client.on(:track_changed) do |track|
96
+ puts "▶ #{track.title} by #{track.artist}"
97
+ end
98
+
99
+ # Tear down when done.
100
+ client.disconnect
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Configuration
106
+
107
+ Three equivalent ways to configure the client. Pick the one that fits your style.
108
+
109
+ ```ruby
110
+ # 1. Defaults — localhost:6062.
111
+ client = Rockbox::Client.new
112
+
113
+ # 2. Keyword arguments.
114
+ client = Rockbox::Client.new(host: "192.168.1.42", port: 6062)
115
+
116
+ # 3. Builder block (great for application initializers).
117
+ client = Rockbox::Client.build do |c|
118
+ c.host = "192.168.1.42"
119
+ c.port = 6062
120
+ end
121
+
122
+ # 4. Fully custom URLs (useful behind a reverse proxy).
123
+ client = Rockbox::Client.new(
124
+ http_url: "https://music.home/graphql",
125
+ ws_url: "wss://music.home/graphql"
126
+ )
127
+
128
+ # Top-level shorthand.
129
+ client = Rockbox.new(host: "localhost")
130
+ ```
131
+
132
+ | Option | Type | Default | Description |
133
+ |----------------|----------|---------------------------------|--------------------------------------|
134
+ | `host` | String | `"localhost"` | Hostname or IP of rockboxd |
135
+ | `port` | Integer | `6062` | GraphQL port |
136
+ | `http_url` | String | `http://{host}:{port}/graphql` | Override the full HTTP URL |
137
+ | `ws_url` | String | `ws://{host}:{port}/graphql` | Override the full WebSocket URL |
138
+ | `open_timeout` | Integer | `5` | HTTP connect timeout (seconds) |
139
+ | `read_timeout` | Integer | `30` | HTTP read timeout (seconds) |
140
+
141
+ ---
142
+
143
+ ## API reference
144
+
145
+ The client exposes a domain namespace per concern (Mopidy-style). Every method returns idiomatic Ruby data — `Struct` instances with `snake_case` accessors.
146
+
147
+ ### Playback
148
+
149
+ ```ruby
150
+ client.playback
151
+ ```
152
+
153
+ ```ruby
154
+ # Status
155
+ client.playback.status # => Integer (Rockbox::PlaybackStatus::PLAYING, etc.)
156
+ client.playback.status_name # => :playing | :paused | :stopped | :unknown
157
+
158
+ # Current/next track
159
+ track = client.playback.current_track # => Rockbox::Track
160
+ client.playback.next_track
161
+ client.playback.file_position
162
+
163
+ # Transport controls
164
+ client.playback.play(elapsed: 0, offset: 0)
165
+ client.playback.pause
166
+ client.playback.resume
167
+ client.playback.next!
168
+ client.playback.previous!
169
+ client.playback.seek(60_000) # ms
170
+ client.playback.stop
171
+ client.playback.flush_and_reload
172
+
173
+ # One-shot play helpers
174
+ client.playback.play_track("/Music/song.mp3")
175
+ client.playback.play_album(album_id, shuffle: true)
176
+ client.playback.play_artist(artist_id)
177
+ client.playback.play_playlist(playlist_id, shuffle: true, position: 0)
178
+ client.playback.play_directory("/Music/Pink Floyd", recurse: true)
179
+ client.playback.play_liked_tracks(shuffle: true)
180
+ client.playback.play_all_tracks
181
+ ```
182
+
183
+ ### Library
184
+
185
+ ```ruby
186
+ # Albums
187
+ client.library.albums # => Array<Rockbox::Album>
188
+ client.library.album(id) # => Rockbox::Album | nil
189
+ client.library.liked_albums
190
+ client.library.like_album(id)
191
+ client.library.unlike_album(id)
192
+
193
+ # Artists
194
+ client.library.artists
195
+ client.library.artist(id)
196
+
197
+ # Tracks
198
+ client.library.tracks
199
+ client.library.track(id)
200
+ client.library.liked_tracks
201
+ client.library.like_track(id)
202
+ client.library.unlike_track(id)
203
+
204
+ # Search
205
+ results = client.library.search("daft punk")
206
+ results.artists # => Array<Rockbox::Artist>
207
+ results.albums # => Array<Rockbox::Album>
208
+ results.tracks # => Array<Rockbox::Track>
209
+ results.liked_tracks
210
+ results.liked_albums
211
+
212
+ # Library scan
213
+ client.library.scan
214
+ ```
215
+
216
+ ### Playlist (queue)
217
+
218
+ ```ruby
219
+ playlist = client.playlist.current
220
+ playlist.amount # 42
221
+ playlist.index # currently playing index
222
+ playlist.tracks # Array<Rockbox::Track>
223
+
224
+ client.playlist.amount # convenience for playlist.current.amount
225
+
226
+ # Inserts (paths or track IDs)
227
+ client.playlist.insert_tracks(["/Music/a.mp3", "/Music/b.mp3"],
228
+ position: Rockbox::InsertPosition::NEXT)
229
+ client.playlist.insert_directory("/Music/Pink Floyd", position: Rockbox::InsertPosition::LAST)
230
+ client.playlist.insert_album(album_id)
231
+
232
+ # Mutations
233
+ client.playlist.remove_track(3)
234
+ client.playlist.clear
235
+ client.playlist.shuffle
236
+
237
+ # Create + start a temporary playlist
238
+ client.playlist.create("Tonight", ["/Music/a.mp3", "/Music/b.mp3"])
239
+ client.playlist.start(start_index: 0)
240
+ client.playlist.resume
241
+ ```
242
+
243
+ `Rockbox::InsertPosition` constants: `NEXT`, `AFTER_CURRENT`, `LAST`, `FIRST`.
244
+
245
+ ### Saved playlists
246
+
247
+ ```ruby
248
+ client.saved_playlists.list # => Array<Rockbox::SavedPlaylist>
249
+ client.saved_playlists.list(folder_id: "f_1")
250
+ client.saved_playlists.get("pl_42")
251
+ client.saved_playlists.track_ids("pl_42")
252
+
253
+ # Builder block (any field is optional)
254
+ playlist = client.saved_playlists.create(name: "Late nights") do |p|
255
+ p.description = "After-dark vibes"
256
+ p.image = "https://…/cover.png"
257
+ p.track_ids = ["abc", "def"]
258
+ end
259
+
260
+ # Or pass kwargs directly
261
+ client.saved_playlists.update("pl_42", name: "Renamed", description: "…")
262
+ client.saved_playlists.add_tracks("pl_42", ["abc", "def"])
263
+ client.saved_playlists.remove_track("pl_42", "abc")
264
+ client.saved_playlists.delete("pl_42")
265
+ client.saved_playlists.play("pl_42")
266
+
267
+ # Folders
268
+ client.saved_playlists.folders
269
+ client.saved_playlists.create_folder("Workout")
270
+ client.saved_playlists.delete_folder("f_1")
271
+ ```
272
+
273
+ ### Smart playlists
274
+
275
+ ```ruby
276
+ client.smart_playlists.list
277
+ client.smart_playlists.get(id)
278
+ client.smart_playlists.track_ids(id)
279
+ client.smart_playlists.create(name: "Heavy hitters", rules: rules_json)
280
+ client.smart_playlists.update(id, name: "…", rules: new_rules)
281
+ client.smart_playlists.delete(id)
282
+ client.smart_playlists.play(id)
283
+
284
+ # Listening stats
285
+ client.smart_playlists.track_stats(track_id)
286
+ client.smart_playlists.record_played(track_id)
287
+ client.smart_playlists.record_skipped(track_id)
288
+ ```
289
+
290
+ ### Sound
291
+
292
+ ```ruby
293
+ info = client.sound.volume # => Rockbox::VolumeInfo(volume:, min:, max:)
294
+ client.sound.adjust(3) # +3 steps
295
+ client.sound.up # +1 step
296
+ client.sound.down # -1 step
297
+ ```
298
+
299
+ ### Settings
300
+
301
+ ```ruby
302
+ settings = client.settings.get # => Rockbox::UserSettings (Struct)
303
+ settings.volume # -20
304
+ settings.shuffle # false
305
+
306
+ # Save with a builder block — only the fields you set are sent.
307
+ client.settings.save do |s|
308
+ s.volume = -10
309
+ s.bass = 4
310
+ s.shuffle = true
311
+ end
312
+
313
+ # Or with a plain Hash.
314
+ client.settings.save(volume: -10, shuffle: true)
315
+ ```
316
+
317
+ ### System
318
+
319
+ ```ruby
320
+ client.system.version # "Rockbox v…"
321
+ client.system.status # Rockbox::SystemStatus
322
+ ```
323
+
324
+ ### Browse (filesystem)
325
+
326
+ ```ruby
327
+ client.browse.entries("/Music") # => Array<Rockbox::Entry>
328
+ client.browse.directories("/Music") # only directories
329
+ client.browse.files("/Music/Pink Floyd") # only files
330
+
331
+ entry = client.browse.entries.first
332
+ Rockbox.directory?(entry) # => true/false
333
+ ```
334
+
335
+ ### Devices
336
+
337
+ ```ruby
338
+ client.devices.list # => Array<Rockbox::Device>
339
+ client.devices.get(id)
340
+ client.devices.connect(id)
341
+ client.devices.disconnect(id)
342
+ ```
343
+
344
+ ### Bluetooth
345
+
346
+ > Linux only. macOS/Windows builds will return errors.
347
+
348
+ ```ruby
349
+ client.bluetooth.devices
350
+ client.bluetooth.scan(timeout: 5)
351
+ client.bluetooth.connect("AA:BB:CC:DD:EE:FF")
352
+ client.bluetooth.disconnect("AA:BB:CC:DD:EE:FF")
353
+ ```
354
+
355
+ ---
356
+
357
+ ## Real-time events
358
+
359
+ Call `#connect` to open the WebSocket and start receiving events. The client speaks the GraphQL `graphql-transport-ws` subprotocol.
360
+
361
+ ```ruby
362
+ client = Rockbox::Client.new
363
+ client.connect
364
+
365
+ client.on(:track_changed) { |track| puts "▶ #{track.title}" }
366
+ client.on(:status_changed) { |status| puts Rockbox::PlaybackStatus.name(status) }
367
+ client.on(:playlist_changed) { |playlist| puts "queue: #{playlist.amount} tracks" }
368
+ client.on(:ws_open) { puts "connected" }
369
+ client.on(:ws_close) { puts "disconnected" }
370
+ client.on(:ws_error) { |err| warn err.message }
371
+
372
+ # Once-only listener
373
+ client.once(:track_changed) { |t| puts "first track: #{t.title}" }
374
+
375
+ # Remove a specific listener with the same Proc
376
+ listener = ->(t) { puts t.title }
377
+ client.on(:track_changed, &listener)
378
+ client.off(:track_changed, listener)
379
+
380
+ # Remove all listeners for an event (or all events)
381
+ client.remove_all_listeners(:track_changed)
382
+ client.remove_all_listeners
383
+
384
+ # Tear down
385
+ client.disconnect
386
+ ```
387
+
388
+ | Event | Payload |
389
+ |--------------------|--------------------------------------|
390
+ | `:track_changed` | `Rockbox::Track` |
391
+ | `:status_changed` | `Integer` (`Rockbox::PlaybackStatus`)|
392
+ | `:playlist_changed`| `Rockbox::Playlist` |
393
+ | `:ws_open` | `nil` |
394
+ | `:ws_close` | `nil` |
395
+ | `:ws_error` | `Exception` |
396
+
397
+ ---
398
+
399
+ ## Plugin system
400
+
401
+ Plugins are duck-typed objects with `#name`, `#version`, and `#install(context)`. Inherit from `Rockbox::Plugin` for sane defaults.
402
+
403
+ ```ruby
404
+ class ConsoleScrobbler < Rockbox::Plugin
405
+ def name; "console-scrobbler" end
406
+ def version; "0.1.0" end
407
+ def description; "Logs every track change" end
408
+
409
+ def install(ctx)
410
+ ctx.events.on(:track_changed) do |track|
411
+ puts "♪ #{track.artist} — #{track.title}"
412
+ end
413
+ end
414
+
415
+ def uninstall
416
+ # cleanup
417
+ end
418
+ end
419
+
420
+ client.use(ConsoleScrobbler.new)
421
+ client.installed_plugins # => [<ConsoleScrobbler ...>]
422
+ client.unuse("console-scrobbler")
423
+ ```
424
+
425
+ The `PluginContext` exposes:
426
+
427
+ - `ctx.query.call(gql, variables = nil)` — issue raw GraphQL operations
428
+ - `ctx.events` — the same `Rockbox::EventEmitter` used by the client
429
+
430
+ ---
431
+
432
+ ## Error handling
433
+
434
+ ```ruby
435
+ begin
436
+ client.library.album("does-not-exist")
437
+ rescue Rockbox::GraphQLError => e
438
+ e.errors.each { |err| warn err[:message] }
439
+ rescue Rockbox::NetworkError => e
440
+ warn "rockboxd unreachable: #{e.message}"
441
+ end
442
+ ```
443
+
444
+ | Class | Raised when… |
445
+ |---------------------------|----------------------------------------------|
446
+ | `Rockbox::Error` | Base class for every SDK error. |
447
+ | `Rockbox::NetworkError` | rockboxd is unreachable / non-2xx response. |
448
+ | `Rockbox::GraphQLError` | rockboxd returns a GraphQL `errors` payload. |
449
+
450
+ ---
451
+
452
+ ## Raw GraphQL queries
453
+
454
+ For operations the SDK doesn't yet wrap, use `#query` directly. Variables are camelized on the way out, response keys are snakeized on the way in.
455
+
456
+ ```ruby
457
+ data = client.query(
458
+ "query LikedSongs { likedTracks { id title } }"
459
+ )
460
+ data[:liked_tracks].each { |t| puts t[:title] }
461
+
462
+ data = client.query(<<~GQL, { id: "abc" })
463
+ query Track($id: String!) {
464
+ track(id: $id) { id title artist }
465
+ }
466
+ GQL
467
+ ```
468
+
469
+ ---
470
+
471
+ ## License
472
+
473
+ MIT License. See [LICENSE](./LICENSE) for details.
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../types"
4
+
5
+ module Rockbox
6
+ module Api
7
+ class Bluetooth
8
+ FIELDS = <<~GQL
9
+ fragment BluetoothDeviceFields on BluetoothDevice {
10
+ address name paired trusted connected rssi
11
+ }
12
+ GQL
13
+
14
+ def initialize(http)
15
+ @http = http
16
+ end
17
+
18
+ # List paired/known Bluetooth devices (Linux only).
19
+ def devices
20
+ data = @http.execute("#{FIELDS}\nquery BluetoothDevices { bluetoothDevices { ...BluetoothDeviceFields } }")
21
+ Array(data[:bluetooth_devices]).map { |d| BluetoothDevice.from_hash(d) }
22
+ end
23
+
24
+ # Scan for nearby devices (Linux only).
25
+ def scan(timeout: nil)
26
+ data = @http.execute(<<~GQL, timeout ? { timeout_secs: timeout } : nil)
27
+ #{FIELDS}
28
+ mutation BluetoothScan($timeoutSecs: Int) {
29
+ bluetoothScan(timeoutSecs: $timeoutSecs) { ...BluetoothDeviceFields }
30
+ }
31
+ GQL
32
+ Array(data[:bluetooth_scan]).map { |d| BluetoothDevice.from_hash(d) }
33
+ end
34
+
35
+ def connect(address)
36
+ @http.execute(
37
+ "mutation BluetoothConnect($address: String!) { bluetoothConnect(address: $address) }",
38
+ { address: address }
39
+ )
40
+ nil
41
+ end
42
+
43
+ def disconnect(address)
44
+ @http.execute(
45
+ "mutation BluetoothDisconnect($address: String!) { bluetoothDisconnect(address: $address) }",
46
+ { address: address }
47
+ )
48
+ nil
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../types"
4
+
5
+ module Rockbox
6
+ module Api
7
+ class Browse
8
+ def initialize(http)
9
+ @http = http
10
+ end
11
+
12
+ # @param path [String, nil] absolute path; nil for the music root.
13
+ # @return [Array<Rockbox::Entry>]
14
+ def entries(path = nil)
15
+ data = @http.execute(
16
+ "query Browse($path: String) { treeGetEntries(path: $path) { name attr timeWrite customaction displayName } }",
17
+ path ? { path: path } : nil
18
+ )
19
+ Array(data[:tree_get_entries]).map { |e| Entry.from_hash(e) }
20
+ end
21
+
22
+ def directories(path = nil)
23
+ entries(path).select { |e| Rockbox.directory?(e) }
24
+ end
25
+
26
+ def files(path = nil)
27
+ entries(path).reject { |e| Rockbox.directory?(e) }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../types"
4
+
5
+ module Rockbox
6
+ module Api
7
+ class Devices
8
+ FIELDS = "id name host ip port service app isConnected baseUrl isCastDevice isSourceDevice isCurrentDevice"
9
+
10
+ def initialize(http)
11
+ @http = http
12
+ end
13
+
14
+ def list
15
+ data = @http.execute("query Devices { devices { #{FIELDS} } }")
16
+ Array(data[:devices]).map { |d| Device.from_hash(d) }
17
+ end
18
+
19
+ def get(id)
20
+ data = @http.execute(
21
+ "query Device($id: String!) { device(id: $id) { #{FIELDS} } }",
22
+ { id: id }
23
+ )
24
+ Device.from_hash(data[:device])
25
+ end
26
+
27
+ def connect(id)
28
+ @http.execute("mutation ConnectDevice($id: String!) { connect(id: $id) }", { id: id })
29
+ nil
30
+ end
31
+
32
+ def disconnect(id)
33
+ @http.execute("mutation DisconnectDevice($id: String!) { disconnect(id: $id) }", { id: id })
34
+ nil
35
+ end
36
+ end
37
+ end
38
+ end