@rocksky/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,702 @@
1
+ #!/usr/bin/env node
2
+ import chalk from 'chalk';
3
+ import fs from 'fs';
4
+ import os from 'os';
5
+ import path from 'path';
6
+ import fs$1 from 'fs/promises';
7
+ import md5 from 'md5';
8
+ import dayjs from 'dayjs';
9
+ import relative from 'dayjs/plugin/relativeTime.js';
10
+ import { table, getBorderCharacters } from 'table';
11
+ import { Command } from 'commander';
12
+ import axios from 'axios';
13
+ import cors from 'cors';
14
+ import express from 'express';
15
+ import open from 'open';
16
+
17
+ const ROCKSKY_API_URL = "https://api.rocksky.app";
18
+ class RockskyClient {
19
+ constructor(token) {
20
+ this.token = token;
21
+ this.token = token;
22
+ }
23
+ async getCurrentUser() {
24
+ const response = await fetch(`${ROCKSKY_API_URL}/profile`, {
25
+ method: "GET",
26
+ headers: {
27
+ Authorization: this.token ? `Bearer ${this.token}` : void 0,
28
+ "Content-Type": "application/json"
29
+ }
30
+ });
31
+ if (!response.ok) {
32
+ throw new Error(`Failed to fetch user data: ${response.statusText}`);
33
+ }
34
+ return response.json();
35
+ }
36
+ async getSpotifyNowPlaying(did) {
37
+ const response = await fetch(
38
+ `${ROCKSKY_API_URL}/spotify/currently-playing` + (did ? `?did=${did}` : ""),
39
+ {
40
+ method: "GET",
41
+ headers: {
42
+ Authorization: this.token ? `Bearer ${this.token}` : void 0,
43
+ "Content-Type": "application/json"
44
+ }
45
+ }
46
+ );
47
+ if (!response.ok) {
48
+ throw new Error(
49
+ `Failed to fetch now playing data: ${response.statusText}`
50
+ );
51
+ }
52
+ return response.json();
53
+ }
54
+ async getNowPlaying(did) {
55
+ const response = await fetch(
56
+ `${ROCKSKY_API_URL}/now-playing` + (did ? `?did=${did}` : ""),
57
+ {
58
+ method: "GET",
59
+ headers: {
60
+ Authorization: this.token ? `Bearer ${this.token}` : void 0,
61
+ "Content-Type": "application/json"
62
+ }
63
+ }
64
+ );
65
+ if (!response.ok) {
66
+ throw new Error(
67
+ `Failed to fetch now playing data: ${response.statusText}`
68
+ );
69
+ }
70
+ return response.json();
71
+ }
72
+ async scrobbles(did, { skip = 0, limit = 20 } = {}) {
73
+ if (did) {
74
+ const response2 = await fetch(
75
+ `${ROCKSKY_API_URL}/users/${did}/scrobbles?offset=${skip}&size=${limit}`,
76
+ {
77
+ method: "GET",
78
+ headers: {
79
+ Authorization: this.token ? `Bearer ${this.token}` : void 0,
80
+ "Content-Type": "application/json"
81
+ }
82
+ }
83
+ );
84
+ if (!response2.ok) {
85
+ throw new Error(
86
+ `Failed to fetch scrobbles data: ${response2.statusText}`
87
+ );
88
+ }
89
+ return response2.json();
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}` : void 0,
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
+ return response.json();
105
+ }
106
+ async search(query, { size }) {
107
+ const response = await fetch(
108
+ `${ROCKSKY_API_URL}/search?q=${query}&size=${size}`,
109
+ {
110
+ method: "GET",
111
+ headers: {
112
+ Authorization: this.token ? `Bearer ${this.token}` : void 0,
113
+ "Content-Type": "application/json"
114
+ }
115
+ }
116
+ );
117
+ if (!response.ok) {
118
+ throw new Error(`Failed to fetch search data: ${response.statusText}`);
119
+ }
120
+ return response.json();
121
+ }
122
+ async stats(did) {
123
+ if (!did) {
124
+ const didFile = path.join(os.homedir(), ".rocksky", "did");
125
+ try {
126
+ await fs.promises.access(didFile);
127
+ did = await fs.promises.readFile(didFile, "utf-8");
128
+ } catch (err) {
129
+ const user = await this.getCurrentUser();
130
+ did = user.did;
131
+ const didPath = path.join(os.homedir(), ".rocksky");
132
+ fs.promises.mkdir(didPath, { recursive: true });
133
+ await fs.promises.writeFile(didFile, did);
134
+ }
135
+ }
136
+ const response = await fetch(`${ROCKSKY_API_URL}/users/${did}/stats`, {
137
+ method: "GET",
138
+ headers: {
139
+ "Content-Type": "application/json"
140
+ }
141
+ });
142
+ if (!response.ok) {
143
+ throw new Error(`Failed to fetch stats data: ${response.statusText}`);
144
+ }
145
+ return response.json();
146
+ }
147
+ async getArtists(did, { skip = 0, limit = 20 } = {}) {
148
+ if (!did) {
149
+ const didFile = path.join(os.homedir(), ".rocksky", "did");
150
+ try {
151
+ await fs.promises.access(didFile);
152
+ did = await fs.promises.readFile(didFile, "utf-8");
153
+ } catch (err) {
154
+ const user = await this.getCurrentUser();
155
+ did = user.did;
156
+ const didPath = path.join(os.homedir(), ".rocksky");
157
+ fs.promises.mkdir(didPath, { recursive: true });
158
+ await fs.promises.writeFile(didFile, did);
159
+ }
160
+ }
161
+ const response = await fetch(
162
+ `${ROCKSKY_API_URL}/users/${did}/artists?offset=${skip}&size=${limit}`,
163
+ {
164
+ method: "GET",
165
+ headers: {
166
+ Authorization: this.token ? `Bearer ${this.token}` : void 0,
167
+ "Content-Type": "application/json"
168
+ }
169
+ }
170
+ );
171
+ if (!response.ok) {
172
+ throw new Error(`Failed to fetch artists data: ${response.statusText}`);
173
+ }
174
+ return response.json();
175
+ }
176
+ async getAlbums(did, { skip = 0, limit = 20 } = {}) {
177
+ if (!did) {
178
+ const didFile = path.join(os.homedir(), ".rocksky", "did");
179
+ try {
180
+ await fs.promises.access(didFile);
181
+ did = await fs.promises.readFile(didFile, "utf-8");
182
+ } catch (err) {
183
+ const user = await this.getCurrentUser();
184
+ did = user.did;
185
+ const didPath = path.join(os.homedir(), ".rocksky");
186
+ fs.promises.mkdir(didPath, { recursive: true });
187
+ await fs.promises.writeFile(didFile, did);
188
+ }
189
+ }
190
+ const response = await fetch(
191
+ `${ROCKSKY_API_URL}/users/${did}/albums?offset=${skip}&size=${limit}`,
192
+ {
193
+ method: "GET",
194
+ headers: {
195
+ Authorization: this.token ? `Bearer ${this.token}` : void 0,
196
+ "Content-Type": "application/json"
197
+ }
198
+ }
199
+ );
200
+ if (!response.ok) {
201
+ throw new Error(`Failed to fetch albums data: ${response.statusText}`);
202
+ }
203
+ return response.json();
204
+ }
205
+ async getTracks(did, { skip = 0, limit = 20 } = {}) {
206
+ if (!did) {
207
+ const didFile = path.join(os.homedir(), ".rocksky", "did");
208
+ try {
209
+ await fs.promises.access(didFile);
210
+ did = await fs.promises.readFile(didFile, "utf-8");
211
+ } catch (err) {
212
+ const user = await this.getCurrentUser();
213
+ did = user.did;
214
+ const didPath = path.join(os.homedir(), ".rocksky");
215
+ fs.promises.mkdir(didPath, { recursive: true });
216
+ await fs.promises.writeFile(didFile, did);
217
+ }
218
+ }
219
+ const response = await fetch(
220
+ `${ROCKSKY_API_URL}/users/${did}/tracks?offset=${skip}&size=${limit}`,
221
+ {
222
+ method: "GET",
223
+ headers: {
224
+ Authorization: this.token ? `Bearer ${this.token}` : void 0,
225
+ "Content-Type": "application/json"
226
+ }
227
+ }
228
+ );
229
+ if (!response.ok) {
230
+ throw new Error(`Failed to fetch tracks data: ${response.statusText}`);
231
+ }
232
+ return response.json();
233
+ }
234
+ async scrobble(api_key, api_sig, track, artist, timestamp) {
235
+ const tokenPath = path.join(os.homedir(), ".rocksky", "token.json");
236
+ try {
237
+ await fs.promises.access(tokenPath);
238
+ } catch (err) {
239
+ console.error(
240
+ `You are not logged in. Please run the login command first.`
241
+ );
242
+ return;
243
+ }
244
+ const tokenData = await fs.promises.readFile(tokenPath, "utf-8");
245
+ const { token: sk } = JSON.parse(tokenData);
246
+ const response = await fetch("https://audioscrobbler.rocksky.app/2.0", {
247
+ method: "POST",
248
+ headers: {
249
+ "Content-Type": "application/x-www-form-urlencoded"
250
+ },
251
+ body: new URLSearchParams({
252
+ method: "track.scrobble",
253
+ "track[0]": track,
254
+ "artist[0]": artist,
255
+ "timestamp[0]": timestamp || Math.floor(Date.now() / 1e3),
256
+ api_key,
257
+ api_sig,
258
+ sk,
259
+ format: "json"
260
+ })
261
+ });
262
+ if (!response.ok) {
263
+ throw new Error(
264
+ `Failed to scrobble track: ${response.statusText} ${await response.text()}`
265
+ );
266
+ }
267
+ return response.json();
268
+ }
269
+ async getApiKeys() {
270
+ const response = await fetch(`${ROCKSKY_API_URL}/apikeys`, {
271
+ method: "GET",
272
+ headers: {
273
+ Authorization: this.token ? `Bearer ${this.token}` : void 0,
274
+ "Content-Type": "application/json"
275
+ }
276
+ });
277
+ if (!response.ok) {
278
+ throw new Error(`Failed to fetch API keys: ${response.statusText}`);
279
+ }
280
+ return response.json();
281
+ }
282
+ async createApiKey(name, description) {
283
+ const response = await fetch(`${ROCKSKY_API_URL}/apikeys`, {
284
+ method: "POST",
285
+ headers: {
286
+ Authorization: this.token ? `Bearer ${this.token}` : void 0,
287
+ "Content-Type": "application/json"
288
+ },
289
+ body: JSON.stringify({
290
+ name,
291
+ description
292
+ })
293
+ });
294
+ if (!response.ok) {
295
+ throw new Error(`Failed to create API key: ${response.statusText}`);
296
+ }
297
+ return response.json();
298
+ }
299
+ }
300
+
301
+ async function albums(did, { skip, limit }) {
302
+ const client = new RockskyClient();
303
+ const albums2 = await client.getAlbums(did, { skip, limit });
304
+ let rank = 1;
305
+ for (const album of albums2) {
306
+ console.log(
307
+ `${rank} ${chalk.magenta(album.title)} ${album.artist} ${chalk.yellow(
308
+ album.play_count + " plays"
309
+ )}`
310
+ );
311
+ rank++;
312
+ }
313
+ }
314
+
315
+ async function artists(did, { skip, limit }) {
316
+ const client = new RockskyClient();
317
+ const artists2 = await client.getArtists(did, { skip, limit });
318
+ let rank = 1;
319
+ for (const artist of artists2) {
320
+ console.log(
321
+ `${rank} ${chalk.magenta(artist.name)} ${chalk.yellow(
322
+ artist.play_count + " plays"
323
+ )}`
324
+ );
325
+ rank++;
326
+ }
327
+ }
328
+
329
+ async function createApiKey(name, { description }) {
330
+ const tokenPath = path.join(os.homedir(), ".rocksky", "token.json");
331
+ try {
332
+ await fs$1.access(tokenPath);
333
+ } catch (err) {
334
+ console.error(
335
+ `You are not logged in. Please run ${chalk.greenBright(
336
+ "`rocksky login <username>.bsky.social`"
337
+ )} first.`
338
+ );
339
+ return;
340
+ }
341
+ const tokenData = await fs$1.readFile(tokenPath, "utf-8");
342
+ const { token } = JSON.parse(tokenData);
343
+ if (!token) {
344
+ console.error(
345
+ `You are not logged in. Please run ${chalk.greenBright(
346
+ "`rocksky login <username>.bsky.social`"
347
+ )} first.`
348
+ );
349
+ return;
350
+ }
351
+ const client = new RockskyClient(token);
352
+ const apikey = await client.createApiKey(name, description);
353
+ if (!apikey) {
354
+ console.error(`Failed to create API key. Please try again later.`);
355
+ return;
356
+ }
357
+ console.log(`API key created successfully!`);
358
+ console.log(`Name: ${chalk.greenBright(apikey.name)}`);
359
+ if (apikey.description) {
360
+ console.log(`Description: ${chalk.greenBright(apikey.description)}`);
361
+ }
362
+ console.log(`Key: ${chalk.greenBright(apikey.api_key)}`);
363
+ console.log(`Secret: ${chalk.greenBright(apikey.shared_secret)}`);
364
+ }
365
+
366
+ async function nowplaying(did) {
367
+ const tokenPath = path.join(os.homedir(), ".rocksky", "token.json");
368
+ try {
369
+ await fs$1.access(tokenPath);
370
+ } catch (err) {
371
+ if (!did) {
372
+ console.error(
373
+ `You are not logged in. Please run ${chalk.greenBright(
374
+ "`rocksky login <username>.bsky.social`"
375
+ )} first.`
376
+ );
377
+ return;
378
+ }
379
+ }
380
+ const tokenData = await fs$1.readFile(tokenPath, "utf-8");
381
+ const { token } = JSON.parse(tokenData);
382
+ if (!token && !did) {
383
+ console.error(
384
+ `You are not logged in. Please run ${chalk.greenBright(
385
+ "`rocksky login <username>.bsky.social`"
386
+ )} first.`
387
+ );
388
+ return;
389
+ }
390
+ const client = new RockskyClient(token);
391
+ try {
392
+ const nowPlaying = await client.getSpotifyNowPlaying(did);
393
+ if (!nowPlaying || Object.keys(nowPlaying).length === 0) {
394
+ const nowPlaying2 = await client.getNowPlaying(did);
395
+ if (!nowPlaying2 || Object.keys(nowPlaying2).length === 0) {
396
+ console.log("No track is currently playing.");
397
+ return;
398
+ }
399
+ console.log(chalk.magenta(`${nowPlaying2.title} - ${nowPlaying2.artist}`));
400
+ console.log(`${nowPlaying2.album}`);
401
+ return;
402
+ }
403
+ console.log(
404
+ chalk.magenta(
405
+ `${nowPlaying.item.name} - ${nowPlaying.item.artists.map((a) => a.name).join(", ")}`
406
+ )
407
+ );
408
+ console.log(`${nowPlaying.item.album.name}`);
409
+ } catch (err) {
410
+ console.log(err);
411
+ console.error(
412
+ `Failed to fetch now playing data. Please check your token and try again.`
413
+ );
414
+ }
415
+ }
416
+
417
+ async function scrobble(track, artist, { timestamp }) {
418
+ const tokenPath = path.join(os.homedir(), ".rocksky", "token.json");
419
+ try {
420
+ await fs$1.access(tokenPath);
421
+ } catch (err) {
422
+ console.error(
423
+ `You are not logged in. Please run ${chalk.greenBright(
424
+ "`rocksky login <username>.bsky.social`"
425
+ )} first.`
426
+ );
427
+ return;
428
+ }
429
+ const tokenData = await fs$1.readFile(tokenPath, "utf-8");
430
+ const { token } = JSON.parse(tokenData);
431
+ if (!token) {
432
+ console.error(
433
+ `You are not logged in. Please run ${chalk.greenBright(
434
+ "`rocksky login <username>.bsky.social`"
435
+ )} first.`
436
+ );
437
+ return;
438
+ }
439
+ const client = new RockskyClient(token);
440
+ const apikeys = await client.getApiKeys();
441
+ if (!apikeys || apikeys.length === 0 || !apikeys[0].enabled) {
442
+ console.error(
443
+ `You don't have any API keys. Please create one using ${chalk.greenBright(
444
+ "`rocksky create apikey`"
445
+ )} command.`
446
+ );
447
+ return;
448
+ }
449
+ const signature = md5(
450
+ `api_key${apikeys[0].apiKey}artist[0]${artist}methodtrack.scrobblesk${token}timestamp[0]${timestamp || Math.floor(Date.now() / 1e3)}track[0]${track}${apikeys[0].sharedSecret}`
451
+ );
452
+ await client.scrobble(
453
+ apikeys[0].apiKey,
454
+ signature,
455
+ track,
456
+ artist,
457
+ timestamp
458
+ );
459
+ console.log(
460
+ `Scrobbled ${chalk.greenBright(track)} by ${chalk.greenBright(
461
+ artist
462
+ )} at ${chalk.greenBright(
463
+ new Date(
464
+ (timestamp || Math.floor(Date.now() / 1e3)) * 1e3
465
+ ).toLocaleString()
466
+ )}`
467
+ );
468
+ }
469
+
470
+ dayjs.extend(relative);
471
+ async function scrobbles(did, { skip, limit }) {
472
+ const client = new RockskyClient();
473
+ const scrobbles2 = await client.scrobbles(did, { skip, limit });
474
+ for (const scrobble of scrobbles2) {
475
+ if (did) {
476
+ console.log(
477
+ `${chalk.bold.magenta(scrobble.title)} ${scrobble.artist} ${chalk.yellow(dayjs(scrobble.created_at + "Z").fromNow())}`
478
+ );
479
+ continue;
480
+ }
481
+ const handle = `@${scrobble.user}`;
482
+ console.log(
483
+ `${chalk.italic.magentaBright(
484
+ handle
485
+ )} is listening to ${chalk.bold.magenta(scrobble.title)} ${scrobble.artist} ${chalk.yellow(dayjs(scrobble.date).fromNow())}`
486
+ );
487
+ }
488
+ }
489
+
490
+ async function search(query, { limit = 20, albums = false, artists = false, tracks = false, users = false }) {
491
+ const client = new RockskyClient();
492
+ const results = await client.search(query, { size: limit });
493
+ if (results.records.length === 0) {
494
+ console.log(`No results found for ${chalk.magenta(query)}.`);
495
+ return;
496
+ }
497
+ let mergedResults = results.records.map((record) => ({
498
+ ...record,
499
+ type: record.table
500
+ }));
501
+ if (albums) {
502
+ mergedResults = mergedResults.filter((record) => record.table === "albums");
503
+ }
504
+ if (artists) {
505
+ mergedResults = mergedResults.filter(
506
+ (record) => record.table === "artists"
507
+ );
508
+ }
509
+ if (tracks) {
510
+ mergedResults = mergedResults.filter(({ table }) => table === "tracks");
511
+ }
512
+ if (users) {
513
+ mergedResults = mergedResults.filter(({ table }) => table === "users");
514
+ }
515
+ mergedResults.sort((a, b) => b.xata_score - a.xata_score);
516
+ for (const { table, record } of mergedResults) {
517
+ if (table === "users") {
518
+ console.log(
519
+ `${chalk.bold.magenta(record.handle)} ${record.display_name} ${chalk.yellow(`https://rocksky.app/profile/${record.did}`)}`
520
+ );
521
+ }
522
+ if (table === "albums") {
523
+ const link = record.uri ? `https://rocksky.app/${record.uri?.split("at://")[1]}` : "";
524
+ console.log(
525
+ `${chalk.bold.magenta(record.title)} ${record.artist} ${chalk.yellow(
526
+ link
527
+ )}`
528
+ );
529
+ }
530
+ if (table === "tracks") {
531
+ const link = record.uri ? `https://rocksky.app/${record.uri?.split("at://")[1]}` : "";
532
+ console.log(
533
+ `${chalk.bold.magenta(record.title)} ${record.artist} ${chalk.yellow(
534
+ link
535
+ )}`
536
+ );
537
+ }
538
+ }
539
+ }
540
+
541
+ async function stats(did) {
542
+ const tokenPath = path.join(os.homedir(), ".rocksky", "token.json");
543
+ try {
544
+ await fs$1.access(tokenPath);
545
+ } catch (err) {
546
+ if (!did) {
547
+ console.error(
548
+ `You are not logged in. Please run ${chalk.greenBright(
549
+ "`rocksky login <username>.bsky.social`"
550
+ )} first.`
551
+ );
552
+ return;
553
+ }
554
+ }
555
+ const tokenData = await fs$1.readFile(tokenPath, "utf-8");
556
+ const { token } = JSON.parse(tokenData);
557
+ if (!token && !did) {
558
+ console.error(
559
+ `You are not logged in. Please run ${chalk.greenBright(
560
+ "`rocksky login <username>.bsky.social`"
561
+ )} first.`
562
+ );
563
+ return;
564
+ }
565
+ const client = new RockskyClient(token);
566
+ const stats2 = await client.stats(did);
567
+ console.log(
568
+ table(
569
+ [
570
+ ["Scrobbles", chalk.magenta(stats2.scrobbles)],
571
+ ["Tracks", chalk.magenta(stats2.tracks)],
572
+ ["Albums", chalk.magenta(stats2.albums)],
573
+ ["Artists", chalk.magenta(stats2.artists)],
574
+ ["Loved Tracks", chalk.magenta(stats2.lovedTracks)]
575
+ ],
576
+ {
577
+ border: getBorderCharacters("void"),
578
+ columnDefault: {
579
+ paddingLeft: 0,
580
+ paddingRight: 1
581
+ },
582
+ drawHorizontalLine: () => false
583
+ }
584
+ )
585
+ );
586
+ }
587
+
588
+ async function tracks(did, { skip, limit }) {
589
+ const client = new RockskyClient();
590
+ const tracks2 = await client.getTracks(did, { skip, limit });
591
+ let rank = 1;
592
+ for (const track of tracks2) {
593
+ console.log(
594
+ `${rank} ${chalk.magenta(track.title)} ${track.artist} ${chalk.yellow(
595
+ track.play_count + " plays"
596
+ )}`
597
+ );
598
+ rank++;
599
+ }
600
+ }
601
+
602
+ async function whoami() {
603
+ const tokenPath = path.join(os.homedir(), ".rocksky", "token.json");
604
+ try {
605
+ await fs$1.access(tokenPath);
606
+ } catch (err) {
607
+ console.error(
608
+ `You are not logged in. Please run ${chalk.greenBright(
609
+ "`rocksky login <username>.bsky.social`"
610
+ )} first.`
611
+ );
612
+ return;
613
+ }
614
+ const tokenData = await fs$1.readFile(tokenPath, "utf-8");
615
+ const { token } = JSON.parse(tokenData);
616
+ if (!token) {
617
+ console.error(
618
+ `You are not logged in. Please run ${chalk.greenBright(
619
+ "`rocksky login <username>.bsky.social`"
620
+ )} first.`
621
+ );
622
+ return;
623
+ }
624
+ const client = new RockskyClient(token);
625
+ try {
626
+ const user = await client.getCurrentUser();
627
+ console.log(`You are logged in as ${user.handle} (${user.displayName}).`);
628
+ console.log(
629
+ `View your profile at: ${chalk.magenta(
630
+ `https://rocksky.app/profile/${user.handle}`
631
+ )}`
632
+ );
633
+ } catch (err) {
634
+ console.error(
635
+ `Failed to fetch user data. Please check your token and try again.`
636
+ );
637
+ }
638
+ }
639
+
640
+ var version = "0.1.0";
641
+ var version$1 = {
642
+ version: version};
643
+
644
+ async function login(handle) {
645
+ const app = express();
646
+ app.use(cors());
647
+ app.use(express.json());
648
+ const server = app.listen(6996);
649
+ app.post("/token", async (req, res) => {
650
+ console.log(chalk.bold(chalk.greenBright("Login successful!\n")));
651
+ console.log(
652
+ "You can use this session key (Token) to authenticate with the API."
653
+ );
654
+ console.log("Received token (session key):", chalk.green(req.body.token));
655
+ const tokenPath = path.join(os.homedir(), ".rocksky", "token.json");
656
+ await fs$1.mkdir(path.dirname(tokenPath), { recursive: true });
657
+ await fs$1.writeFile(
658
+ tokenPath,
659
+ JSON.stringify({ token: req.body.token }, null, 2)
660
+ );
661
+ res.json({
662
+ ok: 1
663
+ });
664
+ server.close();
665
+ });
666
+ const response = await axios.post("https://api.rocksky.app/login", {
667
+ handle,
668
+ cli: true
669
+ });
670
+ const redirectUrl = response.data;
671
+ if (!redirectUrl.includes("authorize")) {
672
+ console.error("Failed to login, please check your handle and try again.");
673
+ server.close();
674
+ return;
675
+ }
676
+ console.log("Please visit this URL to authorize the app:");
677
+ console.log(chalk.cyan(redirectUrl));
678
+ open(redirectUrl);
679
+ }
680
+
681
+ const program = new Command();
682
+ program.name("rocksky").description(
683
+ "Command-line interface for Rocksky \u2013 scrobble tracks, view stats, and manage your listening history."
684
+ ).version(version$1.version);
685
+ program.command("login").argument("<handle>", "Your BlueSky handle (e.g., <username>.bsky.social)").description("Login with your BlueSky account and get a session token.").action(login);
686
+ program.command("whoami").description("Get the current logged-in user.").action(whoami);
687
+ program.command("nowplaying").argument(
688
+ "[did]",
689
+ "The DID or handle of the user to get the now playing track for."
690
+ ).description("Get the currently playing track.").action(nowplaying);
691
+ program.command("scrobbles").option("-s, --skip <number>", "Number of scrobbles to skip").option("-l, --limit <number>", "Number of scrobbles to limit").argument("[did]", "The DID or handle of the user to get the scrobbles for.").description("Display recently played tracks.").action(scrobbles);
692
+ program.command("search").option("-a, --albums", "Search for albums").option("-t, --tracks", "Search for tracks").option("-u, --users", "Search for users").option("-l, --limit <number>", "Number of results to limit").argument(
693
+ "<query>",
694
+ "The search query, e.g., artist, album, title or account"
695
+ ).description("Search for tracks, albums, or accounts.").action(search);
696
+ program.command("stats").option("-l, --limit <number>", "Number of results to limit").argument("[did]", "The DID or handle of the user to get stats for.").description("Get the user's listening stats.").action(stats);
697
+ program.command("artists").option("-l, --limit <number>", "Number of results to limit").argument("[did]", "The DID or handle of the user to get artists for.").description("Get the user's top artists.").action(artists);
698
+ program.command("albums").option("-l, --limit <number>", "Number of results to limit").argument("[did]", "The DID or handle of the user to get albums for.").description("Get the user's top albums.").action(albums);
699
+ program.command("tracks").option("-l, --limit <number>", "Number of results to limit").argument("[did]", "The DID or handle of the user to get tracks for.").description("Get the user's top tracks.").action(tracks);
700
+ program.command("scrobble").argument("<track>", "The title of the track").argument("<artist>", "The artist of the track").option("-t, --timestamp <timestamp>", "The timestamp of the scrobble").description("Scrobble a track to your profile.").action(scrobble);
701
+ program.command("create").command("apikey").argument("<name>", "The name of the API key").option("-d, --description <description>", "The description of the API key").description("Create a new API key.").action(createApiKey);
702
+ program.parse(process.argv);