A decentralized music tracking and discovery platform built on AT Protocol 🎵
rocksky.app
spotify
atproto
lastfm
musicbrainz
scrobbling
listenbrainz
1defmodule Rocksky do
2 @moduledoc """
3 Official Elixir SDK for Rocksky.
4
5 A thin wrapper over the `:rocksky_erl` Rustler NIF (the shared Rust core,
6 `rocksky-sdk`): AppView reads, AT Protocol PDS writes (scrobble fan-out, like,
7 follow, shout) and the identity hashes — the same engine behind every Rocksky
8 SDK.
9
10 Reads/writes return `{:ok, value}` | `{:error, message}` with binary-keyed
11 maps (the wire shape). Records passed to the write verbs are maps with
12 camelCase binary keys — `"title"`, `"artist"`, `"album"`, `"albumArtist"`,
13 `"durationMs"`, …
14
15 {:ok, stats} = Rocksky.global_stats()
16 Rocksky.song_hash("Chaser", "Calibro 35", "Jazzploitation")
17 """
18
19 # ---- reads (unauthenticated; trailing base overrides the AppView URL) ----
20
21 @doc "An actor's detailed profile."
22 def profile(actor, base \\ ""), do: :rocksky.profile(to_bin(actor), to_bin(base))
23
24 @doc "An actor's scrobbles, newest first."
25 def scrobbles(actor, limit \\ 50, offset \\ 0, base \\ ""),
26 do: :rocksky.scrobbles(to_bin(actor), limit, offset, to_bin(base))
27
28 @doc "Platform-wide top tracks chart."
29 def top_tracks(limit \\ 50, offset \\ 0, base \\ ""),
30 do: :rocksky.top_tracks(limit, offset, to_bin(base))
31
32 @doc "Platform-wide totals."
33 def global_stats(base \\ ""), do: :rocksky.global_stats(to_bin(base))
34
35 @doc """
36 Universal read escape hatch — call any `app.rocksky.*` query by nsid.
37
38 `params` is a map of string params; the whole read-query catalog is reachable.
39
40 Rocksky.get("app.rocksky.album.getAlbum", %{"uri" => uri})
41 Rocksky.get("app.rocksky.charts.getScrobblesChart", %{"did" => did})
42 """
43 def get(nsid, params \\ %{}, base \\ "", token \\ ""),
44 do: :rocksky.get(to_bin(nsid), params, to_bin(base), to_bin(token))
45
46 @doc """
47 The authenticated viewer's unread-notification count (`token` required).
48 Returns `%{"count" => n}`.
49 """
50 def unread_count(token, base \\ ""),
51 do: :rocksky.unread_count(to_bin(token), to_bin(base))
52
53 @doc """
54 The authenticated viewer's notifications, most recent first (`token` required).
55 `params` may include `"limit"` (default 30) and `"cursor"`.
56 """
57 def notifications(token, params \\ %{}, base \\ ""),
58 do: :rocksky.notifications(to_bin(token), params, to_bin(base))
59
60 @doc """
61 Mark notifications as viewed (`token` required). `ids` is a list of notification
62 ids, or `[]` to mark all. Returns `%{"unreadCount" => n}`.
63 """
64 def update_seen(token, ids \\ [], base \\ ""),
65 do: :rocksky.update_seen(to_bin(token), ids, to_bin(base))
66
67 @doc "Resolve full canonical metadata for a bare title + artist (matchSong)."
68 def match_song(title, artist, mb_id \\ "", isrc \\ "", base \\ ""),
69 do: :rocksky.match_song(to_bin(base), to_bin(title), to_bin(artist), to_bin(mb_id), to_bin(isrc))
70
71 @doc """
72 Top tracks chart over a typed date window.
73
74 `interval` is `:all` | `{:days, n}` | `{:weeks, n}` | `{:months, n}` |
75 `{:years, n}` | `{:range, start_rfc3339, end_rfc3339}`.
76
77 Rocksky.top_tracks_interval(10, 0, {:days, 7})
78 """
79 def top_tracks_interval(limit \\ 50, offset \\ 0, interval \\ :all, base \\ ""),
80 do: :rocksky.top_tracks_interval(limit, offset, interval, to_bin(base))
81
82 @doc "Top artists chart over a typed date window (see `top_tracks_interval/4`)."
83 def top_artists_interval(limit \\ 50, offset \\ 0, interval \\ :all, base \\ ""),
84 do: :rocksky.top_artists_interval(limit, offset, interval, to_bin(base))
85
86 @doc "Identity hash of a song — identical across every Rocksky SDK."
87 def song_hash(title, artist, album),
88 do: :rocksky.song_hash(to_bin(title), to_bin(artist), to_bin(album))
89
90 # ---- authenticated agent -------------------------------------------------
91
92 @doc """
93 Log in with an app password, persisting the session at `session_path`.
94 Returns an opaque agent handle (a NIF resource freed by GC).
95 """
96 def login(session_path, identifier, password, appview \\ "", dedup_path \\ ""),
97 do:
98 :rocksky.agent_login(
99 to_bin(session_path),
100 to_bin(identifier),
101 to_bin(password),
102 to_bin(appview),
103 to_bin(dedup_path)
104 )
105
106 @doc "Scrobble a play (fans out to artist/album/song/scrobble). Returns the URIs."
107 def scrobble(agent, track), do: :rocksky.agent_scrobble(agent, track)
108
109 @doc """
110 Scrobble from just a title + artist (album optional): resolve full metadata
111 via matchSong, then fan out. `input` is a map with camelCase string keys:
112 required `"title"`/`"artist"`; optional `"album"`, `"mbId"`, `"isrc"` (match
113 anchors) and `"timestamp"` (scrobbled-at Unix seconds, default now).
114
115 Rocksky.scrobble_match(agent, %{"title" => "Chaser", "artist" => "Calibro 35"})
116 """
117 def scrobble_match(agent, input) when is_map(input),
118 do: :rocksky.agent_scrobble_match(agent, input)
119
120 @doc "Download the caller's repo and (re)build the local dedup index (needs a dedup_path at login)."
121 def sync_repo(agent), do: :rocksky.agent_sync_repo(agent)
122
123 @doc "Keep the local dedup index hydrated from Jetstream in the background."
124 def hydrate_from_jetstream(agent), do: :rocksky.agent_hydrate_from_jetstream(agent)
125
126 @doc "Like a record by strong reference."
127 def like(agent, uri, cid), do: :rocksky.agent_like(agent, to_bin(uri), to_bin(cid))
128
129 @doc "Follow an account by DID."
130 def follow(agent, did), do: :rocksky.agent_follow(agent, to_bin(did))
131
132 @doc "Post a shout on a subject."
133 def shout(agent, subject_uri, subject_cid, message),
134 do: :rocksky.agent_shout(agent, to_bin(subject_uri), to_bin(subject_cid), to_bin(message))
135
136 @doc """
137 Post a shout with an optional GIF/sticker/clip. Pass at least one of `message`
138 / `gif`. `gif` is a map (`"url"` required, plus `"previewUrl"`, `"alt"`,
139 `"width"`, `"height"`) or `nil`.
140 """
141 def shout_with_gif(agent, subject_uri, subject_cid, message, gif \\ nil),
142 do:
143 :rocksky.agent_shout_with_gif(
144 agent,
145 to_bin(subject_uri),
146 to_bin(subject_cid),
147 to_bin(message),
148 gif_arg(gif)
149 )
150
151 @doc "Reply to a shout with an optional GIF/sticker/clip (see `shout_with_gif/5`)."
152 def reply_shout_with_gif(agent, subject_uri, subject_cid, parent_uri, parent_cid, message, gif \\ nil),
153 do:
154 :rocksky.agent_reply_shout_with_gif(
155 agent,
156 to_bin(subject_uri),
157 to_bin(subject_cid),
158 to_bin(parent_uri),
159 to_bin(parent_cid),
160 to_bin(message),
161 gif_arg(gif)
162 )
163
164 @doc "Proactively refresh the session (keep-alive)."
165 def refresh_session(agent), do: :rocksky.agent_refresh_session(agent)
166
167 defp gif_arg(nil), do: :undefined
168 defp gif_arg(gif), do: gif
169
170 defp to_bin(s) when is_binary(s), do: s
171 defp to_bin(s), do: to_string(s)
172end