rocksky 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: e48d42415ab7937416c2e008a6b637b4e2aef4474f60aae824cf1e089a3814f3
4
+ data.tar.gz: eefc4a0023800e5f0d03a4fd41837589b9fc02bd49e4244bcd7a260bc88895a6
5
+ SHA512:
6
+ metadata.gz: 11773d9ea99df5d975d9c81636a3cd0058ea69a4a2cfcb9aa7b2579fbdcd0350f7b7c80ef34e4dadadd5b55232fd4d458773a319449d034118e7f436fafab71b
7
+ data.tar.gz: 94cf1dba5ddb40fb27781044b27bf3cbe1e3cea3720e522541f4db5ab85d1431b1992af384db625128f13c3971a7a543f22f2747e0e011b936d4de3092219a57
data/CHANGELOG.md ADDED
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Initial release. Coverage for all `app.rocksky.*` XRPC endpoints across the
6
+ actor, album, apikey, artist, charts, feed, graph, like, mirror, player,
7
+ playlist, scrobble, shout, song, spotify, and stats namespaces.
8
+ - `rocksky-console` and `bin/console` IRB entrypoints.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tsiry Sandratraina <tsiry.sndr@rocksky.app>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,224 @@
1
+ # rocksky (Ruby)
2
+
3
+ Idiomatic Ruby client for the [Rocksky](https://rocksky.app) XRPC API.
4
+
5
+ ```ruby
6
+ client = Rocksky.new(token: ENV["ROCKSKY_TOKEN"])
7
+
8
+ client.actor.get_profile(did: "tsiry-sandratraina.com")
9
+ client.charts.get_top_artists(limit: 10, start_date: "2025-01-01")
10
+ client.scrobble.create_scrobble(title: "In Bloom", artist: "Nirvana")
11
+ ```
12
+
13
+ Every XRPC NSID maps to a method on a resource object. `app.rocksky.actor.getProfile`
14
+ becomes `client.actor.get_profile(...)`. `app.rocksky.scrobble.createScrobble`
15
+ becomes `client.scrobble.create_scrobble(...)`. No magic — just kwargs in,
16
+ parsed JSON out.
17
+
18
+ ## Installation
19
+
20
+ Add it to your `Gemfile`:
21
+
22
+ ```ruby
23
+ gem "rocksky"
24
+ ```
25
+
26
+ Or install directly:
27
+
28
+ ```bash
29
+ gem install rocksky
30
+ ```
31
+
32
+ Requires Ruby 3.0+. The SDK depends only on Ruby's stdlib (`net/http`, `json`, `uri`).
33
+
34
+ ## Quick start
35
+
36
+ ```ruby
37
+ require "rocksky"
38
+
39
+ # Reads ROCKSKY_BASE_URL and ROCKSKY_TOKEN from the env when omitted.
40
+ client = Rocksky.new
41
+
42
+ profile = client.actor.get_profile(did: "tsiry-sandratraina.com")
43
+ puts profile["displayName"]
44
+ ```
45
+
46
+ For authenticated calls, pass a Bluesky-issued Bearer token (see
47
+ [lexicons documentation](https://github.com/rocksky/rocksky/blob/main/LEXICONS.md)):
48
+
49
+ ```ruby
50
+ client = Rocksky.new(token: "eyJ...")
51
+ client.scrobble.create_scrobble(title: "In Bloom", artist: "Nirvana")
52
+ ```
53
+
54
+ `with_token` derives a new client without mutating the original — useful in
55
+ web apps that share one base client across users:
56
+
57
+ ```ruby
58
+ base = Rocksky.new
59
+ def for_user(base, token) = base.with_token(token)
60
+ ```
61
+
62
+ ## Resources
63
+
64
+ | Resource | Methods |
65
+ |----------|---------|
66
+ | `client.actor` | `get_profile`, `get_actor_albums/artists/songs/scrobbles/playlists/loved_songs`, `get_actor_neighbours`, `get_actor_compatibility` |
67
+ | `client.album` | `get_album`, `get_albums`, `get_album_tracks` |
68
+ | `client.apikey` | `get_apikeys`, `create_apikey`, `update_apikey`, `remove_apikey` *(auth)* |
69
+ | `client.artist` | `get_artist`, `get_artists`, `get_artist_albums/tracks/listeners/recent_listeners` |
70
+ | `client.charts` | `get_scrobbles_chart`, `get_top_artists`, `get_top_tracks` |
71
+ | `client.feed` | `search`, `get_feed_generators/generator/feed`, `get_stories`, `get_recommendations`, `get_artist_recommendations`, `get_album_recommendations` |
72
+ | `client.graph` | `follow_account`, `unfollow_account`, `get_followers`, `get_follows`, `get_known_followers` *(auth)* |
73
+ | `client.like` | `like_song`, `dislike_song`, `like_shout`, `dislike_shout` *(auth)* |
74
+ | `client.mirror` | `get_mirror_sources`, `put_mirror_source` *(auth)* |
75
+ | `client.player` | `play`, `pause`, `next`, `previous`, `seek`, `play_file`, `play_directory`, `add_items_to_queue`, `get_currently_playing`, `get_playback_queue` |
76
+ | `client.playlist` | `get_playlist`, `get_playlists`, `create_playlist`, `remove_playlist`, `start_playlist`, `insert_files`, `insert_directory` |
77
+ | `client.scrobble` | `create_scrobble`, `get_scrobble`, `get_scrobbles` |
78
+ | `client.shout` | `create_shout`, `reply_shout`, `remove_shout`, `report_shout`, `get_*_shouts`, `get_shout_replies` |
79
+ | `client.song` | `get_song`, `get_songs`, `get_song_recent_listeners`, `match_song`, `create_song` |
80
+ | `client.spotify` | `play`, `pause`, `next`, `previous`, `seek`, `get_currently_playing` *(auth)* |
81
+ | `client.stats` | `get_stats`, `get_wrapped` |
82
+
83
+ For anything not covered, drop down to the raw transport:
84
+
85
+ ```ruby
86
+ client.query("app.rocksky.actor.getProfile", did: "did:plc:7vdlgi2bflelz7mmuxoqjfcr")
87
+ client.procedure("app.rocksky.like.likeSong", body: { uri: "at://..." })
88
+ ```
89
+
90
+ ## Conventions
91
+
92
+ - **Keyword args** for every parameter. Ruby `snake_case` names map to the
93
+ lexicon's `camelCase` (e.g. `start_date:` → `startDate`).
94
+ - **`nil` is dropped.** Pass `nil` for any optional param and it won't be sent.
95
+ - **Arrays are CSV-joined.** Lexicon list params like `names:` accept Ruby arrays:
96
+ `client.artist.get_artists(names: %w[Nirvana Pixies])`.
97
+ - **Hashes in, Hashes out.** Responses come back as plain `Hash` (string keys) —
98
+ no DSL, no model classes. Match the shape of the lexicon JSON 1:1.
99
+
100
+ ## Error handling
101
+
102
+ Every non-2xx response raises a subclass of `Rocksky::Error`:
103
+
104
+ ```ruby
105
+ begin
106
+ client.song.get_song(uri: "at://does-not-exist")
107
+ rescue Rocksky::NotFound => e
108
+ puts "missing: #{e.message} (status=#{e.status}, nsid=#{e.nsid})"
109
+ rescue Rocksky::RateLimited
110
+ sleep 5; retry
111
+ rescue Rocksky::Error => e
112
+ warn "rocksky failure: #{e.class}: #{e.message}"
113
+ end
114
+ ```
115
+
116
+ | Class | Status |
117
+ |-------|--------|
118
+ | `Rocksky::BadRequest` | 400 |
119
+ | `Rocksky::Unauthorized` | 401 |
120
+ | `Rocksky::Forbidden` | 403 |
121
+ | `Rocksky::NotFound` | 404 |
122
+ | `Rocksky::RateLimited` | 429 |
123
+ | `Rocksky::ServerError` | 5xx |
124
+ | `Rocksky::HTTPError` | any other non-2xx |
125
+ | `Rocksky::TransportError` | DNS/TCP/timeouts |
126
+
127
+ ## IRB console
128
+
129
+ The gem ships with a `rocksky-console` executable: an IRB session
130
+ pre-loaded with a `client` bound to your environment.
131
+
132
+ ```bash
133
+ $ gem install rocksky
134
+ $ ROCKSKY_TOKEN=eyJ... rocksky-console
135
+ Rocksky 0.1.0 — interactive console
136
+ base_url : https://api.rocksky.app
137
+ token : present (set via ROCKSKY_TOKEN)
138
+
139
+ A client is bound to `client`. Try:
140
+ client.actor.get_profile(did: "did:plc:7vdlgi2bflelz7mmuxoqjfcr")
141
+ client.charts.get_top_artists(limit: 10)
142
+
143
+ irb> client.actor.get_profile(did: "did:plc:7vdlgi2bflelz7mmuxoqjfcr")
144
+ => {"did"=>"did:plc:...", "handle"=>"tsiry-sandratraina.com", ...}
145
+ ```
146
+
147
+ ### From a checkout (development)
148
+
149
+ If you've cloned the repo, use `bin/console` instead. It loads the local
150
+ source tree, so edits to `lib/` are picked up on the next `reload!`-style
151
+ restart:
152
+
153
+ ```bash
154
+ $ cd sdk/ruby
155
+ $ bundle install
156
+ $ bin/console
157
+ ```
158
+
159
+ ### Ad-hoc IRB (no script)
160
+
161
+ You can always launch IRB yourself:
162
+
163
+ ```bash
164
+ $ irb -rrocksky
165
+ irb> client = Rocksky.new(token: ENV["ROCKSKY_TOKEN"])
166
+ irb> client.charts.get_top_tracks(limit: 5)
167
+ ```
168
+
169
+ ### Useful IRB recipes
170
+
171
+ ```ruby
172
+ # Pretty-print responses
173
+ require "json"
174
+ puts JSON.pretty_generate(client.actor.get_profile(did: "did:plc:7vdlgi2bflelz7mmuxoqjfcr"))
175
+
176
+ # Inspect what the SDK is about to send
177
+ client = Rocksky.new(headers: { "X-Debug" => "1" })
178
+
179
+ # Try things against staging without touching prod
180
+ client = Rocksky.new(base_url: "https://api.staging.rocksky.app")
181
+
182
+ # Tighter timeouts in a script
183
+ client = Rocksky.new(open_timeout: 2, read_timeout: 5)
184
+ ```
185
+
186
+ ## Configuration
187
+
188
+ | Option | Default | Env var |
189
+ |----------------|-------------------------------|-----------------------|
190
+ | `base_url` | `https://api.rocksky.app` | `ROCKSKY_BASE_URL` |
191
+ | `token` | `nil` | `ROCKSKY_TOKEN` |
192
+ | `headers` | `{}` | — |
193
+ | `user_agent` | `rocksky-ruby/<version>` | — |
194
+ | `open_timeout` | `10` seconds | — |
195
+ | `read_timeout` | `30` seconds | — |
196
+
197
+ ## Examples
198
+
199
+ The `examples/` directory contains runnable scripts:
200
+
201
+ ```bash
202
+ bundle exec ruby examples/01_profile.rb tsiry-sandratraina.com
203
+ bundle exec ruby examples/03_charts.rb
204
+ ROCKSKY_TOKEN=... bundle exec ruby examples/02_scrobble.rb
205
+ ```
206
+
207
+ See [examples/README.md](examples/README.md) for the full list.
208
+
209
+ ## Development
210
+
211
+ ```bash
212
+ $ bundle install
213
+ $ bundle exec rake test # run the suite
214
+ $ bin/console # IRB with the local source tree
215
+ ```
216
+
217
+ Tests use Minitest + WebMock — no live network access needed. Add a new
218
+ resource by dropping a file in `lib/rocksky/resources/`, wiring it into
219
+ `lib/rocksky.rb` and `lib/rocksky/client.rb`, and adding tests under
220
+ `test/resources/`.
221
+
222
+ ## License
223
+
224
+ [MIT](LICENSE) © Tsiry Sandratraina.
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+ # rocksky-console — IRB session pre-loaded with a `client` bound to your env.
3
+ #
4
+ # $ ROCKSKY_TOKEN=... rocksky-console
5
+ # irb> client.actor.get_profile(did: "alice.bsky.social")
6
+ #
7
+ # Without ROCKSKY_TOKEN, the client is unauthenticated (read-only endpoints
8
+ # still work).
9
+
10
+ require "irb"
11
+ require "rocksky"
12
+
13
+ client = Rocksky.new
14
+
15
+ puts <<~BANNER
16
+ Rocksky #{Rocksky::VERSION} — interactive console
17
+ base_url : #{client.base_url}
18
+ token : #{client.token ? "present (set via ROCKSKY_TOKEN)" : "none — set ROCKSKY_TOKEN for auth"}
19
+
20
+ A client is bound to `client`. Try:
21
+ client.actor.get_profile(did: "alice.bsky.social")
22
+ client.charts.get_top_artists(limit: 10)
23
+
24
+ BANNER
25
+
26
+ IRB.setup(nil)
27
+ IRB.conf[:USE_MULTILINE] = false if IRB.conf.key?(:USE_MULTILINE)
28
+
29
+ workspace = IRB::WorkSpace.new(binding)
30
+ workspace.main.instance_variable_set(:@client, client)
31
+ workspace.main.define_singleton_method(:client) { @client }
32
+
33
+ IRB::Irb.new(workspace).run(IRB.conf)
@@ -0,0 +1,84 @@
1
+ module Rocksky
2
+ # Top-level client for the Rocksky XRPC API.
3
+ #
4
+ # client = Rocksky.new(token: ENV["ROCKSKY_TOKEN"])
5
+ # client.actor.get_profile(did: "alice.bsky.social")
6
+ #
7
+ # Resources are lazily instantiated and memoised: `client.actor`, `client.album`,
8
+ # `client.artist`, `client.scrobble`, etc.
9
+ class Client
10
+ DEFAULT_BASE_URL = "https://api.rocksky.app".freeze
11
+
12
+ attr_reader :base_url, :token, :headers, :user_agent,
13
+ :open_timeout, :read_timeout
14
+
15
+ def initialize(
16
+ base_url: nil,
17
+ token: nil,
18
+ headers: {},
19
+ user_agent: "rocksky-ruby/#{Rocksky::VERSION}",
20
+ open_timeout: HTTP::DEFAULT_OPEN_TIMEOUT,
21
+ read_timeout: HTTP::DEFAULT_READ_TIMEOUT
22
+ )
23
+ @base_url = normalize_base_url(base_url || ENV["ROCKSKY_BASE_URL"] || DEFAULT_BASE_URL)
24
+ @token = token || ENV["ROCKSKY_TOKEN"]
25
+ @headers = headers.dup
26
+ @user_agent = user_agent
27
+ @open_timeout = open_timeout
28
+ @read_timeout = read_timeout
29
+ @http = HTTP.new(self)
30
+ end
31
+
32
+ # Return a derived client that uses the given token (everything else copied).
33
+ # Handy for sharing one client across users in a request-scoped server.
34
+ def with_token(new_token)
35
+ self.class.new(
36
+ base_url: base_url,
37
+ token: new_token,
38
+ headers: headers,
39
+ user_agent: user_agent,
40
+ open_timeout: open_timeout,
41
+ read_timeout: read_timeout
42
+ )
43
+ end
44
+
45
+ # ---- Raw XRPC access ---------------------------------------------------
46
+
47
+ def query(nsid, **params)
48
+ @http.query(nsid, params)
49
+ end
50
+
51
+ def procedure(nsid, params: {}, body: nil)
52
+ @http.procedure(nsid, params, body)
53
+ end
54
+
55
+ # ---- Resource accessors ------------------------------------------------
56
+
57
+ def actor; @actor ||= Resources::Actor.new(@http); end
58
+ def album; @album ||= Resources::Album.new(@http); end
59
+ def apikey; @apikey ||= Resources::Apikey.new(@http); end
60
+ def artist; @artist ||= Resources::Artist.new(@http); end
61
+ def charts; @charts ||= Resources::Charts.new(@http); end
62
+ def feed; @feed ||= Resources::Feed.new(@http); end
63
+ def graph; @graph ||= Resources::Graph.new(@http); end
64
+ def like; @like ||= Resources::Like.new(@http); end
65
+ def mirror; @mirror ||= Resources::Mirror.new(@http); end
66
+ def player; @player ||= Resources::Player.new(@http); end
67
+ def playlist; @playlist ||= Resources::Playlist.new(@http); end
68
+ def scrobble; @scrobble ||= Resources::Scrobble.new(@http); end
69
+ def shout; @shout ||= Resources::Shout.new(@http); end
70
+ def song; @song ||= Resources::Song.new(@http); end
71
+ def spotify; @spotify ||= Resources::Spotify.new(@http); end
72
+ def stats; @stats ||= Resources::Stats.new(@http); end
73
+
74
+ def inspect
75
+ "#<Rocksky::Client base_url=#{base_url.inspect} token=#{token ? "[FILTERED]" : nil.inspect}>"
76
+ end
77
+
78
+ private
79
+
80
+ def normalize_base_url(url)
81
+ url.to_s.sub(%r{/+\z}, "")
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,58 @@
1
+ module Rocksky
2
+ class Error < StandardError
3
+ attr_reader :status, :body, :nsid
4
+
5
+ def initialize(message, status: nil, body: nil, nsid: nil)
6
+ super(message)
7
+ @status = status
8
+ @body = body
9
+ @nsid = nsid
10
+ end
11
+
12
+ def self.from_response(status, body, nsid: nil)
13
+ message = extract_message(body) || "request failed"
14
+ klass = klass_for(status)
15
+ klass.new(message, status: status, body: body, nsid: nsid)
16
+ end
17
+
18
+ def self.from_transport(cause, nsid: nil)
19
+ TransportError.new(
20
+ "transport error: #{cause.class}: #{cause.message}",
21
+ body: nil,
22
+ nsid: nsid
23
+ )
24
+ end
25
+
26
+ def self.klass_for(status)
27
+ case status
28
+ when 400 then BadRequest
29
+ when 401 then Unauthorized
30
+ when 403 then Forbidden
31
+ when 404 then NotFound
32
+ when 429 then RateLimited
33
+ when 500..599 then ServerError
34
+ else HTTPError
35
+ end
36
+ end
37
+ private_class_method :klass_for
38
+
39
+ def self.extract_message(body)
40
+ case body
41
+ when Hash
42
+ body["message"] || body["error"] || body[:message] || body[:error]
43
+ when String
44
+ body unless body.empty?
45
+ end
46
+ end
47
+ private_class_method :extract_message
48
+ end
49
+
50
+ class HTTPError < Error; end
51
+ class BadRequest < HTTPError; end
52
+ class Unauthorized < HTTPError; end
53
+ class Forbidden < HTTPError; end
54
+ class NotFound < HTTPError; end
55
+ class RateLimited < HTTPError; end
56
+ class ServerError < HTTPError; end
57
+ class TransportError < Error; end
58
+ end
@@ -0,0 +1,123 @@
1
+ require "json"
2
+ require "net/http"
3
+ require "uri"
4
+
5
+ module Rocksky
6
+ # Low-level XRPC transport. Most users should go through the resource
7
+ # accessors on {Rocksky::Client} (e.g. `client.actor.get_profile(...)`).
8
+ class HTTP
9
+ DEFAULT_OPEN_TIMEOUT = 10
10
+ DEFAULT_READ_TIMEOUT = 30
11
+
12
+ attr_reader :client
13
+
14
+ def initialize(client)
15
+ @client = client
16
+ end
17
+
18
+ # GET /xrpc/<nsid>?...
19
+ def query(nsid, params = {})
20
+ request(method: :get, nsid: nsid, params: params)
21
+ end
22
+
23
+ # POST /xrpc/<nsid>?... with optional JSON body.
24
+ def procedure(nsid, params = {}, body = nil)
25
+ request(method: :post, nsid: nsid, params: params, body: body)
26
+ end
27
+
28
+ private
29
+
30
+ def request(method:, nsid:, params: {}, body: nil)
31
+ uri = build_uri(nsid, params)
32
+ req = build_request(method, uri, body)
33
+ apply_headers(req)
34
+
35
+ response = perform(uri, req)
36
+ handle_response(response, nsid)
37
+ rescue Timeout::Error, Errno::ECONNREFUSED, SocketError, IOError,
38
+ Net::OpenTimeout, Net::ReadTimeout => e
39
+ raise Error.from_transport(e, nsid: nsid)
40
+ end
41
+
42
+ def build_uri(nsid, params)
43
+ url = "#{client.base_url}/xrpc/#{nsid}"
44
+ uri = URI.parse(url)
45
+ encoded = encode_params(params)
46
+ uri.query = URI.encode_www_form(encoded) unless encoded.empty?
47
+ uri
48
+ end
49
+
50
+ def build_request(method, uri, body)
51
+ req =
52
+ case method
53
+ when :get then Net::HTTP::Get.new(uri.request_uri)
54
+ when :post then Net::HTTP::Post.new(uri.request_uri)
55
+ else raise ArgumentError, "unsupported method: #{method}"
56
+ end
57
+
58
+ if body && !body.empty?
59
+ req["content-type"] = "application/json"
60
+ req.body = JSON.generate(body)
61
+ end
62
+
63
+ req
64
+ end
65
+
66
+ def apply_headers(req)
67
+ req["accept"] = "application/json"
68
+ req["user-agent"] = client.user_agent if client.user_agent
69
+ req["authorization"] = "Bearer #{client.token}" if client.token
70
+ client.headers.each { |name, value| req[name] = value }
71
+ end
72
+
73
+ def perform(uri, req)
74
+ http = Net::HTTP.new(uri.host, uri.port)
75
+ http.use_ssl = (uri.scheme == "https")
76
+ http.open_timeout = client.open_timeout
77
+ http.read_timeout = client.read_timeout
78
+ http.request(req)
79
+ end
80
+
81
+ def handle_response(response, nsid)
82
+ status = response.code.to_i
83
+ body = parse_body(response)
84
+
85
+ return body if status.between?(200, 299)
86
+
87
+ raise Error.from_response(status, body, nsid: nsid)
88
+ end
89
+
90
+ def parse_body(response)
91
+ return nil if response.body.nil? || response.body.empty?
92
+
93
+ content_type = response["content-type"].to_s
94
+ if content_type.include?("application/json")
95
+ JSON.parse(response.body)
96
+ else
97
+ response.body
98
+ end
99
+ rescue JSON::ParserError
100
+ response.body
101
+ end
102
+
103
+ # Drop nil values; convert symbol keys to strings; join arrays with commas
104
+ # (matches the lexicon-defined array encoding used by other SDKs).
105
+ def encode_params(params)
106
+ return {} if params.nil?
107
+
108
+ Hash(params).each_with_object({}) do |(key, value), out|
109
+ next if value.nil?
110
+
111
+ out[key.to_s] = encode_value(value)
112
+ end
113
+ end
114
+
115
+ def encode_value(value)
116
+ case value
117
+ when Array then value.join(",")
118
+ when true, false then value.to_s
119
+ else value
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,60 @@
1
+ module Rocksky
2
+ module Resources
3
+ # `app.rocksky.actor.*` endpoints.
4
+ class Actor < Base
5
+ # Fetch a profile by DID or handle.
6
+ def get_profile(did:)
7
+ query("app.rocksky.actor.getProfile", did: did)
8
+ end
9
+
10
+ # Albums an actor has scrobbled.
11
+ def get_actor_albums(did:, limit: nil, offset: nil, start_date: nil, end_date: nil)
12
+ query("app.rocksky.actor.getActorAlbums",
13
+ did: did, limit: limit, offset: offset,
14
+ startDate: start_date, endDate: end_date)
15
+ end
16
+
17
+ # Artists an actor has scrobbled.
18
+ def get_actor_artists(did:, limit: nil, offset: nil, start_date: nil, end_date: nil)
19
+ query("app.rocksky.actor.getActorArtists",
20
+ did: did, limit: limit, offset: offset,
21
+ startDate: start_date, endDate: end_date)
22
+ end
23
+
24
+ # Songs an actor has scrobbled.
25
+ def get_actor_songs(did:, limit: nil, offset: nil, start_date: nil, end_date: nil)
26
+ query("app.rocksky.actor.getActorSongs",
27
+ did: did, limit: limit, offset: offset,
28
+ startDate: start_date, endDate: end_date)
29
+ end
30
+
31
+ # Songs an actor has loved.
32
+ def get_actor_loved_songs(did:, limit: nil, offset: nil)
33
+ query("app.rocksky.actor.getActorLovedSongs",
34
+ did: did, limit: limit, offset: offset)
35
+ end
36
+
37
+ # Scrobbles for an actor.
38
+ def get_actor_scrobbles(did:, limit: nil, offset: nil)
39
+ query("app.rocksky.actor.getActorScrobbles",
40
+ did: did, limit: limit, offset: offset)
41
+ end
42
+
43
+ # Playlists for an actor.
44
+ def get_actor_playlists(did:, limit: nil, offset: nil)
45
+ query("app.rocksky.actor.getActorPlaylists",
46
+ did: did, limit: limit, offset: offset)
47
+ end
48
+
49
+ # Musical neighbours of an actor.
50
+ def get_actor_neighbours(did:)
51
+ query("app.rocksky.actor.getActorNeighbours", did: did)
52
+ end
53
+
54
+ # Compatibility score between the authenticated user and another actor.
55
+ def get_actor_compatibility(did:)
56
+ query("app.rocksky.actor.getActorCompatibility", did: did)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,22 @@
1
+ module Rocksky
2
+ module Resources
3
+ # `app.rocksky.album.*` endpoints.
4
+ class Album < Base
5
+ # Fetch an album by AT-URI.
6
+ def get_album(uri:)
7
+ query("app.rocksky.album.getAlbum", uri: uri)
8
+ end
9
+
10
+ # List albums.
11
+ def get_albums(limit: nil, offset: nil, genre: nil)
12
+ query("app.rocksky.album.getAlbums",
13
+ limit: limit, offset: offset, genre: genre)
14
+ end
15
+
16
+ # Tracks belonging to an album.
17
+ def get_album_tracks(uri:)
18
+ query("app.rocksky.album.getAlbumTracks", uri: uri)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,28 @@
1
+ module Rocksky
2
+ module Resources
3
+ # `app.rocksky.apikey.*` endpoints. All require an authenticated client.
4
+ class Apikey < Base
5
+ # List your API keys.
6
+ def get_apikeys(limit: nil, offset: nil)
7
+ query("app.rocksky.apikey.getApikeys", limit: limit, offset: offset)
8
+ end
9
+
10
+ # Create a new API key.
11
+ def create_apikey(name:, description: nil)
12
+ body = { name: name, description: description }.compact
13
+ procedure("app.rocksky.apikey.createApikey", body: body)
14
+ end
15
+
16
+ # Update an API key.
17
+ def update_apikey(id:, name: nil, description: nil)
18
+ body = { id: id, name: name, description: description }.compact
19
+ procedure("app.rocksky.apikey.updateApikey", body: body)
20
+ end
21
+
22
+ # Remove an API key.
23
+ def remove_apikey(id:)
24
+ procedure("app.rocksky.apikey.removeApikey", params: { id: id })
25
+ end
26
+ end
27
+ end
28
+ end