rocksky 0.8.1 → 0.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5fa47af05712ec34a47fee263a1483c99053c24a9c391a0598018248ad3f73ed
4
- data.tar.gz: a5bd7548931ae1c9abfceacf1ca97c755d1c1e1b6c59be12c52f89f831553d84
3
+ metadata.gz: e27b27d24d5d5c59963b65031fb641ecb90cb322a9107f3a62a431e6d1973a0c
4
+ data.tar.gz: 4a314bfa67627103279e453ab7f3563f3c0265c02ca0c1db1ca88259ca26cb33
5
5
  SHA512:
6
- metadata.gz: c0dd411cb63a1e79b51e55ca2eb0376b4bf92903f94ee4459ec764a92071b10900e25f00adafb0ba4d25d35dd3a1ef08c152566d9d10994d074957ddeba4185d
7
- data.tar.gz: e896f5451742b2c104aab53dc6cb389cc1f9efa482b37d503a98a70bb2e9cef908cf2a2148a5f484b1aea8609c5314104678f1b9100697fe2f94e4cf5c28a7bf
6
+ metadata.gz: 498b7790bd6961f4a5e118ab4485bf124afb9283fb838a38f6c21195fa50ba47e22bc10ae98518e6804401e3b8de63e5580cb8957a3690f0dcf39a4e72d18de9
7
+ data.tar.gz: 3f1bb359b30802470a0c7d59ea921850ee43060bba3c67ba8f672eebb1d01c3d6814135462b47c029104bbfb3f5bb32de2b592ffab590c6521f703c3b37995f9
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "repo": "tsirysndr/rocksky",
3
- "tag": "bindings-v0.5.1",
3
+ "tag": "bindings-v0.6.0",
4
4
  "checksums": {
5
- "aarch64-apple-darwin": "2f8ea8e242f2a899b081e44d139d363f79e943427c639e73b92e813a3232752d",
6
- "aarch64-linux-gnu": "eba5b2f93e19b77d1f2170baa6161f0bad33663cf5d4bbf8f4977ade3c60d286",
7
- "x86_64-apple-darwin": "8e6c25c3cad1e45a241745c503bcd505bb26f6c091243cf90ece0f49a9349126",
8
- "x86_64-linux-gnu": "79bbe4cb83535e5ed32b538809ba030bc10214befc9d4fce76540f62568c81c1",
9
- "x86_64-unknown-freebsd": "59b4e07b6b12931538a2ae77f6ca2e0b542a3aebd23544f6efcf22c429ba088f",
10
- "x86_64-unknown-netbsd": "808ec11ba8cb0e168f399f9f61a8993c8dedfbced51ac57a9922e16e31f1a511"
5
+ "aarch64-apple-darwin": "e2de89af96e1330427898524254ddb57728453947b607c4dd29fb8ed7c513b27",
6
+ "aarch64-linux-gnu": "37b8f5ec3812cb15f7eeb75beb44ffe857cca95c11064d1e781eaff6edf7c14d",
7
+ "x86_64-apple-darwin": "7a550b51fc75e1e4d7c5393423f2920dda980ea8f026c9bde8edd926d18d3795",
8
+ "x86_64-linux-gnu": "683e9c14579041c77fa69b53f73b43a44432719c608d7af74f950c9263c5c53b",
9
+ "x86_64-unknown-freebsd": "4ecf5a61000b46111aef2c66e241c97e712c8ef6af386a01afddfe2c05136893",
10
+ "x86_64-unknown-netbsd": "0c619e60f6cf95c42bbe61adb26be8550024c8cbea08be6de9fb3127db627900"
11
11
  }
