···88- ๐ See your recent scrobbles
99- ๐ค Manually scrobble tracks
1010- ๐ ๏ธ Useful developer tools for integrating Rocksky into your workflows
1111+- ๐ค MCP Server
1212+1313+## Table of Contents
1414+- [Installation](#installation)
1515+- [Run in development](#run-in-development)
1616+- [Usage](#usage)
1717+- [Available Commands](#available-commands)
1818+ - [login](#login)
1919+ - [nowplaying](#nowplaying)
2020+ - [scrobbles](#scrobbles)
2121+ - [search](#search)
2222+ - [stats](#stats)
2323+ - [artists](#artists)
2424+ - [albums](#albums)
2525+ - [tracks](#tracks)
2626+ - [scrobble](#scrobble)
2727+ - [mcp](#mcp)
2828+- [Rocksky MCP Server Tools](#rocksky-mcp-server-tools)
2929+11301231## Installation
1332···96115```bash
97116rocksky scrobble "Karma Police" "Radiohead"
98117```
118118+119119+`whoami` - Displays the current user's information.
120120+121121+```bash
122122+rocksky whoami
123123+```
124124+125125+`mcp` - Starts the Rocksky MCP server.
126126+127127+```bash
128128+rocksky mcp
129129+```
130130+131131+## Rocksky MCP Server Tools
132132+133133+Here is a list of tools provided by the Rocksky MCP server:
134134+135135+### whoami
136136+137137+Get the current user's information.
138138+139139+### nowplaying
140140+141141+Get the currently playing track.
142142+143143+**Parameters:**
144144+145145+- `did` (optional): The DID or handle of the user to get the now playing track for. If not provided, it defaults to the current user.
146146+147147+**Example:**
148148+```json
149149+{
150150+ "name": "nowplaying",
151151+ "args": {
152152+ "did": "did:plc:7vdlgi2bflelz7mmuxoqjfcr"
153153+ }
154154+}
155155+```
156156+157157+**Returns:**
158158+The currently playing track for the specified user.
159159+160160+### scrobbles
161161+162162+Display recently played tracks (recent scrobbles).
163163+164164+**Parameters:**
165165+- `did` (optional): The DID or handle of the user to get scrobbles for. If not provided, it returns all recent scrobbles from Rocksky.
166166+167167+**Example:**
168168+```json
169169+{
170170+ "name": "scrobbles",
171171+ "args": {
172172+ "did": "did:plc:7vdlgi2bflelz7mmuxoqjfcr"
173173+ }
174174+}
175175+```
176176+177177+**Returns:**
178178+A list of recently played tracks for the specified user.
179179+180180+### my-scrobbles
181181+182182+Display recently played tracks (recent scrobbles) for the current user.
183183+184184+**Example:**
185185+```json
186186+{
187187+ "name": "my-scrobbles"
188188+}
189189+```
190190+191191+**Returns:**
192192+A list of recently played tracks for the current user.
193193+194194+### search
195195+Search for tracks, albums, artists, or Rocksky users.
196196+197197+**Parameters:**
198198+- `query`: The search query string.
199199+- `limit` (optional): The maximum number of results to return. Defaults to 10.
200200+- `albums` (optional): If true, search for albums. Defaults to false.
201201+- `artists` (optional): If true, search for artists. Defaults to false.
202202+- `tracks` (optional): If true, search for tracks. Defaults to false.
203203+- `users` (optional): If true, search for Rocksky users. Defaults to false.
204204+205205+**Example:**
206206+```json
207207+{
208208+ "name": "search",
209209+ "args": {
210210+ "query": "Radiohead",
211211+ "limit": 5,
212212+ "albums": false,
213213+ "artists": false,
214214+ "tracks": false,
215215+ "users": false
216216+ }
217217+}
218218+```
219219+220220+**Returns:**
221221+A list of search results based on the specified query and filters.
222222+223223+### artists
224224+List the user's top artists or current user's top artists if no `did` is provided.
225225+226226+**Parameters:**
227227+- `did` (optional): The DID or handle of the user to get top artists for. If not provided, it defaults to the current user.
228228+- `limit` (optional): The maximum number of artists to return. Defaults to 20.
229229+230230+**Example:**
231231+```json
232232+{
233233+ "name": "artists",
234234+ "args": {
235235+ "did": "did:plc:7vdlgi2bflelz7mmuxoqjfcr",
236236+ "limit": 20
237237+ }
238238+}
239239+```
240240+241241+**Returns:**
242242+A list of the user's top artists, including their names and play counts.
243243+244244+### albums
245245+List the user's top albums or current user's top albums if no `did` is provided.
246246+247247+**Parameters:**
248248+- `did` (optional): The DID or handle of the user to get top albums for. If not provided, it defaults to the current user.
249249+- `limit` (optional): The maximum number of albums to return. Defaults to 20.
250250+251251+**Example:**
252252+```json
253253+{
254254+ "name": "albums",
255255+ "args": {
256256+ "did": "did:plc:7vdlgi2bflelz7mmuxoqjfcr",
257257+ "limit": 20
258258+ }
259259+}
260260+```
261261+262262+**Returns:**
263263+A list of the user's top albums, including their names and play counts.
264264+265265+### tracks
266266+List the user's top tracks or current user's top tracks if no `did` is provided.
267267+268268+**Parameters:**
269269+- `did` (optional): The DID or handle of the user to get top tracks for. If not provided, it defaults to the current user.
270270+- `limit` (optional): The maximum number of tracks to return. Defaults to 20.
271271+272272+**Example:**
273273+```json
274274+{
275275+ "name": "tracks",
276276+ "args": {
277277+ "did": "did:plc:7vdlgi2bflelz7mmuxoqjfcr",
278278+ "limit": 20
279279+ }
280280+}
281281+```
282282+283283+**Returns:**
284284+A list of the user's top tracks, including their names and play counts.
285285+286286+### stats
287287+Display the user's Rocksky account statistics or current user's statistics if no `did` is provided.
288288+289289+**Parameters:**
290290+- `did` (optional): The DID or handle of the user to get statistics for. If not provided, it defaults to the current user.
291291+292292+**Example:**
293293+```json
294294+{
295295+ "name": "stats",
296296+ "args": {
297297+ "did": "did:plc:7vdlgi2bflelz7mmuxoqjfcr"
298298+ }
299299+}
300300+```
301301+302302+### create-apikey
303303+Create a new API key for the current user.
304304+305305+**Parameters:**
306306+- `name`: The name of the API key.
307307+- `description` (optional): A description of the API key.
308308+309309+**Example:**
310310+```json
311311+{
312312+ "name": "create-apikey",
313313+ "args": {
314314+ "name": "My API Key",
315315+ "description": "This is my API key."
316316+ }
317317+}
318318+```
319319+320320+**Returns:**
321321+A confirmation message indicating that the API key was created successfully.
322322+323323+324324+
+194
TOOLS.md
···11+# Rocksky MCP Tools
22+33+This document provides a comprehensive list of all tools available in the Rocksky MCP server.
44+55+## whoami
66+77+Get the current user's information.
88+99+## nowplaying
1010+1111+Get the currently playing track.
1212+1313+**Parameters:**
1414+1515+- `did` (optional): The DID or handle of the user to get the now playing track for. If not provided, it defaults to the current user.
1616+1717+**Example:**
1818+```json
1919+{
2020+ "name": "nowplaying",
2121+ "args": {
2222+ "did": "did:plc:7vdlgi2bflelz7mmuxoqjfcr"
2323+ }
2424+}
2525+```
2626+2727+**Returns:**
2828+The currently playing track for the specified user.
2929+3030+## scrobbles
3131+3232+Display recently played tracks (recent scrobbles).
3333+3434+**Parameters:**
3535+- `did` (optional): The DID or handle of the user to get scrobbles for. If not provided, it returns all recent scrobbles from Rocksky.
3636+3737+**Example:**
3838+```json
3939+{
4040+ "name": "scrobbles",
4141+ "args": {
4242+ "did": "did:plc:7vdlgi2bflelz7mmuxoqjfcr"
4343+ }
4444+}
4545+```
4646+4747+**Returns:**
4848+A list of recently played tracks for the specified user.
4949+5050+## my-scrobbles
5151+5252+Display recently played tracks (recent scrobbles) for the current user.
5353+5454+**Example:**
5555+```json
5656+{
5757+ "name": "my-scrobbles"
5858+}
5959+```
6060+6161+**Returns:**
6262+A list of recently played tracks for the current user.
6363+6464+## search
6565+Search for tracks, albums, artists, or Rocksky users.
6666+6767+**Parameters:**
6868+- `query`: The search query string.
6969+- `limit` (optional): The maximum number of results to return. Defaults to 10.
7070+- `albums` (optional): If true, search for albums. Defaults to false.
7171+- `artists` (optional): If true, search for artists. Defaults to false.
7272+- `tracks` (optional): If true, search for tracks. Defaults to false.
7373+- `users` (optional): If true, search for Rocksky users. Defaults to false.
7474+7575+**Example:**
7676+```json
7777+{
7878+ "name": "search",
7979+ "args": {
8080+ "query": "Radiohead",
8181+ "limit": 5,
8282+ "albums": false,
8383+ "artists": false,
8484+ "tracks": false,
8585+ "users": false
8686+ }
8787+}
8888+```
8989+9090+**Returns:**
9191+A list of search results based on the specified query and filters.
9292+9393+## artists
9494+List the user's top artists or current user's top artists if no `did` is provided.
9595+9696+**Parameters:**
9797+- `did` (optional): The DID or handle of the user to get top artists for. If not provided, it defaults to the current user.
9898+- `limit` (optional): The maximum number of artists to return. Defaults to 20.
9999+100100+**Example:**
101101+```json
102102+{
103103+ "name": "artists",
104104+ "args": {
105105+ "did": "did:plc:7vdlgi2bflelz7mmuxoqjfcr",
106106+ "limit": 20
107107+ }
108108+}
109109+```
110110+111111+**Returns:**
112112+A list of the user's top artists, including their names and play counts.
113113+114114+## albums
115115+List the user's top albums or current user's top albums if no `did` is provided.
116116+117117+**Parameters:**
118118+- `did` (optional): The DID or handle of the user to get top albums for. If not provided, it defaults to the current user.
119119+- `limit` (optional): The maximum number of albums to return. Defaults to 20.
120120+121121+**Example:**
122122+```json
123123+{
124124+ "name": "albums",
125125+ "args": {
126126+ "did": "did:plc:7vdlgi2bflelz7mmuxoqjfcr",
127127+ "limit": 20
128128+ }
129129+}
130130+```
131131+132132+**Returns:**
133133+A list of the user's top albums, including their names and play counts.
134134+135135+## tracks
136136+List the user's top tracks or current user's top tracks if no `did` is provided.
137137+138138+**Parameters:**
139139+- `did` (optional): The DID or handle of the user to get top tracks for. If not provided, it defaults to the current user.
140140+- `limit` (optional): The maximum number of tracks to return. Defaults to 20.
141141+142142+**Example:**
143143+```json
144144+{
145145+ "name": "tracks",
146146+ "args": {
147147+ "did": "did:plc:7vdlgi2bflelz7mmuxoqjfcr",
148148+ "limit": 20
149149+ }
150150+}
151151+```
152152+153153+**Returns:**
154154+A list of the user's top tracks, including their names and play counts.
155155+156156+## stats
157157+Display the user's Rocksky account statistics or current user's statistics if no `did` is provided.
158158+159159+**Parameters:**
160160+- `did` (optional): The DID or handle of the user to get statistics for. If not provided, it defaults to the current user.
161161+162162+**Example:**
163163+```json
164164+{
165165+ "name": "stats",
166166+ "args": {
167167+ "did": "did:plc:7vdlgi2bflelz7mmuxoqjfcr"
168168+ }
169169+}
170170+```
171171+172172+## create-apikey
173173+Create a new API key for the current user.
174174+175175+**Parameters:**
176176+- `name`: The name of the API key.
177177+- `description` (optional): A description of the API key.
178178+179179+**Example:**
180180+```json
181181+{
182182+ "name": "create-apikey",
183183+ "args": {
184184+ "name": "My API Key",
185185+ "description": "This is my API key."
186186+ }
187187+}
188188+```
189189+190190+**Returns:**
191191+A confirmation message indicating that the API key was created successfully.
192192+193193+194194+
···11+import { rockskyMcpServer } from "mcp";
22+33+export function mcp() {
44+ rockskyMcpServer.run().catch((error) => {
55+ console.error("Failed to run Rocksky MCP server", { error });
66+ process.exit(1);
77+ });
88+}
+10-1
src/index.ts
···11#!/usr/bin/env node
2233+import chalk from "chalk";
34import { albums } from "cmd/albums";
45import { artists } from "cmd/artists";
56import { createApiKey } from "cmd/create";
77+import { mcp } from "cmd/mcp";
68import { nowplaying } from "cmd/nowplaying";
79import { scrobble } from "cmd/scrobble";
810import { scrobbles } from "cmd/scrobbles";
···1921program
2022 .name("rocksky")
2123 .description(
2222- "Command-line interface for Rocksky โ scrobble tracks, view stats, and manage your listening history."
2424+ `Command-line interface for Rocksky (${chalk.underline(
2525+ "https://rocksky.app"
2626+ )}) โ scrobble tracks, view stats, and manage your listening history.`
2327 )
2428 .version(version.version);
2529···108112 .option("-d, --description <description>", "the description of the API key")
109113 .description("create a new API key.")
110114 .action(createApiKey);
115115+116116+program
117117+ .command("mcp")
118118+ .description("Starts an MCP server to use with Claude or other LLMs.")
119119+ .action(mcp);
111120112121program.parse(process.argv);
+269
src/mcp/index.ts
···11+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
33+import { RockskyClient } from "client";
44+import { z } from "zod";
55+import { albums } from "./tools/albums";
66+import { artists } from "./tools/artists";
77+import { createApiKey } from "./tools/create";
88+import { myscrobbles } from "./tools/myscrobbles";
99+import { nowplaying } from "./tools/nowplaying";
1010+import { scrobbles } from "./tools/scrobbles";
1111+import { search } from "./tools/search";
1212+import { stats } from "./tools/stats";
1313+import { tracks } from "./tools/tracks";
1414+import { whoami } from "./tools/whoami";
1515+1616+class RockskyMcpServer {
1717+ private readonly server: McpServer;
1818+ private readonly client: RockskyClient;
1919+2020+ constructor() {
2121+ this.server = new McpServer({
2222+ name: "rocksky-mcp",
2323+ version: "0.1.0",
2424+ });
2525+ const client = new RockskyClient();
2626+ this.setupTools();
2727+ }
2828+2929+ private setupTools() {
3030+ this.server.tool("whoami", "get the current logged-in user.", async () => {
3131+ return {
3232+ content: [
3333+ {
3434+ type: "text",
3535+ text: await whoami(),
3636+ },
3737+ ],
3838+ };
3939+ });
4040+4141+ this.server.tool(
4242+ "nowplaying",
4343+ "get the currently playing track.",
4444+ {
4545+ did: z
4646+ .string()
4747+ .optional()
4848+ .describe(
4949+ "the DID or handle of the user to get the now playing track for."
5050+ ),
5151+ },
5252+ async ({ did }) => {
5353+ return {
5454+ content: [
5555+ {
5656+ type: "text",
5757+ text: await nowplaying(did),
5858+ },
5959+ ],
6060+ };
6161+ }
6262+ );
6363+6464+ this.server.tool(
6565+ "scrobbles",
6666+ "display recently played tracks (recent scrobbles).",
6767+ {
6868+ did: z
6969+ .string()
7070+ .optional()
7171+ .describe("the DID or handle of the user to get the scrobbles for."),
7272+ skip: z.number().optional().describe("number of scrobbles to skip"),
7373+ limit: z.number().optional().describe("number of scrobbles to limit"),
7474+ },
7575+ async ({ did, skip = 0, limit = 10 }) => {
7676+ return {
7777+ content: [
7878+ {
7979+ type: "text",
8080+ text: await scrobbles(did, { skip, limit }),
8181+ },
8282+ ],
8383+ };
8484+ }
8585+ );
8686+8787+ this.server.tool(
8888+ "my-scrobbles",
8989+ "display my recently played tracks (recent scrobbles).",
9090+ {
9191+ skip: z.number().optional().describe("number of scrobbles to skip"),
9292+ limit: z.number().optional().describe("number of scrobbles to limit"),
9393+ },
9494+ async ({ skip = 0, limit = 10 }) => {
9595+ return {
9696+ content: [
9797+ {
9898+ type: "text",
9999+ text: await myscrobbles({ skip, limit }),
100100+ },
101101+ ],
102102+ };
103103+ }
104104+ );
105105+106106+ this.server.tool(
107107+ "search",
108108+ "search for tracks, artists, albums or users.",
109109+ {
110110+ query: z
111111+ .string()
112112+ .describe("the search query, e.g., artist, album, title or account"),
113113+ limit: z.number().optional().describe("number of results to limit"),
114114+ albums: z.boolean().optional().describe("search for albums"),
115115+ tracks: z.boolean().optional().describe("search for tracks"),
116116+ users: z.boolean().optional().describe("search for users"),
117117+ artists: z.boolean().optional().describe("search for artists"),
118118+ },
119119+ async ({
120120+ query,
121121+ limit = 10,
122122+ albums = false,
123123+ tracks = false,
124124+ users = false,
125125+ artists = false,
126126+ }) => {
127127+ return {
128128+ content: [
129129+ {
130130+ type: "text",
131131+ text: await search(query, {
132132+ limit,
133133+ albums,
134134+ tracks,
135135+ users,
136136+ artists,
137137+ }),
138138+ },
139139+ ],
140140+ };
141141+ }
142142+ );
143143+144144+ this.server.tool(
145145+ "artists",
146146+ "get the user's top artists or current user's artists if no did is provided.",
147147+ {
148148+ did: z
149149+ .string()
150150+ .optional()
151151+ .describe("the DID or handle of the user to get artists for."),
152152+153153+ limit: z.number().optional().describe("number of results to limit"),
154154+ },
155155+ async ({ did, limit }) => {
156156+ return {
157157+ content: [
158158+ {
159159+ type: "text",
160160+ text: await artists(did, { skip: 0, limit }),
161161+ },
162162+ ],
163163+ };
164164+ }
165165+ );
166166+167167+ this.server.tool(
168168+ "albums",
169169+ "get the user's top albums or current user's albums if no did is provided.",
170170+ {
171171+ did: z
172172+ .string()
173173+ .optional()
174174+ .describe("the DID or handle of the user to get albums for."),
175175+ limit: z.number().optional().describe("number of results to limit"),
176176+ },
177177+ async ({ did, limit }) => {
178178+ return {
179179+ content: [
180180+ {
181181+ type: "text",
182182+ text: await albums(did, { skip: 0, limit }),
183183+ },
184184+ ],
185185+ };
186186+ }
187187+ );
188188+189189+ this.server.tool(
190190+ "tracks",
191191+ "get the user's top tracks or current user's tracks if no did is provided.",
192192+ {
193193+ did: z
194194+ .string()
195195+ .optional()
196196+ .describe("the DID or handle of the user to get tracks for."),
197197+ limit: z.number().optional().describe("number of results to limit"),
198198+ },
199199+ async ({ did, limit }) => {
200200+ return {
201201+ content: [
202202+ {
203203+ type: "text",
204204+ text: await tracks(did, { skip: 0, limit }),
205205+ },
206206+ ],
207207+ };
208208+ }
209209+ );
210210+211211+ this.server.tool(
212212+ "stats",
213213+ "get the user's listening stats or current user's stats if no did is provided.",
214214+ {
215215+ did: z
216216+ .string()
217217+ .optional()
218218+ .describe("the DID or handle of the user to get stats for."),
219219+ },
220220+ async ({ did }) => {
221221+ return {
222222+ content: [
223223+ {
224224+ type: "text",
225225+ text: await stats(did),
226226+ },
227227+ ],
228228+ };
229229+ }
230230+ );
231231+232232+ this.server.tool(
233233+ "create-apikey",
234234+ "create an API key.",
235235+ {
236236+ name: z.string().describe("the name of the API key"),
237237+ description: z
238238+ .string()
239239+ .optional()
240240+ .describe("the description of the API key"),
241241+ },
242242+ async ({ name, description }) => {
243243+ return {
244244+ content: [
245245+ {
246246+ type: "text",
247247+ text: await createApiKey(name, { description }),
248248+ },
249249+ ],
250250+ };
251251+ }
252252+ );
253253+ }
254254+255255+ async run() {
256256+ const stdioTransport = new StdioServerTransport();
257257+ try {
258258+ await this.server.connect(stdioTransport);
259259+ } catch (error) {
260260+ process.exit(1);
261261+ }
262262+ }
263263+264264+ public getServer(): McpServer {
265265+ return this.server;
266266+ }
267267+}
268268+269269+export const rockskyMcpServer = new RockskyMcpServer();
+13
src/mcp/tools/albums.ts
···11+import { RockskyClient } from "client";
22+33+export async function albums(did, { skip, limit = 20 }): Promise<string> {
44+ const client = new RockskyClient();
55+ const albums = await client.getAlbums(did, { skip, limit });
66+ let rank = 1;
77+ let response = `Top ${limit} albums:\n`;
88+ for (const album of albums) {
99+ response += `${rank} ${album.title} - ${album.artist} - ${album.play_count} plays\n`;
1010+ rank++;
1111+ }
1212+ return response;
1313+}
+17
src/mcp/tools/artists.ts
···11+import { RockskyClient } from "client";
22+33+export async function artists(did, { skip, limit = 20 }): Promise<string> {
44+ try {
55+ const client = new RockskyClient();
66+ const artists = await client.getArtists(did, { skip, limit });
77+ let rank = 1;
88+ let response = `Top ${limit} artists:\n`;
99+ for (const artist of artists) {
1010+ response += `${rank} ${artist.name} - ${artist.play_count} plays\n`;
1111+ rank++;
1212+ }
1313+ return response;
1414+ } catch (err) {
1515+ return `Failed to fetch artists data. Please check your token and try again, error: ${err.message}`;
1616+ }
1717+}
+27
src/mcp/tools/create.ts
···11+import { RockskyClient } from "client";
22+import fs from "fs/promises";
33+import os from "os";
44+import path from "path";
55+66+export async function createApiKey(name, { description }) {
77+ const tokenPath = path.join(os.homedir(), ".rocksky", "token.json");
88+ try {
99+ await fs.access(tokenPath);
1010+ } catch (err) {
1111+ return "You are not logged in. Please run `rocksky login <username>.bsky.social` first.";
1212+ }
1313+1414+ const tokenData = await fs.readFile(tokenPath, "utf-8");
1515+ const { token } = JSON.parse(tokenData);
1616+ if (!token) {
1717+ return "You are not logged in. Please run `rocksky login <username>.bsky.social` first.";
1818+ }
1919+2020+ const client = new RockskyClient(token);
2121+ const apikey = await client.createApiKey(name, description);
2222+ if (!apikey) {
2323+ return "Failed to create API key. Please try again later.";
2424+ }
2525+2626+ return "API key created successfully!, navigate to your Rocksky account to view it.";
2727+}
+42
src/mcp/tools/myscrobbles.ts
···11+import { RockskyClient } from "client";
22+import dayjs from "dayjs";
33+import relative from "dayjs/plugin/relativeTime.js";
44+import fs from "fs/promises";
55+import os from "os";
66+import path from "path";
77+88+dayjs.extend(relative);
99+1010+export async function myscrobbles({ skip, limit }): Promise<string> {
1111+ const tokenPath = path.join(os.homedir(), ".rocksky", "token.json");
1212+ try {
1313+ await fs.access(tokenPath);
1414+ } catch (err) {
1515+ return "You are not logged in. Please run `rocksky login <username>.bsky.social` first.";
1616+ }
1717+1818+ const tokenData = await fs.readFile(tokenPath, "utf-8");
1919+ const { token } = JSON.parse(tokenData);
2020+ if (!token) {
2121+ return "You are not logged in. Please run `rocksky login <username>.bsky.social` first.";
2222+ }
2323+2424+ const client = new RockskyClient(token);
2525+ try {
2626+ const { did } = await client.getCurrentUser();
2727+ const scrobbles = await client.scrobbles(did, { skip, limit });
2828+2929+ return JSON.stringify(
3030+ scrobbles.map((scrobble) => ({
3131+ title: scrobble.title,
3232+ artist: scrobble.artist,
3333+ date: dayjs(scrobble.created_at + "Z").fromNow(),
3434+ isoDate: scrobble.created_at,
3535+ })),
3636+ null,
3737+ 2
3838+ );
3939+ } catch (err) {
4040+ return `Failed to fetch scrobbles data. Please check your token and try again, error: ${err.message}`;
4141+ }
4242+}
+53
src/mcp/tools/nowplaying.ts
···11+import { RockskyClient } from "client";
22+import fs from "fs/promises";
33+import os from "os";
44+import path from "path";
55+66+export async function nowplaying(did?: string): Promise<string> {
77+ const tokenPath = path.join(os.homedir(), ".rocksky", "token.json");
88+ try {
99+ await fs.access(tokenPath);
1010+ } catch (err) {
1111+ if (!did) {
1212+ return "You are not logged in. Please run `rocksky login <username>.bsky.social` first.";
1313+ }
1414+ }
1515+1616+ const tokenData = await fs.readFile(tokenPath, "utf-8");
1717+ const { token } = JSON.parse(tokenData);
1818+ if (!token && !did) {
1919+ return "You are not logged in. Please run `rocksky login <username>.bsky.social` first.";
2020+ }
2121+2222+ const client = new RockskyClient(token);
2323+ try {
2424+ const nowPlaying = await client.getSpotifyNowPlaying(did);
2525+ if (!nowPlaying || Object.keys(nowPlaying).length === 0) {
2626+ const nowPlaying = await client.getNowPlaying(did);
2727+ if (!nowPlaying || Object.keys(nowPlaying).length === 0) {
2828+ return "No track is currently playing.";
2929+ }
3030+ return JSON.stringify(
3131+ {
3232+ title: nowPlaying.title,
3333+ artist: nowPlaying.artist,
3434+ album: nowPlaying.album,
3535+ },
3636+ null,
3737+ 2
3838+ );
3939+ }
4040+4141+ return JSON.stringify(
4242+ {
4343+ title: nowPlaying.item.name,
4444+ artist: nowPlaying.item.artists.map((a) => a.name).join(", "),
4545+ album: nowPlaying.item.album.name,
4646+ },
4747+ null,
4848+ 2
4949+ );
5050+ } catch (err) {
5151+ return `Failed to fetch now playing data. Please check your token and try again, error: ${err.message}`;
5252+ }
5353+}