Skip to content

Commit 9b6dcc9

Browse files
authored
Implement spotify-player's CLI commands (#159)
Resolves #111. Partially #103. ## Changes Added CLI commands and subcommands: - `get`: ``` Get spotify data Usage: spotify_player get <COMMAND> Commands: key Get data by key context Get context data help Print this message or the help of the given subcommand(s) Options: -h, --help Print help ``` - `get key`: ``` Get data by key Usage: spotify_player get key <key> Arguments: <key> [possible values: playback, devices, user-playlists, user-liked-tracks, user-saved-albums, user-followed-artists, user-top-tracks, queue] Options: -h, --help Print help ``` - `get context` ``` Get context data Usage: spotify_player get context <context_type> <context_id> Arguments: <context_type> [possible values: playlist, album, artist] <context_id> Options: -h, --help Print help ``` - `playback` ``` Interact with the playback Usage: spotify_player playback <COMMAND> Commands: play Start a playback resume Resume the playback pause Pause the playback next Next track previous Previous track shuffle Toggle the shuffle mode repeat Cycle the repeat mode volume Set playback's volume percentage seek Seek the playback by an offset help Print this message or the help of the given subcommand(s) Options: -h, --help Print help ```
1 parent 9119383 commit 9b6dcc9

File tree

10 files changed

+765
-199
lines changed

10 files changed

+765
-199
lines changed

Cargo.lock

Lines changed: 243 additions & 144 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

spotify_player/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ readme = "../README.md"
1111

1212
[dependencies]
1313
anyhow = "1.0.70"
14-
clap = "4.2.0"
14+
clap = { version = "4.2.0", features = ["derive"] }
1515
config_parser2 = "0.1.4"
1616
crossterm = "0.26.1"
1717
dirs-next = "2.0.0"

spotify_player/src/cli/commands.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use clap::{builder::EnumValueParser, value_parser, Arg, Command};
2+
3+
use super::{ContextType, Key};
4+
5+
pub fn init_get_subcommand() -> Command {
6+
Command::new("get")
7+
.about("Get spotify data")
8+
.subcommand_required(true)
9+
.subcommand(
10+
Command::new("key").about("Get data by key").arg(
11+
Arg::new("key")
12+
.value_parser(EnumValueParser::<Key>::new())
13+
.required(true),
14+
),
15+
)
16+
.subcommand(
17+
Command::new("context")
18+
.about("Get context data")
19+
.arg(
20+
Arg::new("context_type")
21+
.value_parser(EnumValueParser::<ContextType>::new())
22+
.required(true),
23+
)
24+
.arg(Arg::new("context_id").required(true)),
25+
)
26+
}
27+
28+
fn init_playback_start_subcommand() -> Command {
29+
Command::new("start")
30+
.about("Start a context playback")
31+
.arg(
32+
Arg::new("context_type")
33+
.value_parser(EnumValueParser::<ContextType>::new())
34+
.required(true),
35+
)
36+
.arg(Arg::new("context_id").required(true))
37+
}
38+
39+
pub fn init_playback_subcommand() -> Command {
40+
Command::new("playback")
41+
.about("Interact with the playback")
42+
.subcommand_required(true)
43+
.subcommand(init_playback_start_subcommand())
44+
.subcommand(Command::new("play-pause").about("Toggle between play and pause"))
45+
.subcommand(Command::new("next").about("Skip to the next track"))
46+
.subcommand(Command::new("previous").about("Skip to the previous track"))
47+
.subcommand(Command::new("shuffle").about("Toggle the shuffle mode"))
48+
.subcommand(Command::new("repeat").about("Cycle the repeat mode"))
49+
.subcommand(
50+
Command::new("volume")
51+
.about("Set the volume percentage")
52+
.arg(
53+
Arg::new("percent")
54+
.value_parser(value_parser!(u8).range(0..=100))
55+
.required(true),
56+
),
57+
)
58+
.subcommand(
59+
Command::new("seek")
60+
.about("Seek by an offset milliseconds")
61+
.arg(
62+
Arg::new("position_offset_ms")
63+
.value_parser(value_parser!(i32))
64+
.required(true),
65+
),
66+
)
67+
}