12
12
  }
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rocksky
4
+ # RemotePlayer — build a Rocksky-controllable player in a few lines.
5
+ #
6
+ # It speaks the remote-control WebSocket protocol (see remote-ws/PROTOCOL.md)
7
+ # over the shared Rust core: it registers as a device, advertises what you're
8
+ # playing (now-playing / status / queue), and invokes your handlers when a
9
+ # miniplayer sends a command (play/pause/next/previous/seek/enqueue/queue
10
+ # actions). Heartbeat, reconnect, and the device-id handshake are handled for
11
+ # you inside the core.
12
+ #
13
+ # Commands arrive over a blocking poll (#next_command). Register handlers with
14
+ # #on and call #listen to spawn a background thread that dispatches them, so you
15
+ # never write the loop yourself.
16
+ #
17
+ # player = Rocksky::RemotePlayer.connect(token, "My Player")
18
+ # player.on(:play) { engine.play }
19
+ # player.on(:pause) { engine.pause }
20
+ # player.on(:seek) { |cmd| engine.seek(cmd[:position]) }
21
+ # player.listen
22
+ # # …then, as your engine plays:
23
+ # player.set_now_playing(title: "Chaser", artist: "Calibro 35",
24
+ # durationMs: 182_320, elapsedMs: 0, isPlaying: true)
25
+ # player.set_status("playing")
26
+ # player.set_queue(items, 0)
27
+ #
28
+ # Records (now-playing / queue items) are passed as Hashes with camelCase keys
29
+ # (title, artist, album, albumArtist, albumArt, durationMs, elapsedMs,
30
+ # isPlaying / songUri, albumUri, trackNumber), matching the wire record shape.
31
+ class RemotePlayer
32
+ # Connect and register a controllable player in the background. +token+ is a
33
+ # Rocksky access token (JWT); +name+ is the device-picker label; +url+
34
+ # defaults to the public endpoint when omitted.
35
+ def self.connect(token, name, url: nil)
36
+ ptr = C.rocksky_remote_player_connect(token.to_s, name.to_s, url.to_s)
37
+ raise Error, (Rocksky.take_string(C.rocksky_last_error()) || "connect failed") if ptr.null?
38
+
39
+ new(ptr)
40
+ end
41
+
42
+ def initialize(ptr)
43
+ @ptr = ptr
44
+ @handlers = {}
45
+ @thread = nil
46
+ end
47
+
48
+ # Register a handler for a command action, one of +:play+, +:pause+, +:next+,
49
+ # +:previous+, +:seek+, +:queue_jump+, +:queue_remove+, +:enqueue+. The block
50
+ # receives the full symbol-keyed command Hash (e.g. +{ action: "seek",
51
+ # position: 42000 }+). Returns +self+ for chaining.
52
+ def on(action, &block)
53
+ @handlers[action.to_sym] = block
54
+ self
55
+ end
56
+
57
+ # Block until the next controller command, returned as a symbol-keyed Hash
58
+ # (e.g. +{ action: "play" }+), or +nil+ once disconnected.
59
+ def next_command
60
+ cmd = Rocksky.unwrap(C.rocksky_remote_player_next_command(@ptr))
61
+ cmd && Rocksky.symbolize(cmd)
62
+ end
63
+
64
+ # Spawn a background thread that polls #next_command and dispatches each
65
+ # command to its registered handler until the player disconnects. Returns the
66
+ # Thread.
67
+ def listen
68
+ @thread ||= Thread.new do
69
+ loop do
70
+ cmd = next_command
71
+ break if cmd.nil?
72
+
73
+ dispatch(cmd)
74
+ end
75
+ end
76
+ end
77
+
78
+ # Advertise the currently-playing track. Call whenever it changes, and
79
+ # periodically (~every 1–4s) with a fresh +elapsedMs+ so controllers show
80
+ # smooth progress. +track+ is a camelCase Hash.
81
+ def set_now_playing(track)
82
+ Rocksky.unwrap(C.rocksky_remote_player_set_now_playing(@ptr, JSON.generate(track)))
83
+ end
84
+
85
+ # Advertise transport state: "playing", "paused", or "stopped".
86
+ def set_status(status)
87
+ Rocksky.unwrap(C.rocksky_remote_player_set_status(@ptr, status.to_s))
88
+ end
89
+
90
+ # Advertise the playback queue (+items+ is an array of camelCase queue-item
91
+ # Hashes) and the active +index+.
92
+ def set_queue(items, index)
93
+ Rocksky.unwrap(C.rocksky_remote_player_set_queue(@ptr, JSON.generate(items), index))
94
+ end
95
+
96
+ # Disconnect and stop the background task. #next_command then returns nil,
97
+ # which ends the #listen thread. The handle stays valid until #close.
98
+ def disconnect
99
+ Rocksky.unwrap(C.rocksky_remote_player_disconnect(@ptr))
100
+ end
101
+
102
+ # Release the native handle. The player is unusable afterwards.
103
+ def close
104
+ return if @ptr.null?
105
+
106
+ @thread&.kill
107
+ @thread = nil
108
+ C.rocksky_remote_player_free(@ptr)
109
+ @ptr = Fiddle::Pointer.new(0)
110
+ end
111
+
112
+ private
113
+
114
+ def dispatch(cmd)
115
+ handler = @handlers[cmd[:action]&.to_sym]
116
+ handler&.call(cmd)
117
+ end
118
+ end
119
+
120
+ # RemoteController — build a Rocksky remote UI in a few lines.
121
+ #
122
+ # The other half of the remote-control WebSocket protocol: where
123
+ # {RemotePlayer} is a controllable player, a controller lists the user's
124
+ # players, observes what each is playing (now-playing / status / queue), picks
125
+ # the primary device, and sends commands. Heartbeat, reconnect, and the
126
+ # register handshake are handled for you inside the core.
127
+ #
128
+ # Events arrive over a blocking poll (#next_event). Register handlers with #on
129
+ # and call #listen to spawn a background dispatch thread.
130
+ #
131
+ # controller = Rocksky::RemoteController.connect(token, "My Controller")
132
+ # controller.on(:devices) { |e| render(e[:devices]) }
133
+ # controller.on(:now_playing) { |e| show_track(e[:deviceId], e[:track]) }
134
+ # controller.listen
135
+ # # …then drive a device:
136
+ # controller.set_primary(device_id)
137
+ # controller.pause(device_id)
138
+ # controller.seek(device_id, 42_000)
139
+ #
140
+ # A +nil+ / omitted +target+ broadcasts a command to all of the user's devices.
141
+ class RemoteController
142
+ # Connect and register a controller in the background. +url+ defaults to the
143
+ # public endpoint when omitted.
144
+ def self.connect(token, name, url: nil)
145
+ ptr = C.rocksky_remote_controller_connect(token.to_s, name.to_s, url.to_s)
146
+ raise Error, (Rocksky.take_string(C.rocksky_last_error()) || "connect failed") if ptr.null?
147
+
148
+ new(ptr)
149
+ end
150
+
151
+ def initialize(ptr)
152
+ @ptr = ptr
153
+ @handlers = {}
154
+ @thread = nil
155
+ end
156
+
157
+ # Register a handler for a server event type, one of +:devices+,
158
+ # +:device_registered+, +:device_unregistered+, +:primary_changed+,
159
+ # +:now_playing+, +:status+, +:queue+. The block receives the full
160
+ # symbol-keyed event Hash. Returns +self+ for chaining.
161
+ def on(event_type, &block)
162
+ @handlers[event_type.to_sym] = block
163
+ self
164
+ end
165
+
166
+ # Block until the next update, returned as a symbol-keyed Hash (each carries a
167
+ # +:type+), or +nil+ once disconnected.
168
+ def next_event
169
+ ev = Rocksky.unwrap(C.rocksky_remote_controller_next_event(@ptr))
170
+ ev && Rocksky.symbolize(ev)
171
+ end
172
+
173
+ # Spawn a background thread that polls #next_event and dispatches each event
174
+ # to its registered handler until the controller disconnects. Returns the
175
+ # Thread.
176
+ def listen
177
+ @thread ||= Thread.new do
178
+ loop do
179
+ ev = next_event
180
+ break if ev.nil?
181
+
182
+ dispatch(ev)
183
+ end
184
+ end
185
+ end
186
+
187
+ # Choose the primary (scrobble/profile) device.
188
+ def set_primary(device_id)
189
+ Rocksky.unwrap(C.rocksky_remote_controller_set_primary(@ptr, device_id.to_s))
190
+ end
191
+
192
+ # +play+. +target+ is a device id, or omit/nil to broadcast to all devices.
193
+ def play(target = nil)
194
+ command("play", target)
195
+ end
196
+
197
+ def pause(target = nil)
198
+ command("pause", target)
199
+ end
200
+
201
+ def next(target = nil)
202
+ command("next", target)
203
+ end
204
+
205
+ def previous(target = nil)
206
+ command("previous", target)
207
+ end
208
+
209
+ # Send a simple command ("play" | "pause" | "next" | "previous").
210
+ def command(action, target = nil)
211
+ Rocksky.unwrap(C.rocksky_remote_controller_command(@ptr, action.to_s, target.to_s))
212
+ end
213
+
214
+ # Seek +target+ to +position_ms+ (nil target = broadcast).
215
+ def seek(target, position_ms)
216
+ Rocksky.unwrap(C.rocksky_remote_controller_seek(@ptr, target.to_s, position_ms))
217
+ end
218
+
219
+ # Jump to +index+ in +target+'s queue (nil target = broadcast).
220
+ def queue_jump(target, index)
221
+ Rocksky.unwrap(C.rocksky_remote_controller_queue_jump(@ptr, target.to_s, index))
222
+ end
223
+
224
+ # Remove queue item +index+ on +target+ (nil target = broadcast).
225
+ def queue_remove(target, index)
226
+ Rocksky.unwrap(C.rocksky_remote_controller_queue_remove(@ptr, target.to_s, index))
227
+ end
228
+
229
+ # Enqueue +tracks+ (array of camelCase queue-item Hashes) on +target+. +mode+
230
+ # is "now" | "next" | "last"; nil target = broadcast.
231
+ def enqueue(target, tracks, mode: "now", shuffle: false, start_index: 0)
232
+ Rocksky.unwrap(C.rocksky_remote_controller_enqueue(
233
+ @ptr, target.to_s, JSON.generate(tracks), mode.to_s, shuffle ? 1 : 0, start_index
234
+ ))
235
+ end
236
+
237
+ # Disconnect and stop the background task. #next_event then returns nil, which
238
+ # ends the #listen thread. The handle stays valid until #close.
239
+ def disconnect
240
+ Rocksky.unwrap(C.rocksky_remote_controller_disconnect(@ptr))
241
+ end
242
+
243
+ # Release the native handle. The controller is unusable afterwards.
244
+ def close
245
+ return if @ptr.null?
246
+
247
+ @thread&.kill
248
+ @thread = nil
249
+ C.rocksky_remote_controller_free(@ptr)
250
+ @ptr = Fiddle::Pointer.new(0)
251
+ end
252
+
253
+ private
254
+
255
+ def dispatch(event)
256
+ handler = @handlers[event[:type]&.to_sym]
257
+ handler&.call(event)
258
+ end
259
+ end
260
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rocksky
4
- VERSION = "0.8.1"
4
+ VERSION = "0.9.0"
5
5
  end
