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 +7 -0
- data/README.md +473 -0
- data/lib/rockbox/api/bluetooth.rb +52 -0
- data/lib/rockbox/api/browse.rb +31 -0
- data/lib/rockbox/api/devices.rb +38 -0
- data/lib/rockbox/api/library.rb +167 -0
- data/lib/rockbox/api/playback.rb +148 -0
- data/lib/rockbox/api/playlist.rb +127 -0
- data/lib/rockbox/api/saved_playlists.rb +163 -0
- data/lib/rockbox/api/settings.rb +118 -0
- data/lib/rockbox/api/smart_playlists.rb +115 -0
- data/lib/rockbox/api/sound.rb +31 -0
- data/lib/rockbox/api/system.rb +32 -0
- data/lib/rockbox/case_conversion.rb +43 -0
- data/lib/rockbox/client.rb +226 -0
- data/lib/rockbox/configuration.rb +38 -0
- data/lib/rockbox/errors.rb +33 -0
- data/lib/rockbox/events.rb +78 -0
- data/lib/rockbox/plugin.rb +60 -0
- data/lib/rockbox/transport.rb +202 -0
- data/lib/rockbox/types.rb +152 -0
- data/lib/rockbox/version.rb +5 -0
- data/lib/rockbox.rb +19 -0
- data/rockbox.gemspec +36 -0
- metadata +124 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rockbox
|
|
4
|
+
# Lightweight typed event emitter. The set of valid events is documented
|
|
5
|
+
# below; emitting an unknown event still works (no enforcement) so plugins
|
|
6
|
+
# can publish their own.
|
|
7
|
+
#
|
|
8
|
+
# Built-in events:
|
|
9
|
+
#
|
|
10
|
+
# | Event | Payload |
|
|
11
|
+
# |--------------------|--------------------------------------|
|
|
12
|
+
# | :track_changed | Rockbox::Track |
|
|
13
|
+
# | :status_changed | Integer (Rockbox::PlaybackStatus) |
|
|
14
|
+
# | :playlist_changed | Rockbox::Playlist |
|
|
15
|
+
# | :ws_open | nil |
|
|
16
|
+
# | :ws_close | nil |
|
|
17
|
+
# | :ws_error | Exception/StandardError |
|
|
18
|
+
class EventEmitter
|
|
19
|
+
def initialize
|
|
20
|
+
@listeners = Hash.new { |h, k| h[k] = [] }
|
|
21
|
+
@lock = Mutex.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @example
|
|
25
|
+
# client.on(:track_changed) { |track| puts track.title }
|
|
26
|
+
def on(event, &block)
|
|
27
|
+
raise ArgumentError, "block required" unless block
|
|
28
|
+
@lock.synchronize { @listeners[event.to_sym] << block }
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @example
|
|
33
|
+
# client.once(:ws_open) { puts "connected!" }
|
|
34
|
+
def once(event, &block)
|
|
35
|
+
raise ArgumentError, "block required" unless block
|
|
36
|
+
wrapper = nil
|
|
37
|
+
wrapper = lambda do |*args|
|
|
38
|
+
off(event, wrapper)
|
|
39
|
+
block.call(*args)
|
|
40
|
+
end
|
|
41
|
+
on(event, &wrapper)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def off(event, listener = nil, &block)
|
|
45
|
+
target = block || listener
|
|
46
|
+
@lock.synchronize do
|
|
47
|
+
if target.nil?
|
|
48
|
+
@listeners.delete(event.to_sym)
|
|
49
|
+
else
|
|
50
|
+
@listeners[event.to_sym].delete(target)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def emit(event, payload = nil)
|
|
57
|
+
listeners = @lock.synchronize { @listeners[event.to_sym].dup }
|
|
58
|
+
listeners.each do |listener|
|
|
59
|
+
if listener.arity.zero? || payload.nil?
|
|
60
|
+
listener.call
|
|
61
|
+
else
|
|
62
|
+
listener.call(payload)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def remove_all_listeners(event = nil)
|
|
68
|
+
@lock.synchronize do
|
|
69
|
+
if event
|
|
70
|
+
@listeners.delete(event.to_sym)
|
|
71
|
+
else
|
|
72
|
+
@listeners.clear
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rockbox
|
|
4
|
+
# Context handed to a plugin's #install method. Plugins can issue raw
|
|
5
|
+
# GraphQL queries and subscribe to the same event stream the SDK uses.
|
|
6
|
+
PluginContext = Struct.new(:query, :events, keyword_init: true) do
|
|
7
|
+
def query!(*args, **kwargs)
|
|
8
|
+
query.call(*args, **kwargs)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Plugin contract — duck-typed. A plugin must respond to:
|
|
13
|
+
# #name => String (unique)
|
|
14
|
+
# #version => String
|
|
15
|
+
# #install(ctx) => any (called when registered)
|
|
16
|
+
#
|
|
17
|
+
# Optional:
|
|
18
|
+
# #description => String
|
|
19
|
+
# #uninstall => any (called on unregister)
|
|
20
|
+
#
|
|
21
|
+
# Inherit from Plugin or just implement the methods directly.
|
|
22
|
+
class Plugin
|
|
23
|
+
def name; raise NotImplementedError; end
|
|
24
|
+
def version; "0.0.0"; end
|
|
25
|
+
def description; nil; end
|
|
26
|
+
def install(_context); end
|
|
27
|
+
def uninstall; end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class PluginRegistry
|
|
31
|
+
def initialize
|
|
32
|
+
@plugins = {}
|
|
33
|
+
@lock = Mutex.new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def register(plugin, context)
|
|
37
|
+
name = plugin.name.to_s
|
|
38
|
+
@lock.synchronize do
|
|
39
|
+
raise ArgumentError, "Plugin #{name.inspect} is already installed" if @plugins.key?(name)
|
|
40
|
+
end
|
|
41
|
+
plugin.install(context)
|
|
42
|
+
@lock.synchronize { @plugins[name] = plugin }
|
|
43
|
+
plugin
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def unregister(name)
|
|
47
|
+
plugin = @lock.synchronize { @plugins.delete(name.to_s) }
|
|
48
|
+
plugin&.uninstall if plugin.respond_to?(:uninstall)
|
|
49
|
+
plugin
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def installed?(name)
|
|
53
|
+
@lock.synchronize { @plugins.key?(name.to_s) }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def list
|
|
57
|
+
@lock.synchronize { @plugins.values.dup }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
|
|
8
|
+
require_relative "errors"
|
|
9
|
+
require_relative "case_conversion"
|
|
10
|
+
|
|
11
|
+
module Rockbox
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# HTTP transport — POSTs GraphQL queries to rockboxd.
|
|
14
|
+
#
|
|
15
|
+
# Every outgoing variables hash is camelCased and every incoming `data`
|
|
16
|
+
# payload is deep-snakeized so callers see idiomatic Ruby keys.
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
class HttpTransport
|
|
19
|
+
DEFAULT_OPEN_TIMEOUT = 5
|
|
20
|
+
DEFAULT_READ_TIMEOUT = 30
|
|
21
|
+
|
|
22
|
+
def initialize(url, open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT)
|
|
23
|
+
@uri = URI.parse(url)
|
|
24
|
+
@open_timeout = open_timeout
|
|
25
|
+
@read_timeout = read_timeout
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Execute a GraphQL operation. Returns the snake-cased `data` Hash.
|
|
29
|
+
def execute(query, variables = nil)
|
|
30
|
+
body = { query: query }
|
|
31
|
+
body[:variables] = CaseConversion.deep_camelize(variables) if variables && !variables.empty?
|
|
32
|
+
|
|
33
|
+
response = perform_request(body)
|
|
34
|
+
parse_response(response)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def perform_request(body)
|
|
40
|
+
http = Net::HTTP.new(@uri.host, @uri.port)
|
|
41
|
+
http.use_ssl = (@uri.scheme == "https")
|
|
42
|
+
http.open_timeout = @open_timeout
|
|
43
|
+
http.read_timeout = @read_timeout
|
|
44
|
+
|
|
45
|
+
request = Net::HTTP::Post.new(@uri.request_uri)
|
|
46
|
+
request["Content-Type"] = "application/json"
|
|
47
|
+
request["Accept"] = "application/json"
|
|
48
|
+
request.body = JSON.generate(body)
|
|
49
|
+
|
|
50
|
+
http.request(request)
|
|
51
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError, Net::OpenTimeout, Net::ReadTimeout => e
|
|
52
|
+
raise NetworkError.new("Failed to reach Rockbox at #{@uri}: #{e.message}", cause: e)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def parse_response(response)
|
|
56
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
57
|
+
raise NetworkError, "HTTP #{response.code} #{response.message}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
payload = JSON.parse(response.body)
|
|
61
|
+
if (errors = payload["errors"]) && !errors.empty?
|
|
62
|
+
raise GraphQLError, errors.map { |e| CaseConversion.deep_snakeize(e) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
CaseConversion.deep_snakeize(payload["data"]) || {}
|
|
66
|
+
rescue JSON::ParserError => e
|
|
67
|
+
raise NetworkError.new("Invalid JSON response: #{e.message}", cause: e)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# WebSocket transport — speaks the `graphql-transport-ws` protocol.
|
|
73
|
+
#
|
|
74
|
+
# Each call to {#subscribe} returns a "stop" lambda that cancels the
|
|
75
|
+
# subscription. Reconnection is intentionally simple: the transport is
|
|
76
|
+
# disposable, so callers should rebuild the client on terminal errors.
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
class WsTransport
|
|
79
|
+
GRAPHQL_TRANSPORT_WS = "graphql-transport-ws"
|
|
80
|
+
|
|
81
|
+
def initialize(url)
|
|
82
|
+
@url = url
|
|
83
|
+
@client = nil
|
|
84
|
+
@lock = Mutex.new
|
|
85
|
+
@sinks = {} # subscription id => sink hash
|
|
86
|
+
@ack = false
|
|
87
|
+
@ack_signal = ConditionVariable.new
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @param query [String]
|
|
91
|
+
# @param variables [Hash, nil]
|
|
92
|
+
# @param sink [Hash{Symbol => Proc}] keys: :next, :error, :complete
|
|
93
|
+
# @return [Proc] call to unsubscribe
|
|
94
|
+
def subscribe(query, variables, sink)
|
|
95
|
+
ensure_connected
|
|
96
|
+
|
|
97
|
+
sub_id = SecureRandom.uuid
|
|
98
|
+
@lock.synchronize { @sinks[sub_id] = sink }
|
|
99
|
+
|
|
100
|
+
send_message(
|
|
101
|
+
id: sub_id,
|
|
102
|
+
type: "subscribe",
|
|
103
|
+
payload: {
|
|
104
|
+
query: query,
|
|
105
|
+
variables: variables ? CaseConversion.deep_camelize(variables) : {}
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
lambda do
|
|
110
|
+
send_message(id: sub_id, type: "complete") rescue nil
|
|
111
|
+
@lock.synchronize { @sinks.delete(sub_id) }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def dispose
|
|
116
|
+
@lock.synchronize do
|
|
117
|
+
@sinks.clear
|
|
118
|
+
if @client
|
|
119
|
+
begin
|
|
120
|
+
@client.close
|
|
121
|
+
rescue StandardError
|
|
122
|
+
# ignored
|
|
123
|
+
end
|
|
124
|
+
@client = nil
|
|
125
|
+
@ack = false
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def ensure_connected
|
|
133
|
+
@lock.synchronize do
|
|
134
|
+
return if @client && @ack
|
|
135
|
+
|
|
136
|
+
require "websocket-client-simple" unless defined?(WebSocket::Client::Simple)
|
|
137
|
+
|
|
138
|
+
transport = self
|
|
139
|
+
@ack = false
|
|
140
|
+
|
|
141
|
+
@client = WebSocket::Client::Simple.connect(@url, headers: { "Sec-WebSocket-Protocol" => GRAPHQL_TRANSPORT_WS })
|
|
142
|
+
|
|
143
|
+
@client.on(:open) { transport.send(:on_open) }
|
|
144
|
+
@client.on(:message) { |msg| transport.send(:on_message, msg) }
|
|
145
|
+
@client.on(:error) { |err| transport.send(:on_error, err) }
|
|
146
|
+
@client.on(:close) { transport.send(:on_close) }
|
|
147
|
+
|
|
148
|
+
# Wait for connection_ack before returning.
|
|
149
|
+
deadline = Time.now + 5.0
|
|
150
|
+
until @ack
|
|
151
|
+
remaining = deadline - Time.now
|
|
152
|
+
raise NetworkError, "Timed out waiting for graphql-transport-ws connection_ack" if remaining <= 0
|
|
153
|
+
@ack_signal.wait(@lock, remaining)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def on_open
|
|
159
|
+
send_message(type: "connection_init", payload: {})
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def on_message(msg)
|
|
163
|
+
data = msg.respond_to?(:data) ? msg.data : msg.to_s
|
|
164
|
+
payload = JSON.parse(data)
|
|
165
|
+
|
|
166
|
+
case payload["type"]
|
|
167
|
+
when "connection_ack"
|
|
168
|
+
@lock.synchronize do
|
|
169
|
+
@ack = true
|
|
170
|
+
@ack_signal.broadcast
|
|
171
|
+
end
|
|
172
|
+
when "next"
|
|
173
|
+
sink = @lock.synchronize { @sinks[payload["id"]] }
|
|
174
|
+
next_payload = payload["payload"] || {}
|
|
175
|
+
data_hash = CaseConversion.deep_snakeize(next_payload["data"])
|
|
176
|
+
sink&.dig(:next)&.call(data: data_hash)
|
|
177
|
+
when "error"
|
|
178
|
+
sink = @lock.synchronize { @sinks[payload["id"]] }
|
|
179
|
+
sink&.dig(:error)&.call(payload["payload"])
|
|
180
|
+
when "complete"
|
|
181
|
+
sink = @lock.synchronize { @sinks.delete(payload["id"]) }
|
|
182
|
+
sink&.dig(:complete)&.call
|
|
183
|
+
end
|
|
184
|
+
rescue JSON::ParserError
|
|
185
|
+
# Drop malformed frames silently; rockboxd never sends them.
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def on_error(err)
|
|
189
|
+
sinks = @lock.synchronize { @sinks.values.dup }
|
|
190
|
+
sinks.each { |s| s[:error]&.call(err) }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def on_close
|
|
194
|
+
@lock.synchronize { @ack = false }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def send_message(message)
|
|
198
|
+
raise NetworkError, "WebSocket is not connected" unless @client
|
|
199
|
+
@client.send(JSON.generate(message))
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rockbox
|
|
4
|
+
# Numeric playback states reported by the firmware.
|
|
5
|
+
module PlaybackStatus
|
|
6
|
+
STOPPED = 0
|
|
7
|
+
PLAYING = 1
|
|
8
|
+
PAUSED = 3
|
|
9
|
+
|
|
10
|
+
def self.name(value)
|
|
11
|
+
{ 0 => :stopped, 1 => :playing, 3 => :paused }[value] || :unknown
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module RepeatMode
|
|
16
|
+
OFF = 0
|
|
17
|
+
ALL = 1
|
|
18
|
+
ONE = 2
|
|
19
|
+
SHUFFLE = 3
|
|
20
|
+
AB_REPEAT = 4
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module ChannelConfig
|
|
24
|
+
STEREO = 0
|
|
25
|
+
STEREO_NARROW = 1
|
|
26
|
+
MONO = 2
|
|
27
|
+
LEFT_MIX = 3
|
|
28
|
+
RIGHT_MIX = 4
|
|
29
|
+
KARAOKE = 5
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module ReplaygainType
|
|
33
|
+
TRACK = 0
|
|
34
|
+
ALBUM = 1
|
|
35
|
+
SHUFFLE = 2
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Where to insert tracks in the queue (Kodi/Mopidy convention).
|
|
39
|
+
module InsertPosition
|
|
40
|
+
NEXT = 0
|
|
41
|
+
AFTER_CURRENT = 1
|
|
42
|
+
LAST = 2
|
|
43
|
+
FIRST = 3
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Value objects
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
#
|
|
50
|
+
# Each type is a Struct that ignores unknown keys at construction so the
|
|
51
|
+
# firmware can add fields without breaking older SDK builds.
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
|
|
54
|
+
module Type
|
|
55
|
+
# Build a Struct that tolerates unknown / missing fields when coming
|
|
56
|
+
# from a Hash. Struct subclasses raise on unknown keys with `keyword_init`,
|
|
57
|
+
# so {.from_hash} filters the input.
|
|
58
|
+
def self.with(*members)
|
|
59
|
+
klass = Struct.new(*members, keyword_init: true)
|
|
60
|
+
klass.define_singleton_method(:from_hash) do |hash|
|
|
61
|
+
return nil if hash.nil?
|
|
62
|
+
attrs = {}
|
|
63
|
+
members.each { |m| attrs[m] = hash[m] }
|
|
64
|
+
new(**attrs)
|
|
65
|
+
end
|
|
66
|
+
klass.define_singleton_method(:known_members) { members.dup }
|
|
67
|
+
klass
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
Track = Type.with(
|
|
72
|
+
:id, :title, :artist, :album, :genre, :disc, :track_string, :year_string,
|
|
73
|
+
:composer, :comment, :album_artist, :grouping,
|
|
74
|
+
:discnum, :tracknum, :layer, :year, :bitrate, :frequency,
|
|
75
|
+
:filesize, :length, :elapsed, :path,
|
|
76
|
+
:album_id, :artist_id, :genre_id, :album_art
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
Album = Type.with(
|
|
80
|
+
:id, :title, :artist, :year, :year_string, :album_art, :md5,
|
|
81
|
+
:artist_id, :copyright_message, :tracks
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
Artist = Type.with(
|
|
85
|
+
:id, :name, :bio, :image, :tracks, :albums
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
SearchResults = Type.with(
|
|
89
|
+
:artists, :albums, :tracks, :liked_tracks, :liked_albums
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
Playlist = Type.with(
|
|
93
|
+
:amount, :index, :max_playlist_size, :first_index,
|
|
94
|
+
:last_insert_pos, :seed, :last_shuffled_start, :tracks
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
SavedPlaylist = Type.with(
|
|
98
|
+
:id, :name, :description, :image, :folder_id,
|
|
99
|
+
:track_count, :created_at, :updated_at
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
SavedPlaylistFolder = Type.with(:id, :name, :created_at, :updated_at)
|
|
103
|
+
|
|
104
|
+
SmartPlaylist = Type.with(
|
|
105
|
+
:id, :name, :description, :image, :folder_id, :is_system,
|
|
106
|
+
:rules, :created_at, :updated_at
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
TrackStats = Type.with(
|
|
110
|
+
:track_id, :play_count, :skip_count, :last_played, :last_skipped, :updated_at
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
BluetoothDevice = Type.with(
|
|
114
|
+
:address, :name, :paired, :trusted, :connected, :rssi
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
VolumeInfo = Type.with(:volume, :min, :max)
|
|
118
|
+
|
|
119
|
+
Device = Type.with(
|
|
120
|
+
:id, :name, :host, :ip, :port, :service, :app, :is_connected,
|
|
121
|
+
:base_url, :is_cast_device, :is_source_device, :is_current_device
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
Entry = Type.with(:name, :attr, :time_write, :customaction, :display_name)
|
|
125
|
+
|
|
126
|
+
# File-attribute bit set on directory entries.
|
|
127
|
+
ENTRY_DIR_BIT = 0x10
|
|
128
|
+
|
|
129
|
+
def self.directory?(entry)
|
|
130
|
+
(entry.attr.to_i & ENTRY_DIR_BIT) != 0
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
SystemStatus = Type.with(
|
|
134
|
+
:resume_index, :resume_crc32, :resume_elapsed, :resume_offset,
|
|
135
|
+
:runtime, :topruntime, :dircache_size,
|
|
136
|
+
:last_screen, :viewer_icon_count, :last_volume_change
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
EqBandSetting = Type.with(:cutoff, :q, :gain)
|
|
140
|
+
ReplaygainSettings = Type.with(:noclip, :type, :preamp)
|
|
141
|
+
CompressorSettings = Type.with(:threshold, :makeup_gain, :ratio, :knee, :release_time, :attack_time)
|
|
142
|
+
|
|
143
|
+
UserSettings = Type.with(
|
|
144
|
+
:music_dir, :volume, :balance, :bass, :treble, :channel_config, :stereo_width,
|
|
145
|
+
:eq_enabled, :eq_precut, :eq_band_settings, :replaygain_settings, :compressor_settings,
|
|
146
|
+
:crossfade_enabled, :crossfade_fade_in_delay, :crossfade_fade_in_duration,
|
|
147
|
+
:crossfade_fade_out_delay, :crossfade_fade_out_duration, :crossfade_fade_out_mixmode,
|
|
148
|
+
:crossfeed_enabled, :crossfeed_direct_gain, :crossfeed_cross_gain,
|
|
149
|
+
:crossfeed_hf_attenuation, :crossfeed_hf_cutoff,
|
|
150
|
+
:repeat_mode, :single_mode, :party_mode, :shuffle, :player_name
|
|
151
|
+
)
|
|
152
|
+
end
|
data/lib/rockbox.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rockbox/version"
|
|
4
|
+
require_relative "rockbox/errors"
|
|
5
|
+
require_relative "rockbox/types"
|
|
6
|
+
require_relative "rockbox/case_conversion"
|
|
7
|
+
require_relative "rockbox/configuration"
|
|
8
|
+
require_relative "rockbox/transport"
|
|
9
|
+
require_relative "rockbox/events"
|
|
10
|
+
require_relative "rockbox/plugin"
|
|
11
|
+
require_relative "rockbox/client"
|
|
12
|
+
|
|
13
|
+
# Top-level convenience constructor — `Rockbox.new(host: "...")` is a synonym
|
|
14
|
+
# for `Rockbox::Client.new(host: "...")`.
|
|
15
|
+
module Rockbox
|
|
16
|
+
def self.new(**kwargs, &block)
|
|
17
|
+
block ? Client.build(&block) : Client.new(**kwargs)
|
|
18
|
+
end
|
|
19
|
+
end
|
data/rockbox.gemspec
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/rockbox/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "rockbox"
|
|
7
|
+
spec.version = Rockbox::VERSION
|
|
8
|
+
spec.authors = ["Tsiry Sandratraina"]
|
|
9
|
+
spec.email = ["tsiry.sndr@rocksky.app"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Idiomatic Ruby SDK for Rockbox"
|
|
12
|
+
spec.description = "Ruby SDK for Rockbox — a builder-friendly, block-friendly GraphQL client " \
|
|
13
|
+
"with real-time event subscriptions and a plugin system."
|
|
14
|
+
spec.homepage = "https://github.com/tsirysndr/rockbox-zig"
|
|
15
|
+
spec.license = "MIT"
|
|
16
|
+
|
|
17
|
+
spec.required_ruby_version = ">= 3.0"
|
|
18
|
+
|
|
19
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
20
|
+
spec.metadata["source_code_uri"] = "#{spec.homepage}/tree/master/sdk/ruby"
|
|
21
|
+
spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
|
|
22
|
+
spec.metadata["documentation_uri"] = "https://www.rockbox.org"
|
|
23
|
+
|
|
24
|
+
spec.files = Dir[
|
|
25
|
+
"lib/**/*.rb",
|
|
26
|
+
"README.md",
|
|
27
|
+
"rockbox.gemspec"
|
|
28
|
+
]
|
|
29
|
+
spec.require_paths = ["lib"]
|
|
30
|
+
|
|
31
|
+
spec.add_dependency "websocket-client-simple", "~> 0.6"
|
|
32
|
+
|
|
33
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
|
34
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
|
35
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
36
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rockbox
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Tsiry Sandratraina
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: websocket-client-simple
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.6'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.6'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: bundler
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: minitest
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '5.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '5.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rake
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '13.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '13.0'
|
|
68
|
+
description: Ruby SDK for Rockbox — a builder-friendly, block-friendly GraphQL client
|
|
69
|
+
with real-time event subscriptions and a plugin system.
|
|
70
|
+
email:
|
|
71
|
+
- tsiry.sndr@rocksky.app
|
|
72
|
+
executables: []
|
|
73
|
+
extensions: []
|
|
74
|
+
extra_rdoc_files: []
|
|
75
|
+
files:
|
|
76
|
+
- README.md
|
|
77
|
+
- lib/rockbox.rb
|
|
78
|
+
- lib/rockbox/api/bluetooth.rb
|
|
79
|
+
- lib/rockbox/api/browse.rb
|
|
80
|
+
- lib/rockbox/api/devices.rb
|
|
81
|
+
- lib/rockbox/api/library.rb
|
|
82
|
+
- lib/rockbox/api/playback.rb
|
|
83
|
+
- lib/rockbox/api/playlist.rb
|
|
84
|
+
- lib/rockbox/api/saved_playlists.rb
|
|
85
|
+
- lib/rockbox/api/settings.rb
|
|
86
|
+
- lib/rockbox/api/smart_playlists.rb
|
|
87
|
+
- lib/rockbox/api/sound.rb
|
|
88
|
+
- lib/rockbox/api/system.rb
|
|
89
|
+
- lib/rockbox/case_conversion.rb
|
|
90
|
+
- lib/rockbox/client.rb
|
|
91
|
+
- lib/rockbox/configuration.rb
|
|
92
|
+
- lib/rockbox/errors.rb
|
|
93
|
+
- lib/rockbox/events.rb
|
|
94
|
+
- lib/rockbox/plugin.rb
|
|
95
|
+
- lib/rockbox/transport.rb
|
|
96
|
+
- lib/rockbox/types.rb
|
|
97
|
+
- lib/rockbox/version.rb
|
|
98
|
+
- rockbox.gemspec
|
|
99
|
+
homepage: https://github.com/tsirysndr/rockbox-zig
|
|
100
|
+
licenses:
|
|
101
|
+
- MIT
|
|
102
|
+
metadata:
|
|
103
|
+
homepage_uri: https://github.com/tsirysndr/rockbox-zig
|
|
104
|
+
source_code_uri: https://github.com/tsirysndr/rockbox-zig/tree/master/sdk/ruby
|
|
105
|
+
bug_tracker_uri: https://github.com/tsirysndr/rockbox-zig/issues
|
|
106
|
+
documentation_uri: https://www.rockbox.org
|
|
107
|
+
rdoc_options: []
|
|
108
|
+
require_paths:
|
|
109
|
+
- lib
|
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
111
|
+
requirements:
|
|
112
|
+
- - ">="
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: '3.0'
|
|
115
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
116
|
+
requirements:
|
|
117
|
+
- - ">="
|
|
118
|
+
- !ruby/object:Gem::Version
|
|
119
|
+
version: '0'
|
|
120
|
+
requirements: []
|
|
121
|
+
rubygems_version: 4.0.10
|
|
122
|
+
specification_version: 4
|
|
123
|
+
summary: Idiomatic Ruby SDK for Rockbox
|
|
124
|
+
test_files: []
|