🎧 The official command-line interface for Rocksky — a modern, decentralized music tracking and discovery platform built on the AT Protocol.

add 'create apikey' and 'scrobble' commands

+214
+6
README.md
··· 78 ```bash 79 rocksky tracks [did] 80 ```
··· 78 ```bash 79 rocksky tracks [did] 80 ``` 81 + 82 + `scrobble` - Manually scrobbles a track. 83 + 84 + ```bash 85 + rocksky scrobble "Karma Police" "Radiohead" 86 + ```
+76
src/client.ts
··· 245 } 246 return response.json(); 247 } 248 }
··· 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 }
+45
src/cmd/create.ts
···
··· 1 + import chalk from "chalk"; 2 + import { RockskyClient } from "client"; 3 + import fs from "fs/promises"; 4 + import os from "os"; 5 + import path from "path"; 6 + 7 + export async function createApiKey(name, { description }) { 8 + const tokenPath = path.join(os.homedir(), ".rocksky", "token.json"); 9 + try { 10 + await fs.access(tokenPath); 11 + } catch (err) { 12 + console.error( 13 + `You are not logged in. Please run ${chalk.greenBright( 14 + "`rocksky login <username>.bsky.social`" 15 + )} first.` 16 + ); 17 + return; 18 + } 19 + 20 + const tokenData = await fs.readFile(tokenPath, "utf-8"); 21 + const { token } = JSON.parse(tokenData); 22 + if (!token) { 23 + console.error( 24 + `You are not logged in. Please run ${chalk.greenBright( 25 + "`rocksky login <username>.bsky.social`" 26 + )} first.` 27 + ); 28 + return; 29 + } 30 + 31 + const client = new RockskyClient(token); 32 + const apikey = await client.createApiKey(name, description); 33 + if (!apikey) { 34 + console.error(`Failed to create API key. Please try again later.`); 35 + return; 36 + } 37 + 38 + console.log(`API key created successfully!`); 39 + console.log(`Name: ${chalk.greenBright(apikey.name)}`); 40 + if (apikey.description) { 41 + console.log(`Description: ${chalk.greenBright(apikey.description)}`); 42 + } 43 + console.log(`Key: ${chalk.greenBright(apikey.api_key)}`); 44 + console.log(`Secret: ${chalk.greenBright(apikey.shared_secret)}`); 45 + }
+69
src/cmd/scrobble.ts
···
··· 1 + import chalk from "chalk"; 2 + import { RockskyClient } from "client"; 3 + import fs from "fs/promises"; 4 + import md5 from "md5"; 5 + import os from "os"; 6 + import path from "path"; 7 + 8 + export async function scrobble(track, artist, { timestamp }) { 9 + const tokenPath = path.join(os.homedir(), ".rocksky", "token.json"); 10 + try { 11 + await fs.access(tokenPath); 12 + } catch (err) { 13 + console.error( 14 + `You are not logged in. Please run ${chalk.greenBright( 15 + "`rocksky login <username>.bsky.social`" 16 + )} first.` 17 + ); 18 + return; 19 + } 20 + 21 + const tokenData = await fs.readFile(tokenPath, "utf-8"); 22 + const { token } = JSON.parse(tokenData); 23 + if (!token) { 24 + console.error( 25 + `You are not logged in. Please run ${chalk.greenBright( 26 + "`rocksky login <username>.bsky.social`" 27 + )} first.` 28 + ); 29 + return; 30 + } 31 + 32 + const client = new RockskyClient(token); 33 + const apikeys = await client.getApiKeys(); 34 + 35 + if (!apikeys || apikeys.length === 0 || !apikeys[0].enabled) { 36 + console.error( 37 + `You don't have any API keys. Please create one using ${chalk.greenBright( 38 + "`rocksky create apikey`" 39 + )} command.` 40 + ); 41 + return; 42 + } 43 + 44 + const signature = md5( 45 + `api_key${ 46 + apikeys[0].apiKey 47 + }artist[0]${artist}methodtrack.scrobblesk${token}timestamp[0]${ 48 + timestamp || Math.floor(Date.now() / 1000) 49 + }track[0]${track}${apikeys[0].sharedSecret}` 50 + ); 51 + 52 + const response = await client.scrobble( 53 + apikeys[0].apiKey, 54 + signature, 55 + track, 56 + artist, 57 + timestamp 58 + ); 59 + 60 + console.log( 61 + `Scrobbled ${chalk.greenBright(track)} by ${chalk.greenBright( 62 + artist 63 + )} at ${chalk.greenBright( 64 + new Date( 65 + (timestamp || Math.floor(Date.now() / 1000)) * 1000 66 + ).toLocaleString() 67 + )}` 68 + ); 69 + }
+18
src/index.ts
··· 2 3 import { albums } from "cmd/albums"; 4 import { artists } from "cmd/artists"; 5 import { nowplaying } from "cmd/nowplaying"; 6 import { scrobbles } from "cmd/scrobbles"; 7 import { search } from "cmd/search"; 8 import { stats } from "cmd/stats"; ··· 89 .argument("[did]", "The DID or handle of the user to get tracks for.") 90 .description("Get the user's top tracks.") 91 .action(tracks); 92 93 program.parse(process.argv);
··· 2 3 import { albums } from "cmd/albums"; 4 import { artists } from "cmd/artists"; 5 + import { createApiKey } from "cmd/create"; 6 import { nowplaying } from "cmd/nowplaying"; 7 + import { scrobble } from "cmd/scrobble"; 8 import { scrobbles } from "cmd/scrobbles"; 9 import { search } from "cmd/search"; 10 import { stats } from "cmd/stats"; ··· 91 .argument("[did]", "The DID or handle of the user to get tracks for.") 92 .description("Get the user's top tracks.") 93 .action(tracks); 94 + 95 + program 96 + .command("scrobble") 97 + .argument("<track>", "The title of the track") 98 + .argument("<artist>", "The artist of the track") 99 + .option("-t, --timestamp <timestamp>", "The timestamp of the scrobble") 100 + .description("Scrobble a track to your profile.") 101 + .action(scrobble); 102 + 103 + program 104 + .command("create") 105 + .command("apikey") 106 + .argument("<name>", "The name of the API key") 107 + .option("-d, --description <description>", "The description of the API key") 108 + .description("Create a new API key.") 109 + .action(createApiKey); 110 111 program.parse(process.argv);