๐ŸŽง The official command-line interface for Rocksky โ€” a modern, decentralized music tracking and discovery platform built on the AT Protocol.
0
fork

Configure Feed

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

add scrobbles command (display recently scrobbled tracks)

+115 -26
+19
README.md
··· 9 9 - ๐Ÿ“ค Manually scrobble tracks 10 10 - ๐Ÿ› ๏ธ Useful developer tools for integrating Rocksky into your workflows 11 11 12 + ## Run in development 13 + To run the CLI in development mode, install the dependencies: 14 + 15 + ```bash 16 + bun install 17 + ``` 18 + 19 + Then, run the CLI with: 20 + 21 + ```bash 22 + bun run dev --help 23 + ``` 24 + 25 + 12 26 ## Usage 13 27 14 28 ```bash ··· 29 43 rocksky nowplaying 30 44 ``` 31 45 46 + `scrobbles` - Lists all recently scrobbled tracks. 47 + 48 + ```bash 49 + rocksky scrobbles 50 + ```
+3
bun.lock
··· 8 8 "chalk": "^5.4.1", 9 9 "commander": "^13.1.0", 10 10 "cors": "^2.8.5", 11 + "dayjs": "^1.11.13", 11 12 "express": "^5.1.0", 12 13 "md5": "^2.3.0", 13 14 "open": "^10.1.0", ··· 201 202 "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], 202 203 203 204 "crypt": ["crypt@0.0.2", "", {}, "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow=="], 205 + 206 + "dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="], 204 207 205 208 "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], 206 209
+2 -1
package.json
··· 9 9 }, 10 10 "scripts": { 11 11 "test": "echo \"Error: no test specified\" && exit 1", 12 - "dev": "tsx --watch ./src/index.ts", 12 + "dev": "tsx ./src/index.ts", 13 13 "build": "pkgroll && chmod +x ./dist/index.js" 14 14 }, 15 15 "keywords": [ ··· 26 26 "chalk": "^5.4.1", 27 27 "commander": "^13.1.0", 28 28 "cors": "^2.8.5", 29 + "dayjs": "^1.11.13", 29 30 "express": "^5.1.0", 30 31 "md5": "^2.3.0", 31 32 "open": "^10.1.0"
+41 -7
src/client.ts
··· 1 1 export const ROCKSKY_API_URL = "https://api.rocksky.app"; 2 2 3 3 export class RockskyClient { 4 - constructor(private readonly token: string) { 5 - if (!token) { 6 - throw new Error("Token is required to create a RockskyClient instance."); 7 - } 4 + constructor(private readonly token?: string) { 8 5 this.token = token; 9 6 } 10 7 ··· 12 9 const response = await fetch(`${ROCKSKY_API_URL}/profile`, { 13 10 method: "GET", 14 11 headers: { 15 - Authorization: `Bearer ${this.token}`, 12 + Authorization: this.token ? `Bearer ${this.token}` : undefined, 16 13 "Content-Type": "application/json", 17 14 }, 18 15 }); ··· 31 28 { 32 29 method: "GET", 33 30 headers: { 34 - Authorization: `Bearer ${this.token}`, 31 + Authorization: this.token ? `Bearer ${this.token}` : undefined, 35 32 "Content-Type": "application/json", 36 33 }, 37 34 } ··· 52 49 { 53 50 method: "GET", 54 51 headers: { 55 - Authorization: `Bearer ${this.token}`, 52 + Authorization: this.token ? `Bearer ${this.token}` : undefined, 56 53 "Content-Type": "application/json", 57 54 }, 58 55 } ··· 62 59 throw new Error( 63 60 `Failed to fetch now playing data: ${response.statusText}` 64 61 ); 62 + } 63 + 64 + return response.json(); 65 + } 66 + 67 + async scrobbles(did?: string, { skip = 0, limit = 20 } = {}) { 68 + if (did) { 69 + const response = await fetch( 70 + `${ROCKSKY_API_URL}/users/${did}/scrobbles?offset=${skip}&size=${limit}`, 71 + { 72 + method: "GET", 73 + headers: { 74 + Authorization: this.token ? `Bearer ${this.token}` : undefined, 75 + "Content-Type": "application/json", 76 + }, 77 + } 78 + ); 79 + if (!response.ok) { 80 + throw new Error( 81 + `Failed to fetch scrobbles data: ${response.statusText}` 82 + ); 83 + } 84 + return response.json(); 85 + } 86 + 87 + const response = await fetch( 88 + `${ROCKSKY_API_URL}/public/scrobbles?offset=${skip}&size=${limit}`, 89 + { 90 + method: "GET", 91 + headers: { 92 + Authorization: this.token ? `Bearer ${this.token}` : undefined, 93 + "Content-Type": "application/json", 94 + }, 95 + } 96 + ); 97 + if (!response.ok) { 98 + throw new Error(`Failed to fetch scrobbles data: ${response.statusText}`); 65 99 } 66 100 67 101 return response.json();
+11 -18
src/cmd/nowplaying.ts
··· 3 3 import fs from "fs/promises"; 4 4 import os from "os"; 5 5 import path from "path"; 6 - import chalk from "chalk"; 7 6 8 7 export async function nowplaying(did?: string) { 9 8 const tokenPath = path.join(os.homedir(), ".rocksky", "token.json"); ··· 11 10 await fs.access(tokenPath); 12 11 } catch (err) { 13 12 console.error( 14 - `You are not logged in. Please run ${ 15 - chalk.greenBright( 16 - "`rocksky login <username>.bsky.social`", 17 - ) 18 - } first.`, 13 + `You are not logged in. Please run ${chalk.greenBright( 14 + "`rocksky login <username>.bsky.social`" 15 + )} first.` 19 16 ); 20 17 return; 21 18 } ··· 24 21 const { token } = JSON.parse(tokenData); 25 22 if (!token) { 26 23 console.error( 27 - `You are not logged in. Please run ${ 28 - chalk.greenBright( 29 - "`rocksky login <username>.bsky.social`", 30 - ) 31 - } first.`, 24 + `You are not logged in. Please run ${chalk.greenBright( 25 + "`rocksky login <username>.bsky.social`" 26 + )} first.` 32 27 ); 33 28 return; 34 29 } ··· 49 44 50 45 console.log( 51 46 chalk.magenta( 52 - `${nowPlaying.item.name} - ${ 53 - nowPlaying.item.artists 54 - .map((a) => a.name) 55 - .join(", ") 56 - }`, 57 - ), 47 + `${nowPlaying.item.name} - ${nowPlaying.item.artists 48 + .map((a) => a.name) 49 + .join(", ")}` 50 + ) 58 51 ); 59 52 console.log(`${nowPlaying.item.album.name}`); 60 53 } catch (err) { 61 54 console.log(err); 62 55 console.error( 63 - `Failed to fetch now playing data. Please check your token and try again.`, 56 + `Failed to fetch now playing data. Please check your token and try again.` 64 57 ); 65 58 } 66 59 }
+30
src/cmd/scrobbles.ts
··· 1 + import chalk from "chalk"; 2 + import { RockskyClient } from "client"; 3 + import dayjs from "dayjs"; 4 + import relative from "dayjs/plugin/relativeTime.js"; 5 + 6 + dayjs.extend(relative); 7 + 8 + export async function scrobbles(did, { skip, limit }) { 9 + const client = new RockskyClient(); 10 + const scrobbles = await client.scrobbles(did, { skip, limit }); 11 + 12 + for (const scrobble of scrobbles) { 13 + if (did) { 14 + console.log( 15 + `${chalk.bold.magenta(scrobble.title)} ${ 16 + scrobble.artist 17 + } ${chalk.yellow(dayjs(scrobble.created_at + "Z").fromNow())}` 18 + ); 19 + continue; 20 + } 21 + const handle = `@${scrobble.user}`; 22 + console.log( 23 + `${chalk.italic.magentaBright( 24 + handle 25 + )} is listening to ${chalk.bold.magenta(scrobble.title)} ${ 26 + scrobble.artist 27 + } ${chalk.yellow(dayjs(scrobble.date).fromNow())}` 28 + ); 29 + } 30 + }
+9
src/index.ts
··· 1 1 #!/usr/bin/env node 2 2 3 3 import { nowplaying } from "cmd/nowplaying"; 4 + import { scrobbles } from "cmd/scrobbles"; 4 5 import { whoami } from "cmd/whoami"; 5 6 import { Command } from "commander"; 6 7 import version from "../package.json" assert { type: "json" }; ··· 34 35 ) 35 36 .description("Get the currently playing track.") 36 37 .action(nowplaying); 38 + 39 + program 40 + .command("scrobbles") 41 + .option("-s, --skip <number>", "Number of scrobbles to skip") 42 + .option("-l, --limit <number>", "Number of scrobbles to limit") 43 + .argument("[did]", "The DID or handle of the user to get the scrobbles for.") 44 + .description("Display recently played tracks.") 45 + .action(scrobbles); 37 46 38 47 program.parse(process.argv);