A decentralized music tracking and discovery platform built on AT Protocol 🎵 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
0

Configure Feed

Select the types of activity you want to include in your feed.

rocksky / sdk / elixir
3 folders 6 files
README.md

Rocksky — Elixir SDK#

Hex.pm Hex Docs Elixir Erlang/OTP NIF License

Elixir bindings to the shared Rocksky Rust core (rocksky-sdk) via the :rocksky_erl Rustler NIF: AppView reads, AT Protocol PDS writes (scrobble fan-out, like, follow, shout) and the identity hashes — the same engine behind every Rocksky SDK.

Installation#

def deps do
  [{:rocksky_ex, "~> 0.7"}]
end

rocksky_ex 0.7.0 pulls in rocksky_erl 0.4.0, whose loader fetches the native library from the GitHub release on first use (checksum-verified). For monorepo dev, build it locally and point at it: ../erlang/build-core.sh then set ROCKSKY_ERL_PATH=../erlang.

Quick start#

# Reads — unauthenticated. The last arg overrides https://api.rocksky.app.
{:ok, stats} = Rocksky.global_stats()
IO.puts(stats["scrobbles"])

{:ok, top} = Rocksky.top_tracks(10, 0)
for t <- top, do: IO.puts("#{t["artist"]}#{t["title"]}")

# Writes — log in once (session persisted at the given path).
agent = Rocksky.login("session.json", "alice.bsky.social", "app-password")
{:ok, out} = Rocksky.scrobble(agent, %{
  "title" => "Chaser", "artist" => "Calibro 35",
  "album" => "Jazzploitation", "albumArtist" => "Calibro 35", "durationMs" => 182_320
})
IO.puts(out["scrobbleUri"])

API#

Reads/writes return {:ok, value} | {:error, message} with binary-keyed maps (the wire shape). Records are maps with camelCase binary keys.

Reads — Rocksky#

Named reads: profile(actor, base \\ ""), scrobbles(actor, limit, offset, base \\ ""), top_tracks(limit, offset, base \\ ""), global_stats(base \\ ""). The trailing base overrides the AppView URL.

Universal get — the escape hatch reaches the whole app.rocksky.* read catalog by NSID: get(nsid, params \\ %{}, base \\ "", token \\ ""){:ok, data}.

{:ok, albums} = Rocksky.get("app.rocksky.album.getAlbums", %{"limit" => 20})
{:ok, tracks} = Rocksky.get("app.rocksky.album.getAlbumTracks", %{"uri" => uri})
{:ok, follows} = Rocksky.get("app.rocksky.graph.getFollows", %{"actor" => actor})
{:ok, stats}  = Rocksky.get("app.rocksky.stats.getStats")

Pass a bearer token (4th arg) for auth-gated queries — sent as Authorization: Bearer.

Typed date-window chartstop_tracks_interval(limit, offset, interval) and top_artists_interval(limit, offset, interval), where interval is :all | {:days, n} | {:weeks, n} | {:months, n} | {:years, n} | {:range, start, end}.

{:ok, top} = Rocksky.top_tracks_interval(5, 0, {:days, 7})
{:ok, top} = Rocksky.top_artists_interval(5, 0, :all)

Matchmatch_song(title, artist) resolves a bare title + artist into full canonical metadata.

Writes — Rocksky#

login(session_path, identifier, password, appview \\ "", dedup_path \\ "") → an opaque agent handle. Then scrobble(agent, track) (full metadata; fans out to artist/album/song/scrobble), like(agent, uri, cid), follow(agent, did), shout(agent, subject_uri, subject_cid, message), refresh_session(agent).

Match-then-scrobblescrobble_match(agent, input) resolves canonical metadata and scrobbles in one call. input is a map with camelCase string keys: required "title"/"artist", optional "album" (override), "mbId"/"isrc" (match anchors) and "timestamp" (scrobbled-at Unix seconds, default now) — e.g. scrobble_match(agent, %{"title" => "Chaser", "artist" => "Calibro 35"}). The full-metadata scrobble/2 still works when you already have it.

Dedup + realtime — pass a dedup_path to login/5 to enable the local dedup store, then keep it warm with sync_repo(agent) and hydrate_from_jetstream(agent).

Identity hashes#

Rocksky.song_hash(title, artist, album) — lowercase-hex SHA-256, identical to the server and every other Rocksky SDK.

Example#

ROCKSKY_ERL_PATH=../erlang mix run examples/native_core.exs

License#

MIT.