spotify_player/src/cli/handlers.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use super::*;
2+
use anyhow::Result;
3+
use clap::ArgMatches;
4+
use std::net::UdpSocket;
5+
6+
fn receive_data(socket: &UdpSocket) -> Result<Vec<u8>> {
7+
// read response from the server's socket, which can be splitted into
8+
// smaller chunks of data
9+
let mut data = Vec::new();
10+
let mut buf = [0; 4096];
11+
loop {
12+
let (n_bytes, _) = socket.recv_from(&mut buf)?;
13+
if n_bytes == 0 {
14+
// end of chunk
15+
break;
16+
}
17+
data.extend_from_slice(&buf[..n_bytes]);
18+
}
19+
20+
Ok(data)
21+
}
22+
23+
fn handle_get_subcommand(args: &ArgMatches, socket: UdpSocket) -> Result<()> {
24+
let (cmd, args) = args.subcommand().expect("playback subcommand is required");
25+
26+
let request = match cmd {
27+
"key" => {
28+
let key = args
29+
.get_one::<Key>("key")
30+
.expect("key is required")
31+
.to_owned();
32+
Request::Get(GetRequest::Key(key))
33+
}
34+
"context" => {
35+
let context_id = args
36+
.get_one::<String>("context_id")
37+
.expect("context_id is required")
38+
.to_owned();
39+
let context_type = args
40+
.get_one::<ContextType>("context_type")
41+
.expect("context_type is required")
42+
.to_owned();
43+
Request::Get(GetRequest::Context(context_id, context_type))
44+
}
45+
_ => unreachable!(),
46+
};
47+
48+
socket.send(&serde_json::to_vec(&request)?)?;
49+
let data = receive_data(&socket)?;
50+
println!("{}", String::from_utf8_lossy(&data));
51+
52+
Ok(())
53+
}
54+
55+
fn handle_playback_subcommand(args: &ArgMatches, socket: UdpSocket) -> Result<()> {
56+
let (cmd, args) = args.subcommand().expect("playback subcommand is required");
57+
let command = match cmd {
58+
"start" => {
59+
let context_id = args
60+
.get_one::<String>("context_id")
61+
.expect("context_id is required");
62+
let context_type = args
63+
.get_one::<ContextType>("context_type")
64+
.expect("context_type is required");
65+
Command::Start(context_id.to_owned(), context_type.to_owned())
66+
}
67+
"play-pause" => Command::PlayPause,
68+
"next" => Command::Next,
69+
"previous" => Command::Previous,
70+
"shuffle" => Command::Shuffle,
71+
"repeat" => Command::Repeat,
72+
"volume" => {
73+
let percent = args
74+
.get_one::<u8>("percent")
75+
.expect("percent arg is required");
76+
Command::Volume(*percent)
77+
}
78+
"seek" => {
79+
let position_offset_ms = args
80+
.get_one::<i32>("position_offset_ms")
81+
.expect("position_offset_ms is required");
82+
Command::Seek(*position_offset_ms)
83+
}
84+
_ => unreachable!(),
85+
};
86+
87+
let request = Request::Playback(command);
88+
socket.send(&serde_json::to_vec(&request)?)?;
89+
90+
Ok(())
91+
}
92+
93+
pub fn handle_cli_subcommand(cmd: &str, args: &ArgMatches, client_port: u16) -> Result<()> {
94+
let socket = UdpSocket::bind("127.0.0.1:0")?;
95+
socket.connect(("127.0.0.1", client_port))?;
96+
97+
match cmd {
98+
"get" => handle_get_subcommand(args, socket),
99+
"playback" => handle_playback_subcommand(args, socket),
100+
_ => unreachable!(),
101+
}
102+
}

spotify_player/src/cli/mod.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
mod commands;
2+
mod handlers;
3+
mod socket;
4+
5+
use serde::{Deserialize, Serialize};
6+
7+
pub use commands::{init_get_subcommand, init_playback_subcommand};
8+
pub use handlers::handle_cli_subcommand;
9+
pub use socket::start_socket;
10+
11+
#[derive(Debug, Serialize, Deserialize, clap::ValueEnum, Clone)]
12+
pub enum Key {
13+
Playback,
14+
Devices,
15+
UserPlaylists,
16+
UserLikedTracks,
17+
UserSavedAlbums,
18+
UserFollowedArtists,
19+
UserTopTracks,
20+
Queue,
21+
}
22+
23+
#[derive(Debug, Serialize, Deserialize, clap::ValueEnum, Clone)]
24+
pub enum ContextType {
25+
Playlist,
26+
Album,
27+
Artist,
28+
}
29+
30+
#[derive(Debug, Serialize, Deserialize)]
31+
pub enum GetRequest {
32+
Key(Key),
33+
Context(String, ContextType),
34+
}
35+
36+
#[derive(Debug, Serialize, Deserialize)]
37+
pub enum Command {
38+
Start(String, ContextType),
39+
PlayPause,
40+
Next,
41+
Previous,
42+
Shuffle,
43+
Repeat,
44+
Volume(u8),
45+
Seek(i32),
46+
}
47+
48+
#[derive(Debug, Serialize, Deserialize)]
49+
pub enum Request {
50+
Get(GetRequest),
51+
Playback(Command),
52+
}

0 commit comments

Comments
 (0)