@crafter/spoti-cli 0.1.1 → 0.2.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.
Files changed (2) hide show
  1. package/dist/index.js +433 -10
  2. package/package.json +7 -3
package/dist/index.js CHANGED
@@ -74,6 +74,22 @@ import { homedir } from "os";
74
74
  import { join } from "path";
75
75
  var CONFIG_DIR = join(homedir(), ".spoti-cli");
76
76
  var CONFIG_FILE = join(CONFIG_DIR, "config.json");
77
+ var LEGACY_SCOPES = [
78
+ "playlist-modify-private",
79
+ "playlist-modify-public",
80
+ "user-read-private",
81
+ "user-read-email"
82
+ ];
83
+ function requireScopes(needed) {
84
+ const config = readConfig();
85
+ const granted = config.scopes ?? LEGACY_SCOPES;
86
+ const missing = needed.filter((s) => !granted.includes(s));
87
+ if (missing.length > 0) {
88
+ console.error(`This command requires additional permissions: ${missing.join(", ")}`);
89
+ console.error("Run: spoti-cli auth --upgrade");
90
+ process.exit(1);
91
+ }
92
+ }
77
93
  function ensureConfigDir() {
78
94
  if (!existsSync(CONFIG_DIR)) {
79
95
  mkdirSync(CONFIG_DIR, { recursive: true });
@@ -94,12 +110,22 @@ function updateConfig(partial) {
94
110
  }
95
111
 
96
112
  // src/commands/auth.ts
97
- var SCOPES = [
113
+ var BASE_SCOPES = [
98
114
  "playlist-modify-private",
99
115
  "playlist-modify-public",
100
116
  "user-read-private",
101
117
  "user-read-email"
102
118
  ];
119
+ var FULL_SCOPES = [
120
+ ...BASE_SCOPES,
121
+ "user-read-playback-state",
122
+ "user-modify-playback-state",
123
+ "user-read-currently-playing",
124
+ "user-top-read",
125
+ "user-library-read",
126
+ "user-library-modify",
127
+ "user-read-recently-played"
128
+ ];
103
129
  function generateCodeVerifier() {
104
130
  const array = new Uint8Array(64);
105
131
  crypto.getRandomValues(array);
@@ -111,7 +137,7 @@ async function generateCodeChallenge(verifier) {
111
137
  const digest = await crypto.subtle.digest("SHA-256", data);
112
138
  return btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
113
139
  }
114
- async function authCommand(clientId) {
140
+ async function authCommand(clientId, upgrade = false) {
115
141
  const config = readConfig();
116
142
  const id = clientId ?? config.client_id;
117
143
  if (!id) {
@@ -125,7 +151,8 @@ async function authCommand(clientId) {
125
151
  const authUrl = new URL("https://accounts.spotify.com/authorize");
126
152
  authUrl.searchParams.set("response_type", "code");
127
153
  authUrl.searchParams.set("client_id", id);
128
- authUrl.searchParams.set("scope", SCOPES.join(" "));
154
+ const scopes = upgrade ? [.../* @__PURE__ */ new Set([...config.scopes ?? BASE_SCOPES, ...FULL_SCOPES])] : FULL_SCOPES;
155
+ authUrl.searchParams.set("scope", scopes.join(" "));
129
156
  authUrl.searchParams.set("redirect_uri", REDIRECT_URI);
130
157
  authUrl.searchParams.set("code_challenge_method", "S256");
131
158
  authUrl.searchParams.set("code_challenge", codeChallenge);
@@ -155,7 +182,8 @@ async function authCommand(clientId) {
155
182
  updateConfig({
156
183
  access_token: data.access_token,
157
184
  refresh_token: data.refresh_token,
158
- expires_at: Date.now() + data.expires_in * 1e3
185
+ expires_at: Date.now() + data.expires_in * 1e3,
186
+ scopes
159
187
  });
160
188
  console.log("Authenticated successfully!");
161
189
  console.log("");
@@ -252,6 +280,74 @@ async function spotify(path, options = {}) {
252
280
  return res.json();
253
281
  }
254
282
 
283
+ // src/commands/artist.ts
284
+ async function artistGetCommand(id, opts) {
285
+ const data = await spotify(`/artists/${id}`);
286
+ const result = {
287
+ id: data.id,
288
+ name: data.name,
289
+ genres: data.genres.join(", "),
290
+ popularity: data.popularity,
291
+ followers: data.followers.total,
292
+ uri: data.uri,
293
+ url: data.external_urls.spotify
294
+ };
295
+ output(result, opts.json);
296
+ }
297
+ async function artistTopTracksCommand(id, opts) {
298
+ const market = opts.market || "US";
299
+ const data = await spotify(
300
+ `/artists/${id}/top-tracks?market=${market}`
301
+ );
302
+ const items = data.tracks.map((t) => ({
303
+ id: t.id,
304
+ name: t.name,
305
+ artist: t.artists.map((a) => a.name).join(", "),
306
+ album: t.album.name,
307
+ popularity: t.popularity,
308
+ uri: t.uri
309
+ }));
310
+ output(items, opts.json);
311
+ }
312
+ async function artistAlbumsCommand(id, opts) {
313
+ const limit = opts.limit || "20";
314
+ const data = await spotify(
315
+ `/artists/${id}/albums?limit=${limit}&include_groups=album,single`
316
+ );
317
+ const items = data.items.map((a) => ({
318
+ id: a.id,
319
+ name: a.name,
320
+ type: a.album_type,
321
+ released: a.release_date,
322
+ tracks: a.total_tracks,
323
+ uri: a.uri
324
+ }));
325
+ output(items, opts.json);
326
+ }
327
+ async function artistRelatedCommand(id, opts) {
328
+ let data;
329
+ try {
330
+ data = await spotify(
331
+ `/artists/${id}/related-artists`
332
+ );
333
+ } catch (err) {
334
+ if (err instanceof Error && (err.message.includes("403") || err.message.includes("404"))) {
335
+ console.error("Related artists endpoint is restricted by Spotify. This may require an approved app.");
336
+ process.exit(1);
337
+ }
338
+ throw err;
339
+ }
340
+ const items = data.artists.map((a) => ({
341
+ id: a.id,
342
+ name: a.name,
343
+ genres: a.genres.join(", "),
344
+ popularity: a.popularity,
345
+ followers: a.followers.total,
346
+ uri: a.uri
347
+ }));
348
+ output(items, opts.json);
349
+ }
350
+
255
351
  // src/commands/create.ts
256
352
  async function createCommand(name, opts) {
257
353
  const user = await spotify("/me");
@@ -291,6 +387,74 @@ async function createCommand(name, opts) {
291
387
  }
292
388
  }
293
389
 
390
+ // src/commands/history.ts
391
+ var HISTORY_SCOPES = ["user-read-recently-played"];
392
+ async function historyCommand(opts) {
393
+ requireScopes(HISTORY_SCOPES);
394
+ const limit = opts.limit || "20";
395
+ const data = await spotify(
396
+ `/me/player/recently-played?limit=${limit}`
397
+ );
398
+ const items = data.items.map((i) => ({
399
+ id: i.track.id,
400
+ name: i.track.name,
401
+ artist: i.track.artists.map((a) => a.name).join(", "),
402
+ album: i.track.album.name,
403
+ played_at: i.played_at,
404
+ uri: i.track.uri
405
+ }));
406
+ output(items, opts.json);
407
+ }
408
+
409
+ // src/commands/library.ts
410
+ var LIBRARY_READ_SCOPES = ["user-library-read"];
411
+ var LIBRARY_WRITE_SCOPES = ["user-library-read", "user-library-modify"];
412
+ async function libraryListCommand(opts) {
413
+ requireScopes(LIBRARY_READ_SCOPES);
414
+ const limit = opts.limit || "20";
415
+ const offset = opts.offset || "0";
416
+ const data = await spotify(
417
+ `/me/tracks?limit=${limit}&offset=${offset}`
418
+ );
419
+ const items = data.items.map((i) => ({
420
+ id: i.track.id,
421
+ name: i.track.name,
422
+ artist: i.track.artists.map((a) => a.name).join(", "),
423
+ album: i.track.album.name,
424
+ added_at: i.added_at,
425
+ uri: i.track.uri
426
+ }));
427
+ output(items, opts.json);
428
+ }
429
+ async function librarySaveCommand(opts) {
430
+ requireScopes(LIBRARY_WRITE_SCOPES);
431
+ const ids = opts.tracks.split(",").map((t) => t.trim().replace("spotify:track:", ""));
432
+ await spotify("/me/tracks", {
433
+ method: "PUT",
434
+ body: JSON.stringify({ ids })
435
+ });
436
+ console.log(`Saved ${ids.length} track(s) to library`);
437
+ }
438
+ async function libraryRemoveCommand(opts) {
439
+ requireScopes(LIBRARY_WRITE_SCOPES);
440
+ const ids = opts.tracks.split(",").map((t) => t.trim().replace("spotify:track:", ""));
441
+ await spotify("/me/tracks", {
442
+ method: "DELETE",
443
+ body: JSON.stringify({ ids })
444
+ });
445
+ console.log(`Removed ${ids.length} track(s) from library`);
446
+ }
447
+ async function libraryCheckCommand(opts) {
448
+ requireScopes(LIBRARY_READ_SCOPES);
449
+ const ids = opts.tracks.split(",").map((t) => t.trim().replace("spotify:track:", ""));
450
+ const data = await spotify(`/me/tracks/contains?ids=${ids.join(",")}`);
451
+ const items = ids.map((id, i) => ({
452
+ id,
453
+ saved: data[i]
454
+ }));
455
+ output(items, opts.json);
456
+ }
457
+
294
458
  // src/commands/me.ts
295
459
  async function meCommand(opts) {
296
460
  const user = await spotify("/me");
@@ -305,6 +469,152 @@ async function meCommand(opts) {
305
469
  output(result, opts.json);
306
470
  }
307
471
 
472
+ // src/commands/player.ts
473
+ var PLAYER_SCOPES = [
474
+ "user-read-playback-state",
475
+ "user-modify-playback-state",
476
+ "user-read-currently-playing"
477
+ ];
478
+ function handlePremiumError(err) {
479
+ if (err instanceof Error && err.message.includes("403")) {
480
+ console.error("Spotify Premium required for playback control.");
481
+ process.exit(1);
482
+ }
483
+ throw err;
484
+ }
485
+ async function playerNowCommand(opts) {
486
+ requireScopes(PLAYER_SCOPES);
487
+ const data = await spotify(
488
+ "/me/player/currently-playing"
489
+ );
490
+ if (!data || !("item" in data)) {
491
+ output({ playing: false }, opts.json);
492
+ return;
493
+ }
494
+ const result = {
495
+ playing: data.is_playing,
496
+ track: data.item.name,
497
+ artist: data.item.artists.map((a) => a.name).join(", "),
498
+ album: data.item.album.name,
499
+ progress_ms: data.progress_ms,
500
+ duration_ms: data.item.duration_ms,
501
+ device: data.device?.name,
502
+ shuffle: data.shuffle_state,
503
+ repeat: data.repeat_state,
504
+ uri: data.item.uri
505
+ };
506
+ output(result, opts.json);
507
+ }
508
+ async function playerPlayCommand(opts) {
509
+ requireScopes(PLAYER_SCOPES);
510
+ try {
511
+ const params = opts.device ? `?device_id=${opts.device}` : "";
512
+ const body = {};
513
+ if (opts.uri) {
514
+ if (opts.uri.includes(":track:")) {
515
+ body.uris = [opts.uri];
516
+ } else {
517
+ body.context_uri = opts.uri;
518
+ }
519
+ }
520
+ await spotify(`/me/player/play${params}`, {
521
+ method: "PUT",
522
+ body: Object.keys(body).length > 0 ? JSON.stringify(body) : void 0
523
+ });
524
+ console.log("Playing");
525
+ } catch (err) {
526
+ handlePremiumError(err);
527
+ }
528
+ }
529
+ async function playerPauseCommand() {
530
+ requireScopes(PLAYER_SCOPES);
531
+ try {
532
+ await spotify("/me/player/pause", { method: "PUT" });
533
+ console.log("Paused");
534
+ } catch (err) {
535
+ handlePremiumError(err);
536
+ }
537
+ }
538
+ async function playerNextCommand() {
539
+ requireScopes(PLAYER_SCOPES);
540
+ try {
541
+ await spotify("/me/player/next", { method: "POST" });
542
+ console.log("Skipped to next");
543
+ } catch (err) {
544
+ handlePremiumError(err);
545
+ }
546
+ }
547
+ async function playerPrevCommand() {
548
+ requireScopes(PLAYER_SCOPES);
549
+ try {
550
+ await spotify("/me/player/previous", { method: "POST" });
551
+ console.log("Skipped to previous");
552
+ } catch (err) {
553
+ handlePremiumError(err);
554
+ }
555
+ }
556
+ async function playerQueueCommand(uri) {
557
+ requireScopes(PLAYER_SCOPES);
558
+ const trackUri = uri.startsWith("spotify:") ? uri : `spotify:track:${uri}`;
559
+ try {
560
+ await spotify(`/me/player/queue?uri=${encodeURIComponent(trackUri)}`, {
561
+ method: "POST"
562
+ });
563
+ console.log(`Added to queue: ${trackUri}`);
564
+ } catch (err) {
565
+ handlePremiumError(err);
566
+ }
567
+ }
568
+ async function playerDevicesCommand(opts) {
569
+ requireScopes(PLAYER_SCOPES);
570
+ const data = await spotify("/me/player/devices");
571
+ const items = data.devices.map((d) => ({
572
+ id: d.id,
573
+ name: d.name,
574
+ type: d.type,
575
+ active: d.is_active,
576
+ volume: d.volume_percent
577
+ }));
578
+ output(items, opts.json);
579
+ }
580
+ async function playerVolumeCommand(percent) {
581
+ requireScopes(PLAYER_SCOPES);
582
+ const vol = Number.parseInt(percent);
583
+ if (Number.isNaN(vol) || vol < 0 || vol > 100) {
584
+ console.error("Volume must be 0-100");
585
+ process.exit(1);
586
+ }
587
+ try {
588
+ await spotify(`/me/player/volume?volume_percent=${vol}`, { method: "PUT" });
589
+ console.log(`Volume: ${vol}%`);
590
+ } catch (err) {
591
+ handlePremiumError(err);
592
+ }
593
+ }
594
+ async function playerShuffleCommand(state) {
595
+ requireScopes(PLAYER_SCOPES);
596
+ const enabled = state === "on" || state === "true";
597
+ try {
598
+ await spotify(`/me/player/shuffle?state=${enabled}`, { method: "PUT" });
599
+ console.log(`Shuffle: ${enabled ? "on" : "off"}`);
600
+ } catch (err) {
601
+ handlePremiumError(err);
602
+ }
603
+ }
604
+ async function playerRepeatCommand(state) {
605
+ requireScopes(PLAYER_SCOPES);
606
+ if (!["off", "track", "context"].includes(state)) {
607
+ console.error("Repeat state must be: off, track, or context");
608
+ process.exit(1);
609
+ }
610
+ try {
611
+ await spotify(`/me/player/repeat?state=${state}`, { method: "PUT" });
612
+ console.log(`Repeat: ${state}`);
613
+ } catch (err) {
614
+ handlePremiumError(err);
615
+ }
616
+ }
617
+
308
618
  // src/commands/playlist.ts
309
619
  async function playlistListCommand(opts) {
310
620
  const data = await spotify(
@@ -355,18 +665,18 @@ async function recommendCommand(opts) {
355
665
  }
356
666
  if (opts.seedArtists) {
357
667
  for (const artistId of opts.seedArtists.split(",")) {
358
- const artist = await spotify(
668
+ const artist2 = await spotify(
359
669
  `/artists/${artistId.trim()}`
360
670
  );
361
- queries.push(`artist:${artist.name}`);
671
+ queries.push(`artist:${artist2.name}`);
362
672
  }
363
673
  }
364
674
  if (opts.seedTracks) {
365
675
  for (const trackId of opts.seedTracks.split(",")) {
366
- const track = await spotify(
676
+ const track2 = await spotify(
367
677
  `/tracks/${trackId.trim()}`
368
678
  );
369
- queries.push(`artist:${track.artists[0].name}`);
679
+ queries.push(`artist:${track2.artists[0].name}`);
370
680
  }
371
681
  }
372
682
  if (queries.length === 0) {
@@ -434,14 +744,127 @@ async function searchCommand(query, opts) {
434
744
  }
435
745
  }
436
746
 
747
+ // src/commands/top.ts
748
+ var TOP_SCOPES = ["user-top-read"];
749
+ async function topTracksCommand(opts) {
750
+ requireScopes(TOP_SCOPES);
751
+ const range = opts.range || "medium_term";
752
+ const limit = opts.limit || "20";
753
+ const data = await spotify(
754
+ `/me/top/tracks?time_range=${range}&limit=${limit}`
755
+ );
756
+ const items = data.items.map((t, i) => ({
757
+ rank: i + 1,
758
+ id: t.id,
759
+ name: t.name,
760
+ artist: t.artists.map((a) => a.name).join(", "),
761
+ album: t.album.name,
762
+ popularity: t.popularity,
763
+ uri: t.uri
764
+ }));
765
+ output(items, opts.json);
766
+ }
767
+ async function topArtistsCommand(opts) {
768
+ requireScopes(TOP_SCOPES);
769
+ const range = opts.range || "medium_term";
770
+ const limit = opts.limit || "20";
771
+ const data = await spotify(
772
+ `/me/top/artists?time_range=${range}&limit=${limit}`
773
+ );
774
+ const items = data.items.map((a, i) => ({
775
+ rank: i + 1,
776
+ id: a.id,
777
+ name: a.name,
778
+ genres: a.genres.join(", "),
779
+ popularity: a.popularity,
780
+ uri: a.uri
781
+ }));
782
+ output(items, opts.json);
783
+ }
784
+
785
+ // src/commands/track.ts
786
+ var KEY_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"];
787
+ async function trackGetCommand(id, opts) {
788
+ const data = await spotify(`/tracks/${id}`);
789
+ const result = {
790
+ id: data.id,
791
+ name: data.name,
792
+ artist: data.artists.map((a) => a.name).join(", "),
793
+ album: data.album.name,
794
+ released: data.album.release_date,
795
+ duration_ms: data.duration_ms,
796
+ popularity: data.popularity,
797
+ explicit: data.explicit,
798
+ uri: data.uri,
799
+ url: data.external_urls.spotify
800
+ };
801
+ output(result, opts.json);
802
+ }
803
+ async function trackFeaturesCommand(id, opts) {
804
+ let data;
805
+ try {
806
+ data = await spotify(`/audio-features/${id}`);
807
+ } catch (err) {
808
+ if (err instanceof Error && err.message.includes("403")) {
809
+ console.error("Audio features endpoint is restricted by Spotify. This may require an approved app.");
810
+ process.exit(1);
811
+ }
812
+ throw err;
813
+ }
814
+ const result = {
815
+ danceability: data.danceability,
816
+ energy: data.energy,
817
+ key: KEY_NAMES[data.key] ?? data.key,
818
+ mode: data.mode === 1 ? "major" : "minor",
819
+ loudness: data.loudness,
820
+ speechiness: data.speechiness,
821
+ acousticness: data.acousticness,
822
+ instrumentalness: data.instrumentalness,
823
+ liveness: data.liveness,
824
+ valence: data.valence,
825
+ tempo: Math.round(data.tempo),
826
+ time_signature: data.time_signature,
827
+ duration_ms: data.duration_ms
828
+ };
829
+ output(result, opts.json);
830
+ }
831
+
437
832
  // src/index.ts
438
833
  var program = new Command();
439
- program.name("spoti-cli").description("Spotify Web API from your terminal").version("0.1.0");
440
- program.command("auth").description("Authenticate with Spotify (OAuth2 PKCE)").option("-c, --client-id <id>", "Spotify app client ID").action((opts) => authCommand(opts.clientId));
834
+ program.name("spoti-cli").description("Spotify Web API from your terminal").version("0.2.0");
835
+ program.command("auth").description("Authenticate with Spotify (OAuth2 PKCE)").option("-c, --client-id <id>", "Spotify app client ID").option("-u, --upgrade", "Re-authenticate with expanded permissions", false).action((opts) => authCommand(opts.clientId, opts.upgrade));
441
836
  program.command("search <query>").description("Search for tracks, artists, or albums").option("-t, --type <type>", "Search type: track, artist, album", "track").option("-l, --limit <n>", "Number of results", "10").option("--json", "Output as JSON", false).action((query, opts) => searchCommand(query, opts));
442
837
  program.command("recommend").description("Get track recommendations based on seeds").option("--seed-tracks <ids>", "Comma-separated track IDs").option("--seed-artists <ids>", "Comma-separated artist IDs").option("--seed-genres <genres>", "Comma-separated genres").option("-l, --limit <n>", "Number of recommendations", "20").option("--energy <n>", "Target energy (0.0-1.0)").option("--danceability <n>", "Target danceability (0.0-1.0)").option("--valence <n>", "Target valence/positiveness (0.0-1.0)").option("--tempo <n>", "Target tempo in BPM").option("--popularity <n>", "Target popularity (0-100)").option("--acousticness <n>", "Target acousticness (0.0-1.0)").option("--instrumentalness <n>", "Target instrumentalness (0.0-1.0)").option("--json", "Output as JSON", false).action((opts) => recommendCommand(opts));
443
838
  program.command("create <name>").description("Create a new playlist").option("-d, --description <text>", "Playlist description").option("--public", "Make playlist public", false).option("--tracks <uris>", "Comma-separated track URIs or IDs to add").option("-o, --open", "Open playlist in Spotify app after creation", false).option("--json", "Output as JSON", false).action((name, opts) => createCommand(name, opts));
444
839
  program.command("me").description("Show current user profile").option("--json", "Output as JSON", false).action((opts) => meCommand(opts));
840
+ program.command("history").description("Recently played tracks").option("-l, --limit <n>", "Number of tracks (max 50)", "20").option("--json", "Output as JSON", false).action((opts) => historyCommand(opts));
841
+ var player = program.command("player").description("Control playback");
842
+ player.command("now").description("Show currently playing track").option("--json", "Output as JSON", false).action((opts) => playerNowCommand(opts));
843
+ player.command("play").description("Start or resume playback").option("--uri <uri>", "Track, album, or playlist URI to play").option("--device <id>", "Device ID to play on").option("--json", "Output as JSON", false).action((opts) => playerPlayCommand(opts));
844
+ player.command("pause").description("Pause playback").action(() => playerPauseCommand());
845
+ player.command("next").description("Skip to next track").action(() => playerNextCommand());
846
+ player.command("prev").description("Skip to previous track").action(() => playerPrevCommand());
847
+ player.command("queue <uri>").description("Add a track to the queue").action((uri) => playerQueueCommand(uri));
848
+ player.command("devices").description("List available devices").option("--json", "Output as JSON", false).action((opts) => playerDevicesCommand(opts));
849
+ player.command("volume <percent>").description("Set volume (0-100)").action((percent) => playerVolumeCommand(percent));
850
+ player.command("shuffle <state>").description("Toggle shuffle (on/off)").action((state) => playerShuffleCommand(state));
851
+ player.command("repeat <state>").description("Set repeat mode (off/track/context)").action((state) => playerRepeatCommand(state));
852
+ var top = program.command("top").description("Your top tracks and artists");
853
+ top.command("tracks").description("Your most listened tracks").option("-r, --range <range>", "Time range: short_term, medium_term, long_term", "medium_term").option("-l, --limit <n>", "Number of results", "20").option("--json", "Output as JSON", false).action((opts) => topTracksCommand(opts));
854
+ top.command("artists").description("Your most listened artists").option("-r, --range <range>", "Time range: short_term, medium_term, long_term", "medium_term").option("-l, --limit <n>", "Number of results", "20").option("--json", "Output as JSON", false).action((opts) => topArtistsCommand(opts));
855
+ var library = program.command("library").description("Manage saved tracks");
856
+ library.command("list").description("List saved tracks").option("-l, --limit <n>", "Number of tracks", "20").option("--offset <n>", "Offset for pagination", "0").option("--json", "Output as JSON", false).action((opts) => libraryListCommand(opts));
857
+ library.command("save").description("Save tracks to library").requiredOption("--tracks <ids>", "Comma-separated track IDs or URIs").action((opts) => librarySaveCommand(opts));
858
+ library.command("remove").description("Remove tracks from library").requiredOption("--tracks <ids>", "Comma-separated track IDs or URIs").action((opts) => libraryRemoveCommand(opts));
859
+ library.command("check").description("Check if tracks are in library").requiredOption("--tracks <ids>", "Comma-separated track IDs or URIs").option("--json", "Output as JSON", false).action((opts) => libraryCheckCommand(opts));
860
+ var track = program.command("track").description("Track details and audio features");
861
+ track.command("get <id>").description("Get track details").option("--json", "Output as JSON", false).action((id, opts) => trackGetCommand(id, opts));
862
+ track.command("features <id>").description("Get audio features (energy, danceability, tempo, key, etc.)").option("--json", "Output as JSON", false).action((id, opts) => trackFeaturesCommand(id, opts));
863
+ var artist = program.command("artist").description("Artist details and discography");
864
+ artist.command("get <id>").description("Get artist details").option("--json", "Output as JSON", false).action((id, opts) => artistGetCommand(id, opts));
865
+ artist.command("top-tracks <id>").description("Get artist's top tracks").option("-m, --market <code>", "Market/country code", "US").option("--json", "Output as JSON", false).action((id, opts) => artistTopTracksCommand(id, opts));
866
+ artist.command("albums <id>").description("Get artist's albums").option("-l, --limit <n>", "Number of albums", "20").option("--json", "Output as JSON", false).action((id, opts) => artistAlbumsCommand(id, opts));
867
+ artist.command("related <id>").description("Get related artists").option("--json", "Output as JSON", false).action((id, opts) => artistRelatedCommand(id, opts));
445
868
  var playlist = program.command("playlist").description("Manage playlists");
446
869
  playlist.command("list").description("List your playlists").option("-l, --limit <n>", "Number of playlists", "20").option("--json", "Output as JSON", false).action((opts) => playlistListCommand(opts));
447
870
  playlist.command("get <id>").description("Get playlist details and tracks").option("--json", "Output as JSON", false).action((id, opts) => playlistGetCommand(id, opts));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@crafter/spoti-cli",
3
- "version": "0.1.1",
4
- "description": "Spotify Web API from your terminal. Search, recommend, and create playlists.",
3
+ "version": "0.2.0",
4
+ "description": "Spotify Web API from your terminal. Search, playback, library, audio features, and playlists.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
@@ -30,7 +30,11 @@
30
30
  "cli",
31
31
  "playlist",
32
32
  "music",
33
- "terminal"
33
+ "terminal",
34
+ "playback",
35
+ "player",
36
+ "library",
37
+ "audio-features"
34
38
  ],
35
39
  "license": "MIT",
36
40
  "repository": {