Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5b8b0c0
implement a simple "get playback" cli command
aome510 Mar 17, 2023
d7b0437
implement get commands for several keys
aome510 Mar 17, 2023
80a2106
add and parse some `playback` commands
aome510 Mar 18, 2023
b991dd2
handle pause,next,shuffle,repeat commands
aome510 Mar 18, 2023
0fa1cd2
refactor cli codes and
aome510 Mar 18, 2023
602641d
refactor `get` command handler codes using `clap::ValueEnum`
aome510 Mar 18, 2023
17189c8
add `UserSavedAlbums` and `UserFollowedArtists` to the supported keys
aome510 Mar 18, 2023
6b9746d
add basic handler for playback play command
aome510 Mar 18, 2023
e42afe9
implement get context command
aome510 Mar 18, 2023
b88f1cd
allow volume to be 100
aome510 Mar 19, 2023
60f6e16
replace `pause` and `resume` command with `resume-pause`
aome510 Mar 19, 2023
f16bd89
rename commands and cleanup commands' document
aome510 Mar 19, 2023
a4360c8
Merge branch 'master' into implement-app-api
aome510 Apr 20, 2023
c88b871
make `cli` a folder module
aome510 Apr 20, 2023
4a418e6
refactor `cli` module into multiple files
aome510 Apr 20, 2023
7db2dfe
update codes for `get` commands to use socket
aome510 Apr 21, 2023
84d7b5d
handle `playback` commands with socket
aome510 Apr 21, 2023
490d051
implement `Debug` for `Request` struct
aome510 Apr 21, 2023
ceb8cb9
use `tokio::net::UdpSocket` to avoid blocking the tokio thread
aome510 Apr 21, 2023
ae4ed71
require application's state in `start_socket` function
aome510 Apr 21, 2023
b7a7f18
remove redundant `async` declaration
aome510 Apr 21, 2023
1f9ffca
tweak error message
aome510 Apr 21, 2023
6a41eb4
handle sending/receiving UDP data
aome510 Apr 21, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
387 changes: 243 additions & 144 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion spotify_player/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ readme = "../README.md"

[dependencies]
anyhow = "1.0.70"
clap = "4.2.0"
clap = { version = "4.2.0", features = ["derive"] }
config_parser2 = "0.1.4"
crossterm = "0.26.1"
dirs-next = "2.0.0"
Expand Down
67 changes: 67 additions & 0 deletions spotify_player/src/cli/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use clap::{builder::EnumValueParser, value_parser, Arg, Command};

use super::{ContextType, Key};

pub fn init_get_subcommand() -> Command {
Command::new("get")
.about("Get spotify data")
.subcommand_required(true)
.subcommand(
Command::new("key").about("Get data by key").arg(
Arg::new("key")
.value_parser(EnumValueParser::<Key>::new())
.required(true),
),
)
.subcommand(
Command::new("context")
.about("Get context data")
.arg(
Arg::new("context_type")
.value_parser(EnumValueParser::<ContextType>::new())
.required(true),
)
.arg(Arg::new("context_id").required(true)),
)
}

fn init_playback_start_subcommand() -> Command {
Command::new("start")
.about("Start a context playback")
.arg(
Arg::new("context_type")
.value_parser(EnumValueParser::<ContextType>::new())
.required(true),
)
.arg(Arg::new("context_id").required(true))
}

pub fn init_playback_subcommand() -> Command {
Command::new("playback")
.about("Interact with the playback")
.subcommand_required(true)
.subcommand(init_playback_start_subcommand())
.subcommand(Command::new("play-pause").about("Toggle between play and pause"))
.subcommand(Command::new("next").about("Skip to the next track"))
.subcommand(Command::new("previous").about("Skip to the previous track"))
.subcommand(Command::new("shuffle").about("Toggle the shuffle mode"))
.subcommand(Command::new("repeat").about("Cycle the repeat mode"))
.subcommand(
Command::new("volume")
.about("Set the volume percentage")
.arg(
Arg::new("percent")
.value_parser(value_parser!(u8).range(0..=100))
.required(true),
),
)
.subcommand(
Command::new("seek")
.about("Seek by an offset milliseconds")
.arg(
Arg::new("position_offset_ms")
.value_parser(value_parser!(i32))
.required(true),
),
)
}
102 changes: 102 additions & 0 deletions spotify_player/src/cli/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use super::*;
use anyhow::Result;
use clap::ArgMatches;
use std::net::UdpSocket;

