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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rockbox
4
+ VERSION = "0.1.0"
5
+ 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: []