data/lib/rocksky.rb CHANGED
@@ -47,7 +47,26 @@ module Rocksky
47
47
  "char* rocksky_agent_shout(void*, const char*, const char*, const char*)",
48
48
  "char* rocksky_agent_shout_with_gif(void*, const char*, const char*, const char*, const char*)",
49
49
  "char* rocksky_agent_reply_shout_with_gif(void*, const char*, const char*, const char*, const char*, const char*, const char*)",
50
- "char* rocksky_agent_refresh_session(void*)"
50
+ "char* rocksky_agent_refresh_session(void*)",
51
+ # ---- remote player (a controllable device) ----
52
+ "void* rocksky_remote_player_connect(const char*, const char*, const char*)",
53
+ "char* rocksky_remote_player_next_command(void*)",
54
+ "char* rocksky_remote_player_set_now_playing(void*, const char*)",
55
+ "char* rocksky_remote_player_set_status(void*, const char*)",
56
+ "char* rocksky_remote_player_set_queue(void*, const char*, unsigned int)",
57
+ "char* rocksky_remote_player_disconnect(void*)",
58
+ "void rocksky_remote_player_free(void*)",
59
+ # ---- remote controller (a remote UI: drive & observe players) ----
60
+ "void* rocksky_remote_controller_connect(const char*, const char*, const char*)",
61
+ "char* rocksky_remote_controller_next_event(void*)",
62
+ "char* rocksky_remote_controller_set_primary(void*, const char*)",
63
+ "char* rocksky_remote_controller_command(void*, const char*, const char*)",
64
+ "char* rocksky_remote_controller_seek(void*, const char*, unsigned long long)",
65
+ "char* rocksky_remote_controller_queue_jump(void*, const char*, unsigned int)",
66
+ "char* rocksky_remote_controller_queue_remove(void*, const char*, unsigned int)",
67
+ "char* rocksky_remote_controller_enqueue(void*, const char*, const char*, const char*, char, unsigned int)",
68
+ "char* rocksky_remote_controller_disconnect(void*)",
69
+ "void rocksky_remote_controller_free(void*)"
51
70
  ].each { |sig| extern sig }
