Rocksky — Elixir SDK
A pipe-friendly Elixir client for the Rocksky XRPC API.
def deps do
[
{:rocksky_ex, "~> 0.1"}
]
endQuick start
client = Rocksky.new(token: System.get_env("ROCKSKY_TOKEN"))
{:ok, profile} =
client
|> Rocksky.Actor.get_profile(did: "did:plc:7vdlgi2bflelz7mmuxoqjfcr")
{:ok, %{"scrobbles" => scrobbles}} =
client
|> Rocksky.Actor.get_actor_scrobbles(did: "did:plc:7vdlgi2bflelz7mmuxoqjfcr", limit: 25)
The client is always the first argument so calls compose naturally with |>.
Every namespace module mirrors an XRPC NSID: app.rocksky.actor.getProfile
becomes Rocksky.Actor.get_profile/2, app.rocksky.scrobble.createScrobble
becomes Rocksky.Scrobble.create_scrobble/2, and so on.
Configuring the client
Rocksky.new(
base_url: "https://api.rocksky.app", # defaults to the public API
token: "your-bearer-token", # required for authenticated procedures
headers: [{"x-app", "my-app"}], # extra request headers
req_options: [retry: false] # forwarded to Req
)You can also configure the default base URL globally:
# config/config.exs
config :rocksky_ex, base_url: "https://api.rocksky.app"Derive an authenticated client from a shared base:
base = Rocksky.new()
authed = Rocksky.Client.with_token(base, "tok")Examples
Create a scrobble
client
|> Rocksky.Scrobble.create_scrobble(
title: "In Bloom",
artist: "Nirvana",
album: "Nevermind",
timestamp: System.system_time(:second)
)Builder style
The SDK ships a chainable builder for every procedure with a JSON body. Each builder validates required fields locally before hitting the network and gives you autocompleteable setters per field:
| Builder | XRPC procedure |
|---|---|
Rocksky.Scrobble.Builder | app.rocksky.scrobble.createScrobble |
Rocksky.Song.Builder | app.rocksky.song.createSong |
Rocksky.Mirror.Builder | app.rocksky.mirror.putMirrorSource |
Rocksky.Apikey.Builder | app.rocksky.apikey.createApikey |
Rocksky.Shout.ReplyBuilder | app.rocksky.shout.replyShout |
Rocksky.Shout.ReportBuilder | app.rocksky.shout.reportShout |
alias Rocksky.Scrobble.Builder, as: Scrobble
Scrobble.new(title: "In Bloom", artist: "Nirvana")
|> Scrobble.album("Nevermind")
|> Scrobble.album_art("https://...")
|> Scrobble.spotify_link("https://open.spotify.com/track/...")
|> Scrobble.timestamp(System.system_time(:second))
|> Scrobble.submit(client)
# => {:ok, %{...}}alias Rocksky.Song.Builder, as: Song
Song.new(title: "Lithium", artist: "Nirvana")
|> Song.album("Nevermind")
|> Song.isrc("USDW19811234")
|> Song.duration(257_000)
|> Song.submit(client)
Camel-cased lexicon keys (e.g. albumArt, mbId, spotifyLink) become
snake-cased setters (album_art/2, mb_id/2, spotify_link/2). new/1
and put/2 accept either form:
alias Rocksky.Mirror.Builder, as: Mirror
Mirror.new(provider: "lastfm", enabled: true, external_username: "alice")
|> Mirror.api_key("...")
|> Mirror.submit(client)
Use put/2 to set several at once, and to_body/1 to inspect the JSON body
without submitting:
Scrobble.new(title: "x", artist: "y")
|> Scrobble.put(album: "Nevermind", year: 1991)
|> Scrobble.to_body()
# => %{title: "x", artist: "y", album: "Nevermind", year: 1991}Missing required fields are caught locally:
Scrobble.new(title: "Only title") |> Scrobble.submit(client)
# => {:error, %Rocksky.Error{reason: :missing_fields, body: %{missing: [:artist]}}}Both styles coexist — use one-shot keyword lists when it fits, builders when you're constructing the payload over several steps.
Find a song
{:ok, song} = Rocksky.Song.get_song(client, isrc: "USDW19811234")
{:ok, song} = Rocksky.Song.get_song(client, mbid: "f1234567-...")
{:ok, song} = Rocksky.Song.get_song(client, uri: "at://did:plc:abc/app.rocksky.song/123")Charts
client
|> Rocksky.Charts.get_top_tracks(limit: 10, startDate: "2026-01-01")Free-text search
{:ok, results} = Rocksky.Feed.search(client, query: "nevermind")Follow / unfollow
client |> Rocksky.Graph.follow_account(account: "alice.bsky.social")
client |> Rocksky.Graph.unfollow_account(account: "alice.bsky.social")Player remote-control
client |> Rocksky.Player.play(playerId: id)
client |> Rocksky.Player.pause(playerId: id)
client |> Rocksky.Player.next(playerId: id)
client |> Rocksky.Player.seek(playerId: id, position: 60_000)
Paginate with Stream
Stream.unfold(0, fn offset ->
case Rocksky.Actor.get_actor_scrobbles(client,
did: "alice.bsky.social",
limit: 50,
offset: offset
) do
{:ok, %{"scrobbles" => []}} -> nil
{:ok, %{"scrobbles" => batch}} -> {batch, offset + length(batch)}
{:error, _} -> nil
end
end)
|> Stream.flat_map(& &1)
|> Enum.take(500)
See examples/ for runnable scripts (mix run examples/...).
Result shape and errors
Every function returns {:ok, body} on a 2xx response and
{:error, %Rocksky.Error{}} otherwise:
case Rocksky.Song.get_song(client, uri: "at://missing") do
{:ok, song} ->
song
{:error, %Rocksky.Error{status: 404}} ->
:not_found
{:error, %Rocksky.Error{reason: :unauthorized}} ->
:reauth
{:error, err} ->
Logger.error("rocksky: #{Exception.message(err)}")
end%Rocksky.Error{} is also an Exception, so it works with raise/1,
Exception.message/1, and with/pattern matching.
Modules
| Module | NSID prefix |
|---|---|
Rocksky.Actor | app.rocksky.actor.* |
Rocksky.Album | app.rocksky.album.* |
Rocksky.Apikey | app.rocksky.apikey.* |
Rocksky.Artist | app.rocksky.artist.* |
Rocksky.Charts | app.rocksky.charts.* |
Rocksky.Dropbox | app.rocksky.dropbox.* |
Rocksky.Feed | app.rocksky.feed.* |
Rocksky.GoogleDrive | app.rocksky.googledrive.* |
Rocksky.Graph | app.rocksky.graph.* |
Rocksky.Like | app.rocksky.like.* |
Rocksky.Mirror | app.rocksky.mirror.* |
Rocksky.Player | app.rocksky.player.* |
Rocksky.Playlist | app.rocksky.playlist.* |
Rocksky.Scrobble | app.rocksky.scrobble.* |
Rocksky.Shout | app.rocksky.shout.* |
Rocksky.Song | app.rocksky.song.* |
Rocksky.Spotify | app.rocksky.spotify.* |
Rocksky.Stats | app.rocksky.stats.* |
If you need an NSID we haven't wrapped yet you can always drop down to
Rocksky.HTTP.query/3 and Rocksky.HTTP.procedure/4:
Rocksky.HTTP.query(client, "app.rocksky.actor.getProfile", did: "alice")Testing your own code
The SDK routes every request through Req, which means you can stub it with
Req.Test — no extra mock dependency
required:
client =
Rocksky.new(
base_url: "https://api.test.rocksky.app",
req_options: [plug: {Req.Test, MyApp.RockskyStub}]
)
Req.Test.stub(MyApp.RockskyStub, fn conn ->
Req.Test.json(conn, %{"handle" => "alice"})
end)
{:ok, %{"handle" => "alice"}} = Rocksky.Actor.get_profile(client, did: "alice")License
MIT © Tsiry Sandratraina.