A decentralized music tracking and discovery platform built on AT Protocol 馃幍 rocksky.app
spotify atproto lastfm musicbrainz scrobbling listenbrainz
96
fork

Configure Feed

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

at feat/pgpull 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}