> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rocksky.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Ruby

> Idiomatic Ruby client for the Rocksky XRPC API.

`rocksky` is a stdlib-only Ruby client for the Rocksky API.

```ruby theme={null}
client = Rocksky.new(token: ENV["ROCKSKY_TOKEN"])

client.actor.get_profile(did: "tsiry-sandratraina.com")
client.charts.get_top_artists(limit: 10, start_date: "2025-01-01")
client.scrobble.create_scrobble(title: "In Bloom", artist: "Nirvana")
```

Every XRPC NSID maps to a method on a resource object.
`app.rocksky.actor.getProfile` becomes `client.actor.get_profile(...)`.
`app.rocksky.scrobble.createScrobble` becomes
`client.scrobble.create_scrobble(...)`. Kwargs in, parsed JSON out.

## Install

```ruby theme={null}
# Gemfile
gem "rocksky"
```

```bash theme={null}
gem install rocksky
```

Requires Ruby 3.0+. Depends only on Ruby's stdlib (`net/http`, `json`, `uri`).

## Quick start

```ruby theme={null}
require "rocksky"

# Reads ROCKSKY_BASE_URL and ROCKSKY_TOKEN from the env when omitted.
client = Rocksky.new

profile = client.actor.get_profile(did: "tsiry-sandratraina.com")
puts profile["displayName"]
```

For authenticated calls, pass a bearer token:

```ruby theme={null}
client = Rocksky.new(token: "eyJ...")
client.scrobble.create_scrobble(title: "In Bloom", artist: "Nirvana")
```

`with_token` derives a new client without mutating the original — handy when a
single base client is shared across users in a web app:

```ruby theme={null}
base = Rocksky.new
def for_user(base, token) = base.with_token(token)
```

## Resources

| Resource          | Methods                                                                                                                                         |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `client.actor`    | `get_profile`, `get_actor_albums/artists/songs/scrobbles/playlists/loved_songs`, `get_actor_neighbours`, `get_actor_compatibility`              |
| `client.album`    | `get_album`, `get_albums`, `get_album_tracks`                                                                                                   |
| `client.apikey`   | `get_apikeys`, `create_apikey`, `update_apikey`, `remove_apikey` *(auth)*                                                                       |
| `client.artist`   | `get_artist`, `get_artists`, `get_artist_albums/tracks/listeners/recent_listeners`                                                              |
| `client.charts`   | `get_scrobbles_chart`, `get_top_artists`, `get_top_tracks`                                                                                      |
| `client.feed`     | `search`, `get_feed_generators/generator/feed`, `get_stories`, `get_recommendations`, `get_artist_recommendations`, `get_album_recommendations` |
| `client.graph`    | `follow_account`, `unfollow_account`, `get_followers`, `get_follows`, `get_known_followers` *(auth)*                                            |
| `client.like`     | `like_song`, `dislike_song`, `like_shout`, `dislike_shout` *(auth)*                                                                             |
| `client.mirror`   | `get_mirror_sources`, `put_mirror_source` *(auth)*                                                                                              |
| `client.player`   | `play`, `pause`, `next`, `previous`, `seek`, `play_file`, `play_directory`, `add_items_to_queue`, `get_currently_playing`, `get_playback_queue` |
| `client.playlist` | `get_playlist`, `get_playlists`, `create_playlist`, `remove_playlist`, `start_playlist`, `insert_files`, `insert_directory`                     |
| `client.scrobble` | `create_scrobble`, `get_scrobble`, `get_scrobbles`                                                                                              |
| `client.shout`    | `create_shout`, `reply_shout`, `remove_shout`, `report_shout`, `get_*_shouts`, `get_shout_replies`                                              |
| `client.song`     | `get_song`, `get_songs`, `get_song_recent_listeners`, `match_song`, `create_song`                                                               |
| `client.spotify`  | `play`, `pause`, `next`, `previous`, `seek`, `get_currently_playing` *(auth)*                                                                   |
| `client.stats`    | `get_stats`, `get_wrapped`                                                                                                                      |

For anything not covered:

```ruby theme={null}
client.query("app.rocksky.actor.getProfile", did: "did:plc:7vdlgi2bflelz7mmuxoqjfcr")
client.procedure("app.rocksky.like.likeSong", body: { uri: "at://..." })
```

## Conventions

* **Keyword args** for every parameter. Ruby `snake_case` maps to the
  lexicon's `camelCase` (`start_date:` → `startDate`).
* **`nil` is dropped.** Pass `nil` for any optional param and it won't be sent.
* **Arrays are CSV-joined.** Lexicon list params accept Ruby arrays.
* **Hashes in, hashes out.** Responses come back as plain `Hash` (string
  keys), mirroring the lexicon JSON 1:1.

## Errors

```ruby theme={null}
begin
  client.song.get_song(uri: "at://does-not-exist")
rescue Rocksky::NotFound => e
  puts "missing: #{e.message} (status=#{e.status}, nsid=#{e.nsid})"
rescue Rocksky::RateLimited
  sleep 5; retry
rescue Rocksky::Error => e
  warn "rocksky failure: #{e.class}: #{e.message}"
end
```

| Class                     | Status            |
| ------------------------- | ----------------- |
| `Rocksky::BadRequest`     | 400               |
| `Rocksky::Unauthorized`   | 401               |
| `Rocksky::Forbidden`      | 403               |
| `Rocksky::NotFound`       | 404               |
| `Rocksky::RateLimited`    | 429               |
| `Rocksky::ServerError`    | 5xx               |
| `Rocksky::HTTPError`      | any other non-2xx |
| `Rocksky::TransportError` | DNS/TCP/timeouts  |

## Configuration

| Option         | Default                   | Env var            |
| -------------- | ------------------------- | ------------------ |
| `base_url`     | `https://api.rocksky.app` | `ROCKSKY_BASE_URL` |
| `token`        | `nil`                     | `ROCKSKY_TOKEN`    |
| `headers`      | `{}`                      | —                  |
| `user_agent`   | `rocksky-ruby/<version>`  | —                  |
| `open_timeout` | `10` seconds              | —                  |
| `read_timeout` | `30` seconds              | —                  |

## IRB console

```bash theme={null}
$ gem install rocksky
$ ROCKSKY_TOKEN=eyJ... rocksky-console
Rocksky 0.2.0 — interactive console
  base_url : https://api.rocksky.app
  token    : present (set via ROCKSKY_TOKEN)

irb> client.actor.get_profile(did: "did:plc:7vdlgi2bflelz7mmuxoqjfcr")
=> {"did"=>"did:plc:...", "handle"=>"tsiry-sandratraina.com", ...}
```

## Types

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.

## License

MIT © Tsiry Sandratraina. Source:
[`sdk/ruby`](https://tangled.org/@rocksky.app/rocksky/blob/main/sdk/ruby).
