A decentralized music tracking and discovery platform built on AT Protocol 馃幍
listenbrainz spotify atproto lastfm musicbrainz scrobbling
at setup-tracing 324 lines 9.0 kB view raw
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}