rocksky 0.3.0 → 0.4.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 +4 -4
- data/README.md +43 -199
- data/lib/rocksky/manifest.json +5 -0
- data/lib/rocksky/native.rb +114 -0
- data/lib/rocksky/version.rb +3 -1
- data/lib/rocksky.rb +130 -28
- data/rocksky.gemspec +7 -3
- metadata +7 -25
- data/lib/rocksky/client.rb +0 -84
- data/lib/rocksky/error.rb +0 -58
- data/lib/rocksky/generated/types.rb +0 -219
- data/lib/rocksky/http.rb +0 -123
- data/lib/rocksky/resources/actor.rb +0 -60
- data/lib/rocksky/resources/album.rb +0 -22
- data/lib/rocksky/resources/apikey.rb +0 -28
- data/lib/rocksky/resources/artist.rb +0 -40
- data/lib/rocksky/resources/base.rb +0 -24
- data/lib/rocksky/resources/charts.rb +0 -33
- data/lib/rocksky/resources/feed.rb +0 -53
- data/lib/rocksky/resources/graph.rb +0 -34
- data/lib/rocksky/resources/like.rb +0 -26
- data/lib/rocksky/resources/mirror.rb +0 -22
- data/lib/rocksky/resources/player.rb +0 -73
- data/lib/rocksky/resources/playlist.rb +0 -45
- data/lib/rocksky/resources/scrobble.rb +0 -36
- data/lib/rocksky/resources/shout.rb +0 -58
- data/lib/rocksky/resources/song.rb +0 -38
- data/lib/rocksky/resources/spotify.rb +0 -37
- data/lib/rocksky/resources/stats.rb +0 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 56d65f24108dabf8d86f3b8cac9c501d29be2e6ef8cc829f0d3105a457988080
|
|
4
|
+
data.tar.gz: 8c781e0d976b90fe1691fc207802a5730d99911e1da99af75909b615282e2d8a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dbc13ce587f747ea82f7dd17f76d3883fbfc3eb5f08794f26b955414f633f2811d1669c84cce525a7c137485ed8b87be204df9e4622a6fe375548127a1911e85
|
|
7
|
+
data.tar.gz: b2e13963888d6fc54cdf183113f627261b41f089a15846e0561ff592b6dc0d50f193448ac2a5d6531cde2e38238ff056d54f9f4f769fb20c9b429b4eefec5ccf
|
data/README.md
CHANGED
|
@@ -1,229 +1,73 @@
|
|
|
1
1
|
# rocksky (Ruby)
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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.
|
|
3
|
+
Ruby bindings to the shared Rocksky Rust core (`rocksky-sdk`) through Ruby's
|
|
4
|
+
stdlib `fiddle` (no `ffi` gem): AppView reads, AT Protocol PDS writes (scrobble
|
|
5
|
+
fan-out, like, follow, shout) and the identity hashes — the same engine behind
|
|
6
|
+
every Rocksky SDK.
|
|
17
7
|
|
|
18
8
|
## Installation
|
|
19
9
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
```ruby
|
|
23
|
-
gem "rocksky"
|
|
10
|
+
```sh
|
|
11
|
+
gem install rocksky
|
|
24
12
|
```
|
|
25
13
|
|
|
26
|
-
|
|
14
|
+
The gem is pure-Ruby; the native library is fetched from the GitHub release on
|
|
15
|
+
first load and cached (checksum-verified). For a local checkout, build it once:
|
|
27
16
|
|
|
28
|
-
```
|
|
29
|
-
|
|
17
|
+
```sh
|
|
18
|
+
./build-core.sh
|
|
30
19
|
```
|
|
31
20
|
|
|
32
|
-
Requires Ruby 3.0+. The SDK depends only on Ruby's stdlib (`net/http`, `json`, `uri`).
|
|
33
|
-
|
|
34
21
|
## Quick start
|
|
35
22
|
|
|
36
23
|
```ruby
|
|
37
24
|
require "rocksky"
|
|
38
25
|
|
|
39
|
-
# Reads
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
26
|
+
# Reads — unauthenticated. base: overrides https://api.rocksky.app.
|
|
27
|
+
stats = Rocksky.global_stats
|
|
28
|
+
puts stats["scrobbles"]
|
|
29
|
+
Rocksky.top_tracks(limit: 10).each { |t| puts "#{t["artist"]} — #{t["title"]}" }
|
|
30
|
+
|
|
31
|
+
# Writes — log in once (session persisted at the given path).
|
|
32
|
+
agent = Rocksky::Agent.login("session.json", "alice.bsky.social", "app-password")
|
|
33
|
+
out = agent.scrobble(
|
|
34
|
+
"title" => "Chaser", "artist" => "Calibro 35",
|
|
35
|
+
"album" => "Jazzploitation", "albumArtist" => "Calibro 35", "durationMs" => 182_320
|
|
36
|
+
)
|
|
37
|
+
puts out["scrobbleUri"]
|
|
38
|
+
agent.close
|
|
114
39
|
```
|
|
115
40
|
|
|
116
|
-
|
|
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.2.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
|
-
```
|
|
41
|
+
## API
|
|
146
42
|
|
|
147
|
-
|
|
43
|
+
Reads/writes return plain Hashes/Strings (the wire shape); write verbs raise
|
|
44
|
+
`Rocksky::Error` on an `{"error": …}` envelope. Records are Hashes with
|
|
45
|
+
camelCase keys.
|
|
148
46
|
|
|
149
|
-
|
|
150
|
-
source tree, so edits to `lib/` are picked up on the next `reload!`-style
|
|
151
|
-
restart:
|
|
47
|
+
### Reads — `Rocksky`
|
|
152
48
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
$ bin/console
|
|
157
|
-
```
|
|
49
|
+
`profile(actor, base:)`, `scrobbles(actor, limit:, offset:, base:)`,
|
|
50
|
+
`top_tracks(limit:, offset:, base:)`, `global_stats(base:)`. Every read takes an
|
|
51
|
+
optional `base:` to target a custom AppView.
|
|
158
52
|
|
|
159
|
-
###
|
|
53
|
+
### Writes — `Rocksky::Agent`
|
|
160
54
|
|
|
161
|
-
|
|
55
|
+
`Agent.login(session_path, identifier, password, appview:)` → an agent. Then
|
|
56
|
+
`scrobble(track)` (fans out to artist/album/song/scrobble), `like(uri, cid)`,
|
|
57
|
+
`follow(did)`, `shout(subject_uri, subject_cid, message)`, `refresh_session`, and
|
|
58
|
+
`close` (release the native handle).
|
|
162
59
|
|
|
163
|
-
|
|
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"))
|
|
60
|
+
### Identity hashes
|
|
175
61
|
|
|
176
|
-
|
|
177
|
-
|
|
62
|
+
`Rocksky.song_hash(title, artist, album)` — lowercase-hex SHA-256, identical
|
|
63
|
+
to the server and every other Rocksky SDK.
|
|
178
64
|
|
|
179
|
-
|
|
180
|
-
client = Rocksky.new(base_url: "https://api.staging.rocksky.app")
|
|
65
|
+
## Example
|
|
181
66
|
|
|
182
|
-
|
|
183
|
-
|
|
67
|
+
```sh
|
|
68
|
+
ruby -Ilib examples/native_core.rb
|
|
184
69
|
```
|
|
185
70
|
|
|
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
|
-
## Types
|
|
198
|
-
|
|
199
|
-
Lexicon-derived `Struct` types are available under `Rocksky::Generated::*`, mirroring every lex `*View*` / `*Record` / `*Input` / `*Output` / `*Params` shape from [the Rocksky lexicons](https://tangled.org/rocksky.app/rocksky/tree/main/apps/api/lexicons). Regenerate with `bun run lexgen:types` at the repo root.
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
## Examples
|
|
203
|
-
|
|
204
|
-
The `examples/` directory contains runnable scripts:
|
|
205
|
-
|
|
206
|
-
```bash
|
|
207
|
-
bundle exec ruby examples/01_profile.rb tsiry-sandratraina.com
|
|
208
|
-
bundle exec ruby examples/03_charts.rb
|
|
209
|
-
ROCKSKY_TOKEN=... bundle exec ruby examples/02_scrobble.rb
|
|
210
|
-
```
|
|
211
|
-
|
|
212
|
-
See [examples/README.md](examples/README.md) for the full list.
|
|
213
|
-
|
|
214
|
-
## Development
|
|
215
|
-
|
|
216
|
-
```bash
|
|
217
|
-
$ bundle install
|
|
218
|
-
$ bundle exec rake test # run the suite
|
|
219
|
-
$ bin/console # IRB with the local source tree
|
|
220
|
-
```
|
|
221
|
-
|
|
222
|
-
Tests use Minitest + WebMock — no live network access needed. Add a new
|
|
223
|
-
resource by dropping a file in `lib/rocksky/resources/`, wiring it into
|
|
224
|
-
`lib/rocksky.rb` and `lib/rocksky/client.rb`, and adding tests under
|
|
225
|
-
`test/resources/`.
|
|
226
|
-
|
|
227
71
|
## License
|
|
228
72
|
|
|
229
|
-
|
|
73
|
+
MIT.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Resolves the native rocksky-uniffi library, downloading a prebuilt from the
|
|
4
|
+
# GitHub release on first use when it isn't already present locally.
|
|
5
|
+
#
|
|
6
|
+
# Order of preference:
|
|
7
|
+
# 1. $ROCKSKY_NATIVE_LIB, if set.
|
|
8
|
+
# 2. librocksky_uniffi.<ext> next to this file — a local ./build-core.sh build.
|
|
9
|
+
# 3. A checksum-verified copy in the user cache, downloaded on first load (the
|
|
10
|
+
# published-gem path: the native lib is not bundled in the gem).
|
|
11
|
+
#
|
|
12
|
+
# The gem ships manifest.json (repo, release tag, one sha256 per target triple)
|
|
13
|
+
# — filled from the release artifacts by sdk/scripts/gen-uniffi-manifest.sh.
|
|
14
|
+
require "json"
|
|
15
|
+
require "rbconfig"
|
|
16
|
+
require "digest"
|
|
17
|
+
require "fileutils"
|
|
18
|
+
require "net/http"
|
|
19
|
+
require "uri"
|
|
20
|
+
|
|
21
|
+
module Rocksky
|
|
22
|
+
# Raised on native-lib resolution failure or an `{"error": …}` envelope.
|
|
23
|
+
class Error < StandardError; end
|
|
24
|
+
|
|
25
|
+
module Native
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def ext
|
|
29
|
+
case RbConfig::CONFIG["host_os"]
|
|
30
|
+
when /darwin/ then "dylib"
|
|
31
|
+
when /mswin|mingw|cygwin/ then "dll"
|
|
32
|
+
else "so"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def arch
|
|
37
|
+
a = RbConfig::CONFIG["host_cpu"]
|
|
38
|
+
case a
|
|
39
|
+
when /x86_64|amd64/ then "x86_64"
|
|
40
|
+
when /arm64|aarch64/ then "aarch64"
|
|
41
|
+
else a
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Canonical target triple matching the release asset names.
|
|
46
|
+
def triple
|
|
47
|
+
case RbConfig::CONFIG["host_os"]
|
|
48
|
+
when /darwin/ then "#{arch}-apple-darwin"
|
|
49
|
+
when /linux/ then "#{arch}-linux-gnu"
|
|
50
|
+
when /freebsd/ then "#{arch}-unknown-freebsd"
|
|
51
|
+
when /netbsd/ then "#{arch}-unknown-netbsd"
|
|
52
|
+
when /openbsd/ then "#{arch}-unknown-openbsd"
|
|
53
|
+
else raise Error, "unsupported platform: #{RbConfig::CONFIG["host_os"]}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def lib_dir
|
|
58
|
+
__dir__ # sdk/ruby/lib/rocksky
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def cache_dir(tag)
|
|
62
|
+
base = ENV["XDG_CACHE_HOME"] || File.join(Dir.home, ".cache")
|
|
63
|
+
File.join(base, "rocksky", tag)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def manifest
|
|
67
|
+
JSON.parse(File.read(File.join(lib_dir, "manifest.json")))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Absolute path to a loadable native library, fetching it if necessary.
|
|
71
|
+
def resolve
|
|
72
|
+
env = ENV["ROCKSKY_NATIVE_LIB"]
|
|
73
|
+
return env if env && File.exist?(env)
|
|
74
|
+
|
|
75
|
+
local = File.join(lib_dir, "librocksky_uniffi.#{ext}")
|
|
76
|
+
return local if File.exist?(local)
|
|
77
|
+
|
|
78
|
+
m = manifest
|
|
79
|
+
t = triple
|
|
80
|
+
sha = m.dig("checksums", t) or
|
|
81
|
+
raise Error, "no prebuilt native lib for #{t} (manifest has no checksum) — run ./build-core.sh"
|
|
82
|
+
tag = m["tag"]
|
|
83
|
+
dest = File.join(cache_dir(tag), "librocksky_uniffi-#{t}.#{ext}")
|
|
84
|
+
return dest if File.exist?(dest) && Digest::SHA256.file(dest).hexdigest == sha
|
|
85
|
+
|
|
86
|
+
download_verify(m["repo"], tag, t, sha, dest)
|
|
87
|
+
dest
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def download_verify(repo, tag, triple, sha, dest)
|
|
91
|
+
url = "https://github.com/#{repo}/releases/download/#{tag}/librocksky_uniffi-#{triple}.#{ext}"
|
|
92
|
+
body = fetch(url)
|
|
93
|
+
got = Digest::SHA256.hexdigest(body)
|
|
94
|
+
raise Error, "checksum mismatch for #{triple}: want #{sha}, got #{got}" unless got == sha
|
|
95
|
+
|
|
96
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
97
|
+
tmp = "#{dest}.download"
|
|
98
|
+
File.binwrite(tmp, body)
|
|
99
|
+
File.chmod(0o755, tmp)
|
|
100
|
+
File.rename(tmp, dest)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def fetch(url, limit = 5)
|
|
104
|
+
raise Error, "too many redirects" if limit.zero?
|
|
105
|
+
|
|
106
|
+
res = Net::HTTP.get_response(URI(url))
|
|
107
|
+
case res
|
|
108
|
+
when Net::HTTPSuccess then res.body
|
|
109
|
+
when Net::HTTPRedirection then fetch(res["location"], limit - 1)
|
|
110
|
+
else raise Error, "download failed (#{res.code}) for #{url}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
data/lib/rocksky/version.rb
CHANGED
data/lib/rocksky.rb
CHANGED
|
@@ -1,33 +1,135 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
require "rocksky
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
require "
|
|
14
|
-
require "
|
|
15
|
-
require "
|
|
16
|
-
|
|
17
|
-
require "rocksky/resources/shout"
|
|
18
|
-
require "rocksky/resources/song"
|
|
19
|
-
require "rocksky/resources/spotify"
|
|
20
|
-
require "rocksky/resources/stats"
|
|
21
|
-
require "rocksky/client"
|
|
22
|
-
require "rocksky/generated/types"
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Official Ruby SDK for Rocksky.
|
|
4
|
+
#
|
|
5
|
+
# A thin binding, via Ruby's stdlib `fiddle` (no `ffi` gem), to the plain C ABI
|
|
6
|
+
# of the shared Rust core (`rocksky-uniffi` → `rocksky-sdk`): AppView reads, AT
|
|
7
|
+
# Protocol PDS writes (scrobble fan-out, like, follow, shout) and the identity
|
|
8
|
+
# hashes — the same engine behind every Rocksky SDK.
|
|
9
|
+
#
|
|
10
|
+
# require "rocksky"
|
|
11
|
+
# Rocksky.global_stats
|
|
12
|
+
# agent = Rocksky::Agent.login("session.json", "alice.bsky.social", "app-pw")
|
|
13
|
+
require "fiddle"
|
|
14
|
+
require "fiddle/import"
|
|
15
|
+
require "json"
|
|
16
|
+
require_relative "rocksky/native"
|
|
23
17
|
|
|
24
18
|
module Rocksky
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
require_relative "rocksky/version"
|
|
20
|
+
|
|
21
|
+
module C
|
|
22
|
+
extend Fiddle::Importer
|
|
23
|
+
dlload Native.resolve
|
|
24
|
+
[
|
|
25
|
+
"char* rocksky_profile(const char*, const char*)",
|
|
26
|
+
"char* rocksky_scrobbles(const char*, const char*, unsigned int, unsigned int)",
|
|
27
|
+
"char* rocksky_top_tracks(const char*, unsigned int, unsigned int)",
|
|
28
|
+
"char* rocksky_global_stats(const char*)",
|
|
29
|
+
"char* rocksky_song_hash(const char*, const char*, const char*)",
|
|
30
|
+
"void rocksky_string_free(void*)",
|
|
31
|
+
"char* rocksky_last_error()",
|
|
32
|
+
"void* rocksky_agent_login(const char*, const char*, const char*, const char*)",
|
|
33
|
+
"void rocksky_agent_free(void*)",
|
|
34
|
+
"char* rocksky_agent_scrobble(void*, const char*)",
|
|
35
|
+
"char* rocksky_agent_like(void*, const char*, const char*)",
|
|
36
|
+
"char* rocksky_agent_follow(void*, const char*)",
|
|
37
|
+
"char* rocksky_agent_shout(void*, const char*, const char*, const char*)",
|
|
38
|
+
"char* rocksky_agent_refresh_session(void*)"
|
|
39
|
+
].each { |sig| extern sig }
|
|
40
|
+
end
|
|
41
|
+
private_constant :C
|
|
42
|
+
|
|
43
|
+
# C strings the core returns aren't length-tagged; resolve length via strlen.
|
|
44
|
+
STRLEN = Fiddle::Function.new(
|
|
45
|
+
Fiddle::Handle::DEFAULT["strlen"], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_SIZE_T
|
|
46
|
+
)
|
|
47
|
+
private_constant :STRLEN
|
|
48
|
+
|
|
49
|
+
# Copy an owned C string into a Ruby string and free the original.
|
|
50
|
+
def self.take_string(ptr)
|
|
51
|
+
return nil if ptr.nil? || ptr.null?
|
|
52
|
+
|
|
53
|
+
len = STRLEN.call(ptr)
|
|
54
|
+
str = ptr[0, len].force_encoding("UTF-8")
|
|
55
|
+
C.rocksky_string_free(ptr)
|
|
56
|
+
str
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Parse a `{"ok"|"error"}` envelope, raising Error on failure.
|
|
60
|
+
def self.unwrap(ptr)
|
|
61
|
+
parsed = JSON.parse(take_string(ptr))
|
|
62
|
+
raise Error, parsed["error"] if parsed.key?("error")
|
|
63
|
+
|
|
64
|
+
parsed["ok"]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# ---- reads (unauthenticated; base: overrides the AppView URL) ----
|
|
68
|
+
|
|
69
|
+
def self.profile(actor, base: nil)
|
|
70
|
+
unwrap(C.rocksky_profile(base.to_s, actor))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def self.scrobbles(actor, limit: 50, offset: 0, base: nil)
|
|
74
|
+
unwrap(C.rocksky_scrobbles(base.to_s, actor, limit, offset))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.top_tracks(limit: 50, offset: 0, base: nil)
|
|
78
|
+
unwrap(C.rocksky_top_tracks(base.to_s, limit, offset))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.global_stats(base: nil)
|
|
82
|
+
unwrap(C.rocksky_global_stats(base.to_s))
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Identity hash — identical across every Rocksky SDK.
|
|
86
|
+
def self.song_hash(title, artist, album)
|
|
87
|
+
take_string(C.rocksky_song_hash(title, artist, album))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# ---- authenticated agent ----
|
|
28
91
|
#
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
92
|
+
# Records are passed as Hashes with camelCase keys (title, artist, album,
|
|
93
|
+
# albumArtist, durationMs, …), matching the wire record shape.
|
|
94
|
+
class Agent
|
|
95
|
+
def self.login(session_path, identifier, password, appview: nil)
|
|
96
|
+
ptr = C.rocksky_agent_login(session_path, identifier, password, appview.to_s)
|
|
97
|
+
raise Error, (Rocksky.take_string(C.rocksky_last_error()) || "login failed") if ptr.null?
|
|
98
|
+
|
|
99
|
+
new(ptr)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def initialize(ptr)
|
|
103
|
+
@ptr = ptr
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Scrobble a play; fans out to artist/album/song/scrobble. Returns the URIs.
|
|
107
|
+
def scrobble(track)
|
|
108
|
+
Rocksky.unwrap(C.rocksky_agent_scrobble(@ptr, JSON.generate(track)))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def like(uri, cid)
|
|
112
|
+
Rocksky.unwrap(C.rocksky_agent_like(@ptr, uri, cid))
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def follow(did)
|
|
116
|
+
Rocksky.unwrap(C.rocksky_agent_follow(@ptr, did))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def shout(subject_uri, subject_cid, message)
|
|
120
|
+
Rocksky.unwrap(C.rocksky_agent_shout(@ptr, subject_uri, subject_cid, message))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def refresh_session
|
|
124
|
+
Rocksky.unwrap(C.rocksky_agent_refresh_session(@ptr))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Release the native handle. The agent is unusable afterwards.
|
|
128
|
+
def close
|
|
129
|
+
return if @ptr.null?
|
|
130
|
+
|
|
131
|
+
C.rocksky_agent_free(@ptr)
|
|
132
|
+
@ptr = Fiddle::Pointer.new(0)
|
|
133
|
+
end
|
|
32
134
|
end
|
|
33
135
|
end
|
data/rocksky.gemspec
CHANGED
|
@@ -6,9 +6,10 @@ Gem::Specification.new do |spec|
|
|
|
6
6
|
spec.authors = ["Rocksky"]
|
|
7
7
|
spec.email = ["hi@rocksky.app"]
|
|
8
8
|
|
|
9
|
-
spec.summary = "Ruby
|
|
10
|
-
spec.description = "
|
|
11
|
-
"
|
|
9
|
+
spec.summary = "Ruby SDK for Rocksky — native bindings to the shared Rust core."
|
|
10
|
+
spec.description = "Ruby SDK for Rocksky (rocksky.app): native bindings to the " \
|
|
11
|
+
"shared Rust core — AppView reads, AT Protocol PDS writes " \
|
|
12
|
+
"(scrobble, like, follow, shout), and identity hashes."
|
|
12
13
|
spec.homepage = "https://github.com/tsirysndr/rocksky"
|
|
13
14
|
spec.license = "MIT"
|
|
14
15
|
|
|
@@ -24,6 +25,9 @@ Gem::Specification.new do |spec|
|
|
|
24
25
|
|
|
25
26
|
spec.files = Dir[
|
|
26
27
|
"lib/**/*.rb",
|
|
28
|
+
# native-core download manifest (the lib itself is fetched on first load,
|
|
29
|
+
# never bundled — only *.rb + this json ship).
|
|
30
|
+
"lib/rocksky/manifest.json",
|
|
27
31
|
"exe/*",
|
|
28
32
|
"*.gemspec",
|
|
29
33
|
"LICENSE",
|
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.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rocksky
|
|
@@ -65,8 +65,9 @@ dependencies:
|
|
|
65
65
|
- - "~>"
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: '3.19'
|
|
68
|
-
description:
|
|
69
|
-
|
|
68
|
+
description: 'Ruby SDK for Rocksky (rocksky.app): native bindings to the shared Rust
|
|
69
|
+
core — AppView reads, AT Protocol PDS writes (scrobble, like, follow, shout), and
|
|
70
|
+
identity hashes.'
|
|
70
71
|
email:
|
|
71
72
|
- hi@rocksky.app
|
|
72
73
|
executables:
|
|
@@ -79,27 +80,8 @@ files:
|
|
|
79
80
|
- README.md
|
|
80
81
|
- exe/rocksky-console
|
|
81
82
|
- lib/rocksky.rb
|
|
82
|
-
- lib/rocksky/
|
|
83
|
-
- lib/rocksky/
|
|
84
|
-
- lib/rocksky/generated/types.rb
|
|
85
|
-
- lib/rocksky/http.rb
|
|
86
|
-
- lib/rocksky/resources/actor.rb
|
|
87
|
-
- lib/rocksky/resources/album.rb
|
|
88
|
-
- lib/rocksky/resources/apikey.rb
|
|
89
|
-
- lib/rocksky/resources/artist.rb
|
|
90
|
-
- lib/rocksky/resources/base.rb
|
|
91
|
-
- lib/rocksky/resources/charts.rb
|
|
92
|
-
- lib/rocksky/resources/feed.rb
|
|
93
|
-
- lib/rocksky/resources/graph.rb
|
|
94
|
-
- lib/rocksky/resources/like.rb
|
|
95
|
-
- lib/rocksky/resources/mirror.rb
|
|
96
|
-
- lib/rocksky/resources/player.rb
|
|
97
|
-
- lib/rocksky/resources/playlist.rb
|
|
98
|
-
- lib/rocksky/resources/scrobble.rb
|
|
99
|
-
- lib/rocksky/resources/shout.rb
|
|
100
|
-
- lib/rocksky/resources/song.rb
|
|
101
|
-
- lib/rocksky/resources/spotify.rb
|
|
102
|
-
- lib/rocksky/resources/stats.rb
|
|
83
|
+
- lib/rocksky/manifest.json
|
|
84
|
+
- lib/rocksky/native.rb
|
|
103
85
|
- lib/rocksky/version.rb
|
|
104
86
|
- rocksky.gemspec
|
|
105
87
|
homepage: https://github.com/tsirysndr/rocksky
|
|
@@ -127,5 +109,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
127
109
|
requirements: []
|
|
128
110
|
rubygems_version: 4.0.10
|
|
129
111
|
specification_version: 4
|
|
130
|
-
summary: Ruby
|
|
112
|
+
summary: Ruby SDK for Rocksky — native bindings to the shared Rust core.
|
|
131
113
|
test_files: []
|