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.") } }