A decentralized music tracking and discovery platform built on AT Protocol 馃幍
listenbrainz
spotify
atproto
lastfm
musicbrainz
scrobbling
1import fs from "fs";
2import os from "os";
3import path from "path";
4
5export const ROCKSKY_API_URL = "https://api.rocksky.app";
6
7export class RockskyClient {
8 constructor(private readonly token?: string) {
9 this.token = token;
10 }
11
12 async getCurrentUser() {
13 const response = await fetch(`${ROCKSKY_API_URL}/profile`, {
14 method: "GET",
15 headers: {
16 Authorization: this.token ? `Bearer ${this.token}` : undefined,
17 "Content-Type": "application/json",
18 },
19 });
20
21 if (!response.ok) {
22 throw new Error(`Failed to fetch user data: ${response.statusText}`);
23 }
24
25 return response.json();
26 }
27
28 async getSpotifyNowPlaying(did?: string) {
29 const response = await fetch(
30 `${ROCKSKY_API_URL}/spotify/currently-playing` +
31 (did ? `?did=${did}` : ""),
32 {
33 method: "GET",
34 headers: {
35 Authorization: this.token ? `Bearer ${this.token}` : undefined,
36 "Content-Type": "application/json",
37 },
38 }
39 );
40
41 if (!response.ok) {
42 throw new Error(
43 `Failed to fetch now playing data: ${response.statusText}`
44 );
45 }
46
47 return response.json();
48 }
49
50 async getNowPlaying(did?: string) {
51 const response = await fetch(
52 `${ROCKSKY_API_URL}/now-playing` + (did ? `?did=${did}` : ""),
53 {
54 method: "GET",
55 headers: {
56 Authorization: this.token ? `Bearer ${this.token}` : undefined,
57 "Content-Type": "application/json",
58 },
59 }
60 );
61
62 if (!response.ok) {
63 throw new Error(
64 `Failed to fetch now playing data: ${response.statusText}`
65 );
66 }
67
68 return response.json();
69 }
70
71 async scrobbles(did?: string, { skip = 0, limit = 20 } = {}) {
72 if (did) {
73 const response = await fetch(
74 `${ROCKSKY_API_URL}/users/${did}/scrobbles?offset=${skip}&size=${limit}`,
75 {
76 method: "GET",
77 headers: {
78 Authorization: this.token ? `Bearer ${this.token}` : undefined,
79 "Content-Type": "application/json",
80 },
81 }
82 );
83 if (!response.ok) {
84 throw new Error(
85 `Failed to fetch scrobbles data: ${response.statusText}`
86 );
87 }
88 return response.json();
89 }
90
91 const response = await fetch(
92 `${ROCKSKY_API_URL}/public/scrobbles?offset=${skip}&size=${limit}`,
93 {
94 method: "GET",
95 headers: {
96 Authorization: this.token ? `Bearer ${this.token}` : undefined,
97 "Content-Type": "application/json",
98 },
99 }
100 );
101 if (!response.ok) {
102 throw new Error(`Failed to fetch scrobbles data: ${response.statusText}`);
103 }
104
105 return response.json();
106 }
107
108 async search(query: string, { size }) {
109 const response = await fetch(
110 `${ROCKSKY_API_URL}/search?q=${query}&size=${size}`,
111 {
112 method: "GET",
113 headers: {
114 Authorization: this.token ? `Bearer ${this.token}` : undefined,
115 "Content-Type": "application/json",
116 },
117 }
118 );
119
120 if (!response.ok) {
121 throw new Error(`Failed to fetch search data: ${response.statusText}`);
122 }
123
124 return response.json();
125 }
126
127 async stats(did?: string) {
128 if (!did) {
129 const didFile = path.join(os.homedir(), ".rocksky", "did");
130 try {
131 await fs.promises.access(didFile);
132 did = await fs.promises.readFile(didFile, "utf-8");
133 } catch (err) {
134 const user = await this.getCurrentUser();
135 did = user.did;
136 const didPath = path.join(os.homedir(), ".rocksky");
137 fs.promises.mkdir(didPath, { recursive: true });
138 await fs.promises.writeFile(didFile, did);
139 }
140 }
141
142 const response = await fetch(`${ROCKSKY_API_URL}/users/${did}/stats`, {
143 method: "GET",
144 headers: {
145 "Content-Type": "application/json",
146 },
147 });
148
149 if (!response.ok) {
150 throw new Error(`Failed to fetch stats data: ${response.statusText}`);
151 }
152
153 return response.json();
154 }
155
156 async getArtists(did?: string, { skip = 0, limit = 20 } = {}) {
157 if (!did) {
158 const didFile = path.join(os.homedir(), ".rocksky", "did");
159 try {
160 await fs.promises.access(didFile);
161 did = await fs.promises.readFile(didFile, "utf-8");
162 } catch (err) {
163 const user = await this.getCurrentUser();
164 did = user.did;
165 const didPath = path.join(os.homedir(), ".rocksky");
166 fs.promises.mkdir(didPath, { recursive: true });
167 await fs.promises.writeFile(didFile, did);
168 }
169 }
170
171 const response = await fetch(
172 `${ROCKSKY_API_URL}/users/${did}/artists?offset=${skip}&size=${limit}`,
173 {
174 method: "GET",
175 headers: {
176 Authorization: this.token ? `Bearer ${this.token}` : undefined,
177 "Content-Type": "application/json",
178 },
179 }
180 );
181 if (!response.ok) {
182 throw new Error(`Failed to fetch artists data: ${response.statusText}`);
183 }
184 return response.json();
185 }
186
187 async getAlbums(did?: string, { skip = 0, limit = 20 } = {}) {
188 if (!did) {
189 const didFile = path.join(os.homedir(), ".rocksky", "did");
190 try {
191 await fs.promises.access(didFile);
192 did = await fs.promises.readFile(didFile, "utf-8");
193 } catch (err) {
194 const user = await this.getCurrentUser();
195 did = user.did;
196 const didPath = path.join(os.homedir(), ".rocksky");
197 fs.promises.mkdir(didPath, { recursive: true });
198 await fs.promises.writeFile(didFile, did);
199 }
200 }
201
202 const response = await fetch(
203 `${ROCKSKY_API_URL}/users/${did}/albums?offset=${skip}&size=${limit}`,
204 {
205 method: "GET",
206 headers: {
207 Authorization: this.token ? `Bearer ${this.token}` : undefined,
208 "Content-Type": "application/json",
209 },
210 }
211 );
212 if (!response.ok) {
213 throw new Error(`Failed to fetch albums data: ${response.statusText}`);
214 }
215 return response.json();
216 }
217
218 async getTracks(did?: string, { skip = 0, limit = 20 } = {}) {
219 if (!did) {
220 const didFile = path.join(os.homedir(), ".rocksky", "did");
221 try {
222 await fs.promises.access(didFile);
223 did = await fs.promises.readFile(didFile, "utf-8");
224 } catch (err) {
225 const user = await this.getCurrentUser();
226 did = user.did;
227 const didPath = path.join(os.homedir(), ".rocksky");
228 fs.promises.mkdir(didPath, { recursive: true });
229 await fs.promises.writeFile(didFile, did);
230 }
231 }
232
233 const response = await fetch(
234 `${ROCKSKY_API_URL}/users/${did}/tracks?offset=${skip}&size=${limit}`,
235 {
236 method: "GET",
237 headers: {
238 Authorization: this.token ? `Bearer ${this.token}` : undefined,
239 "Content-Type": "application/json",
240 },
241 }
242 );
243 if (!response.ok) {
244 throw new Error(`Failed to fetch tracks data: ${response.statusText}`);
245 }
246 return response.json();
247 }
248
249 async scrobble(api_key, api_sig, track, artist, timestamp) {
250 const tokenPath = path.join(os.homedir(), ".rocksky", "token.json");
251 try {
252 await fs.promises.access(tokenPath);
253 } catch (err) {
254 console.error(
255 `You are not logged in. Please run the login command first.`
256 );
257 return;
258 }
259 const tokenData = await fs.promises.readFile(tokenPath, "utf-8");
260 const { token: sk } = JSON.parse(tokenData);
261 const response = await fetch("https://audioscrobbler.rocksky.app/2.0", {
262 method: "POST",
263 headers: {
264 "Content-Type": "application/x-www-form-urlencoded",
265 },
266 body: new URLSearchParams({
267 method: "track.scrobble",
268 "track[0]": track,
269 "artist[0]": artist,
270 "timestamp[0]": timestamp || Math.floor(Date.now() / 1000),
271 api_key,
272 api_sig,
273 sk,
274 format: "json",
275 }),
276 });
277
278 if (!response.ok) {
279 throw new Error(
280 `Failed to scrobble track: ${
281 response.statusText
282 } ${await response.text()}`
283 );
284 }
285
286 return response.json();
287 }
288
289 async getApiKeys() {
290 const response = await fetch(`${ROCKSKY_API_URL}/apikeys`, {
291 method: "GET",
292 headers: {
293 Authorization: this.token ? `Bearer ${this.token}` : undefined,
294 "Content-Type": "application/json",
295 },
296 });
297
298 if (!response.ok) {
299 throw new Error(`Failed to fetch API keys: ${response.statusText}`);
300 }
301
302 return response.json();
303 }
304
305 async createApiKey(name: string, description?: string) {
306 const response = await fetch(`${ROCKSKY_API_URL}/apikeys`, {
307 method: "POST",
308 headers: {
309 Authorization: this.token ? `Bearer ${this.token}` : undefined,
310 "Content-Type": "application/json",
311 },
312 body: JSON.stringify({
313 name,
314 description,
315 }),
316 });
317
318 if (!response.ok) {
319 throw new Error(`Failed to create API key: ${response.statusText}`);
320 }
321
322 return response.json();
323 }
324}