From 202542ad29bfb0568eb02051416d4bcb30f8daec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Boh=C3=A1=C4=8Dek?= Date: Sat, 8 Jul 2023 20:51:01 +0200 Subject: [PATCH] feat(cli): add basic cli handling --- cli/.projectile | 0 cli/Cargo.toml | 16 +++ cli/src/main.rs | 252 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 268 insertions(+) create mode 100644 cli/.projectile create mode 100644 cli/Cargo.toml create mode 100644 cli/src/main.rs diff --git a/cli/.projectile b/cli/.projectile new file mode 100644 index 0000000..e69de29 diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 0000000..10e6dae --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "mpris-ctl" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +clap = { version = "4.3.11", features = ["derive"] } +futures = { version = "0.3.28", features = ["std"] } +mpris = "2.0.1" +serde = { version = "1.0.167", features = ["derive"] } +serde_json = { version = "1.0.100", features = ["std"] } +sorted-iter = "0.1.11" +tokio = { version = "1.29.1", features = ["net", "macros", "rt", "rt-multi-thread", "io-util", "time"] } +tokio-util = { version = "0.7.8", features = ["codec"] } diff --git a/cli/src/main.rs b/cli/src/main.rs new file mode 100644 index 0000000..026ef0d --- /dev/null +++ b/cli/src/main.rs @@ -0,0 +1,252 @@ +use std::path::PathBuf; + +use clap::{Args, Parser, Subcommand}; +use mpris::{Player, PlayerFinder, PlaybackStatus, MetadataValueKind}; +use serde::{Serialize, Deserialize}; +use tokio::{net::UnixStream, io}; +use tokio_util::codec::{Framed, LengthDelimitedCodec}; + +use futures::{prelude::stream::StreamExt, SinkExt}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum Request { + GetLastActive, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum Response { + None, + Players(Vec), +} + +#[derive(Debug, Parser)] +#[command(version = "v0.1")] +#[command(author = "Rutherther")] +#[command(about = "Manage dbus mpris2 players")] +pub struct Cli { + #[command(flatten)] + pub player_selector: PlayerSelector, + + #[arg(short = 's', long, default_value = r"/tmp/mpris-ctl.sock", value_hint = clap::ValueHint::FilePath)] + pub socket: PathBuf, + + #[command(subcommand)] + pub command: Commands, +} + +#[derive(Debug, Args)] +pub struct PlayerSelector { + #[arg(long)] + pub all_players: bool, + #[arg(long)] + pub player: Vec, +} + +#[derive(Debug, Subcommand, Eq, PartialEq)] +pub enum Commands { + #[command(about = "Send play media command")] + Play, + #[command(about = "Send pause media command")] + Pause, + #[command(about = "Send play if paused, else send pause")] + Toggle, + #[command(about = "Switch to previous media/song")] + Prev, + #[command(about = "Switch to next media/song")] + Next, + #[command(about = "Obtain metadata of the currently playing media")] + Metadata(Metadata), + #[command(about = "Obtain status of the currently active player")] + Status, + #[command(about = "List all available players")] + List +} + +#[derive(Debug, Args, Eq, PartialEq)] +pub struct Metadata { + #[arg(help = "Key of the metadata to obtain, else all information will be obtained.")] + pub key: Option +} + +#[tokio::main] +async fn main() { + let args = Cli::parse(); + + let selected_players = obtain_selected_players(&args.socket, args.player_selector).await; + + if selected_players.len() == 0 { + println!("No players matching the criteria found."); + return; + } + + match args.command { + Commands::Play => { + for player in selected_players { + player.play().expect("Could not play."); + } + }, + Commands::Pause => { + for player in selected_players { + player.pause().expect("Could not pause."); + } + }, + Commands::Toggle => { + for player in selected_players { + player.play_pause().expect("Could not toggle."); + } + }, + Commands::Prev => { + for player in selected_players { + player.previous().expect("Could not return back to previous."); + } + }, + Commands::Next => { + for player in selected_players { + player.next().expect("Could not skip."); + } + }, + Commands::Status => { + let player = selected_players.first().unwrap(); + println!("{}", match player.get_playback_status().unwrap() { + PlaybackStatus::Playing => "Playing", + PlaybackStatus::Paused => "Paused", + PlaybackStatus::Stopped => "Stopped", + }); + }, + Commands::Metadata(Metadata { key: search_key }) => { + for player in selected_players { + let identity = get_short_name(player.identity()); + let metadata = player + .get_metadata() + .expect("Could not obtain metadata"); + + let mut keys = metadata + .keys() + .collect::>(); + keys.sort(); + + let metadata = keys + .iter() + .map(|key| (key, metadata.get(key).unwrap())); + + for (key, value) in metadata { + if let Some(skey) = &search_key { + if key.contains(skey) { + println!("{}", &value.as_str().unwrap_or("-")); + break; + } + } else { + println!( + "{} {} {}", + identity, + key, + match value.kind() { + MetadataValueKind::String => value.as_str().unwrap().to_string(), + MetadataValueKind::Array => value.as_str_array().map(|x| x.join(" ")).unwrap(), + MetadataValueKind::U32 | + MetadataValueKind::U16 | + MetadataValueKind::U64 => value.as_u64().unwrap().to_string(), + MetadataValueKind::I32 | + MetadataValueKind::I16 | + MetadataValueKind::I64 => value.as_i64().unwrap().to_string(), + MetadataValueKind::F64 => value.as_f64().unwrap().to_string(), + _ => "-".to_string() + } + ); + } + } + } + }, + Commands::List => { + for player in selected_players { + println!("{:?}", player.identity()); + } + } + }; +} + +fn get_short_name(name: &str) -> String { + name.split(' ') + .last() + .map(|x| x.to_lowercase()) + .unwrap_or(name.to_string()) +} + +async fn obtain_selected_players(socket: &PathBuf, selector: PlayerSelector) -> Vec { + let player_finder = PlayerFinder::new() + .expect("Could not connect to the D-Bus."); + if selector.all_players { + return player_finder + .find_all() + .expect("Could not iterate the players."); + } + + if selector.player.len() > 0 { + return player_finder + .iter_players() + .expect("Could not iterate the players.") + .map(|x| x.expect("Could not obtain player.")) + .filter(|x| selector.player.iter().any(|sel| x.identity().to_lowercase().contains(&sel.to_lowercase()))) + .collect(); + } + + let daemon_result = obtain_daemon_active_players(&player_finder, socket).await; + if let Ok(players) = daemon_result { + players + } else { + let mut players: Vec = player_finder + .iter_players() + .expect("Could not iterate the players.") + .map(|x| x.expect("Could not obtain a player.")) + .filter(|x| x.get_playback_status().expect("Could not obtain playback status") == PlaybackStatus::Playing) + .collect(); + + if players.len() == 0 { + if let Ok(player) = player_finder.find_active() { + players.push(player); + } else if let Ok(player) = player_finder.find_first() { + players.push(player); + } + } + + players + } +} + +async fn obtain_daemon_active_players(player_finder: &PlayerFinder, socket: &PathBuf) -> io::Result> { + let stream = UnixStream::connect(socket).await?; + let mut framed = Framed::new(stream, LengthDelimitedCodec::new()); + + let buffer = serde_json::to_vec(&Request::GetLastActive).unwrap(); + framed.send(buffer.into()).await.expect("Could not send request to the daemon"); + + let response = if let Some(frame) = framed.next().await { + match frame { + Ok(data) => { + let response: serde_json::Result = serde_json::from_slice(&data); + if response.is_err() { + panic!("Could not parse the frame from server: {:?}", response.err()); + } + response.unwrap() + }, + Err(err) => { + panic!("Could not read from server: {:?}", err); + } + } + } else { + panic!("Could not obtain data from the server.") + }; + + match response { + Response::Players(players) => { + player_finder + .iter_players() + .expect("Could not iterate the players.") + .map(|x| x.expect("Could not obtain a player.")) + .filter(|x| players.contains(&String::from(x.identity()))) + .map(|x| Ok(x)) + .collect() + }, + _ => panic!("Could not get active players from the daemon.") + } +} -- 2.48.1