@@ 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"] }
@@ 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<String>),
+}
+
+#[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<String>,
+}
+
+#[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<String>
+}
+
+#[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::<Vec<_>>();
+ 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<Player> {
+ 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> = 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<Vec<Player>> {
+ 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<Response> = 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.")
+ }
+}