@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.
@@ -0,0 +1,72 @@
1
+ import chalk from "chalk";
2
+ import { RockskyClient } from "client";
3
+
4
+ export async function search(
5
+ query: string,
6
+ { limit = 20, albums = false, artists = false, tracks = false, users = false }
7
+ ) {
8
+ const client = new RockskyClient();
9
+ const results = await client.search(query, { size: limit });
10
+ if (results.records.length === 0) {
11
+ console.log(`No results found for ${chalk.magenta(query)}.`);
12
+ return;
13
+ }
14
+
15
+ // merge all results into one array with type and sort by xata_scrore
16
+ let mergedResults = results.records.map((record) => ({
17
+ ...record,
18
+ type: record.table,
19
+ }));
20
+
21
+ if (albums) {
22
+ mergedResults = mergedResults.filter((record) => record.table === "albums");
23
+ }
24
+
25
+ if (artists) {
26
+ mergedResults = mergedResults.filter(
27
+ (record) => record.table === "artists"
28
+ );
29
+ }
30
+
31
+ if (tracks) {
32
+ mergedResults = mergedResults.filter(({ table }) => table === "tracks");
33
+ }
34
+
35
+ if (users) {
36
+ mergedResults = mergedResults.filter(({ table }) => table === "users");
37
+ }
38
+
39
+ mergedResults.sort((a, b) => b.xata_score - a.xata_score);
40
+
41
+ for (const { table, record } of mergedResults) {
42
+ if (table === "users") {
43
+ console.log(
44
+ `${chalk.bold.magenta(record.handle)} ${
45
+ record.display_name
46
+ } ${chalk.yellow(`https://rocksky.app/profile/${record.did}`)}`
47
+ );
48
+ }
49
+
50
+ if (table === "albums") {
51
+ const link = record.uri
52
+ ? `https://rocksky.app/${record.uri?.split("at://")[1]}`
53
+ : "";
54
+ console.log(
55
+ `${chalk.bold.magenta(record.title)} ${record.artist} ${chalk.yellow(
56
+ link
57
+ )}`
58
+ );
59
+ }
60
+
61
+ if (table === "tracks") {
62
+ const link = record.uri
63
+ ? `https://rocksky.app/${record.uri?.split("at://")[1]}`
64
+ : "";
65
+ console.log(
66
+ `${chalk.bold.magenta(record.title)} ${record.artist} ${chalk.yellow(
67
+ link
68
+ )}`
69
+ );
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,56 @@
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
+ import { getBorderCharacters, table } from "table";
7
+
8
+ export async function stats(did?: string) {
9
+ const tokenPath = path.join(os.homedir(), ".rocksky", "token.json");
10
+ try {
11
+ await fs.access(tokenPath);
12
+ } catch (err) {
13
+ if (!did) {
14
+ console.error(
15
+ `You are not logged in. Please run ${chalk.greenBright(
16
+ "`rocksky login <username>.bsky.social`"
17
+ )} first.`
18
+ );
19
+ return;
20
+ }
21
+ }
22
+
23
+ const tokenData = await fs.readFile(tokenPath, "utf-8");
24
+ const { token } = JSON.parse(tokenData);
25
+ if (!token && !did) {
26
+ console.error(
27
+ `You are not logged in. Please run ${chalk.greenBright(
28
+ "`rocksky login <username>.bsky.social`"
29
+ )} first.`
30
+ );
31
+ return;
32
+ }
33
+
34
+ const client = new RockskyClient(token);
35
+ const stats = await client.stats(did);
36
+
37
+ console.log(
38
+ table(
39
+ [
40
+ ["Scrobbles", chalk.magenta(stats.scrobbles)],
41
+ ["Tracks", chalk.magenta(stats.tracks)],
42
+ ["Albums", chalk.magenta(stats.albums)],
43
+ ["Artists", chalk.magenta(stats.artists)],
44
+ ["Loved Tracks", chalk.magenta(stats.lovedTracks)],
45
+ ],
46
+ {
47
+ border: getBorderCharacters("void"),
48
+ columnDefault: {
49
+ paddingLeft: 0,
50
+ paddingRight: 1,
51
+ },
52
+ drawHorizontalLine: () => false,
53
+ }
54
+ )
55
+ );
56
+ }
@@ -0,0 +1,16 @@
1
+ import chalk from "chalk";
2
+ import { RockskyClient } from "client";
3
+
4
+ export async function tracks(did, { skip, limit }) {
5
+ const client = new RockskyClient();
6
+ const tracks = await client.getTracks(did, { skip, limit });
7
+ let rank = 1;
8
+ for (const track of tracks) {
9
+ console.log(
10
+ `${rank} ${chalk.magenta(track.title)} ${track.artist} ${chalk.yellow(
11
+ track.play_count + " plays"
12
+ )}`
13
+ );
14
+ rank++;
15
+ }
16
+ }
@@ -0,0 +1,45 @@
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 whoami() {
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
+ try {
33
+ const user = await client.getCurrentUser();
34
+ console.log(`You are logged in as ${user.handle} (${user.displayName}).`);
35
+ console.log(
36
+ `View your profile at: ${chalk.magenta(
37
+ `https://rocksky.app/profile/${user.handle}`
38
+ )}`
39
+ );
40
+ } catch (err) {
41
+ console.error(
42
+ `Failed to fetch user data. Please check your token and try again.`
43
+ );
44
+ }
45
+ }
package/src/index.ts ADDED
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
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";
11
+ import { tracks } from "cmd/tracks";
12
+ import { whoami } from "cmd/whoami";
13
+ import { Command } from "commander";
14
+ import version from "../package.json" assert { type: "json" };
15
+ import { login } from "./cmd/login";
16
+
17
+ const program = new Command();
18
+
19
+ program
20
+ .name("rocksky")
21
+ .description(
22
+ "Command-line interface for Rocksky – scrobble tracks, view stats, and manage your listening history."
23
+ )
24
+ .version(version.version);
25
+
26
+ program
27
+ .command("login")
28
+ .argument("<handle>", "your BlueSky handle (e.g., <username>.bsky.social)")
29
+ .description("login with your BlueSky account and get a session token.")
30
+ .action(login);
31
+
32
+ program
33
+ .command("whoami")
34
+ .description("get the current logged-in user.")
35
+ .action(whoami);
36
+
37
+ program
38
+ .command("nowplaying")
39
+ .argument(
40
+ "[did]",
41
+ "the DID or handle of the user to get the now playing track for."
42
+ )
43
+ .description("get the currently playing track.")
44
+ .action(nowplaying);
45
+
46
+ program
47
+ .command("scrobbles")
48
+ .option("-s, --skip <number>", "number of scrobbles to skip")
49
+ .option("-l, --limit <number>", "number of scrobbles to limit")
50
+ .argument("[did]", "the DID or handle of the user to get the scrobbles for.")
51
+ .description("display recently played tracks.")
52
+ .action(scrobbles);
53
+
54
+ program
55
+ .command("search")
56
+ .option("-a, --albums", "search for albums")
57
+ .option("-t, --tracks", "search for tracks")
58
+ .option("-u, --users", "search for users")
59
+ .option("-l, --limit <number>", "number of results to limit")
60
+ .argument(
61
+ "<query>",
62
+ "the search query, e.g., artist, album, title or account"
63
+ )
64
+ .description("search for tracks, albums, or accounts.")
65
+ .action(search);
66
+
67
+ program
68
+ .command("stats")
69
+ .option("-l, --limit <number>", "number of results to limit")
70
+ .argument("[did]", "the DID or handle of the user to get stats for.")
71
+ .description("get the user's listening stats.")
72
+ .action(stats);
73
+
74
+ program
75
+ .command("artists")
76
+ .option("-l, --limit <number>", "number of results to limit")
77
+ .argument("[did]", "the DID or handle of the user to get artists for.")
78
+ .description("get the user's top artists.")
79
+ .action(artists);
80
+
81
+ program
82
+ .command("albums")
83
+ .option("-l, --limit <number>", "number of results to limit")
84
+ .argument("[did]", "the DID or handle of the user to get albums for.")
85
+ .description("get the user's top albums.")
86
+ .action(albums);
87
+
88
+ program
89
+ .command("tracks")
90
+ .option("-l, --limit <number>", "number of results to limit")
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
+ .description("create a new API key.")
106
+ .command("apikey")
107
+ .argument("<name>", "the name of the API key")
108
+ .option("-d, --description <description>", "the description of the API key")
109
+ .description("create a new API key.")
110
+ .action(createApiKey);
111
+
112
+ program.parse(process.argv);
package/tsconfig.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowJs": true,
4
+ "allowSyntheticDefaultImports": true,
5
+ "baseUrl": "src",
6
+ "declaration": true,
7
+ "sourceMap": true,
8
+ "esModuleInterop": true,
9
+ "inlineSourceMap": false,
10
+ "lib": ["esnext", "DOM"],
11
+ "listEmittedFiles": false,
12
+ "listFiles": false,
13
+ "moduleResolution": "node",
14
+ "noFallthroughCasesInSwitch": true,
15
+ "pretty": true,
16
+ "resolveJsonModule": true,
17
+ "rootDir": ".",
18
+ "skipLibCheck": true,
19
+ "strict": false,
20
+ "traceResolution": false,
21
+ "outDir": "",
22
+ "target": "esnext",
23
+ "module": "esnext",
24
+ "types": [
25
+ "@types/node",
26
+ "@types/express",
27
+ ]
28
+ },
29
+ "exclude": ["node_modules", "dist", "tests"],
30
+ "include": ["src", "scripts"]
31
+ }