fn receive_data(socket: &UdpSocket) -> Result<Vec<u8>> {
// read response from the server's socket, which can be splitted into
// smaller chunks of data
let mut data = Vec::new();
let mut buf = [0; 4096];
loop {
let (n_bytes, _) = socket.recv_from(&mut buf)?;
if n_bytes == 0 {
// end of chunk
break;
}
data.extend_from_slice(&buf[..n_bytes]);
}

Ok(data)
}

fn handle_get_subcommand(args: &ArgMatches, socket: UdpSocket) -> Result<()> {
let (cmd, args) = args.subcommand().expect("playback subcommand is required");

let request = match cmd {
"key" => {
let key = args
.get_one::<Key>("key")
.expect("key is required")
.to_owned();
Request::Get(GetRequest::Key(key))
}
"context" => {
let context_id = args
.get_one::<String>("context_id")
.expect("context_id is required")
.to_owned();
let context_type = args
.get_one::<ContextType>("context_type")
.expect("context_type is required")
.to_owned();
Request::Get(GetRequest::Context(context_id, context_type))
}
_ => unreachable!(),
};

socket.send(&serde_json::to_vec(&request)?)?;
let data = receive_data(&socket)?;
println!("{}", String::from_utf8_lossy(&data));

Ok(())
}

fn handle_playback_subcommand(args: &ArgMatches, socket: UdpSocket) -> Result<()> {
let (cmd, args) = args.subcommand().expect("playback subcommand is required");
let command = match cmd {
"start" => {
let context_id = args
.get_one::<String>("context_id")
.expect("context_id is required");
let context_type = args
.get_one::<ContextType>("context_type")
.expect("context_type is required");
Command::Start(context_id.to_owned(), context_type.to_owned())
}
"play-pause" => Command::PlayPause,
"next" => Command::Next,
"previous" => Command::Previous,
"shuffle" => Command::Shuffle,
"repeat" => Command::Repeat,
"volume" => {
let percent = args
.get_one::<u8>("percent")
.expect("percent arg is required");
Command::Volume(*percent)
}
"seek" => {
let position_offset_ms = args
.get_one::<i32>("position_offset_ms")
.expect("position_offset_ms is required");
Command::Seek(*position_offset_ms)
}
_ => unreachable!(),
};

let request = Request::Playback(command);
socket.send(&serde_json::to_vec(&request)?)?;

Ok(())
}

pub fn handle_cli_subcommand(cmd: &str, args: &ArgMatches, client_port: u16) -> Result<()> {
let socket = UdpSocket::bind("127.0.0.1:0")?;
socket.connect(("127.0.0.1", client_port))?;

match cmd {
"get" => handle_get_subcommand(args, socket),
"playback" => handle_playback_subcommand(args, socket),
_ => unreachable!(),
}
}
52 changes: 52 additions & 0 deletions spotify_player/src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
mod commands;
mod handlers;
mod socket;

use serde::{Deserialize, Serialize};

pub use commands::{init_get_subcommand, init_playback_subcommand};
pub use handlers::handle_cli_subcommand;
pub use socket::start_socket;

#[derive(Debug, Serialize, Deserialize, clap::ValueEnum, Clone)]
pub enum Key {
Playback,
Devices,
UserPlaylists,
UserLikedTracks,
UserSavedAlbums,
UserFollowedArtists,
UserTopTracks,
Queue,
}

#[derive(Debug, Serialize, Deserialize, clap::ValueEnum, Clone)]
pub enum ContextType {
Playlist,
Album,
Artist,
}

#[derive(Debug, Serialize, Deserialize)]
pub enum GetRequest {
Key(Key),
Context(String, ContextType),
}

#[derive(Debug, Serialize, Deserialize)]
pub enum Command {
Start(String, ContextType),
PlayPause,
Next,
Previous,
Shuffle,
Repeat,
Volume(u8),
Seek(i32),
}

#[derive(Debug, Serialize, Deserialize)]
pub enum Request {
Get(GetRequest),
Playback(Command),
}
Loading