52
71
  end
53
72
  private_constant :C
@@ -76,6 +95,20 @@ module Rocksky
76
95
  parsed["ok"]
77
96
  end
78
97
 
98
+ # Recursively convert Hash string keys to symbols (Arrays walked too). Used by
99
+ # the remote-control glue so commands/events reach handlers as symbol-keyed
100
+ # hashes (e.g. cmd[:action], event[:type]).
101
+ def self.symbolize(obj)
102
+ case obj
103
+ when Hash
104
+ obj.each_with_object({}) { |(k, v), acc| acc[k.to_sym] = symbolize(v) }
105
+ when Array
106
+ obj.map { |e| symbolize(e) }
107
+ else
108
+ obj
109
+ end
110
+ end
111
+
79
112
  # ---- reads (unauthenticated; base: overrides the AppView URL) ----
80
113
 
81
114
  def self.profile(actor, base: nil)
@@ -269,3 +302,4 @@ module Rocksky
269
302
  end
270
303
 
271
304
  require_relative "rocksky/library"
305
+ require_relative "rocksky/remote"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rocksky
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rocksky
@@ -83,6 +83,7 @@ files:
83
83
  - lib/rocksky/library.rb
84
84
  - lib/rocksky/manifest.json
85
85
  - lib/rocksky/native.rb
86
+ - lib/rocksky/remote.rb
86
87
  - lib/rocksky/version.rb
87
88
  - rocksky.gemspec
88
89
  homepage: https://github.com/tsirysndr/rocksky