use anyhow::{anyhow, Context, Error, Result}; use mpris::{Player, PlayerFinder, TrackID}; fn main() { match run() { Ok(_) => {} Err(error) => { println!("Error: {}", error); for (i, cause) in error.chain().skip(1).enumerate() { print!("{}", " ".repeat(i + 1)); println!("Caused by: {}", cause); } std::process::exit(1); } } } fn prompt_string(message: &str) -> Result { use std::io::stdin; let mut answer = String::new(); println!("{}", message); stdin().read_line(&mut answer)?; Ok(String::from(answer.trim())) } fn run() -> Result<()> { let player_finder = PlayerFinder::new().context("Could not connect to D-Bus")?; let player = player_finder .find_active() .context("Could not find any player")?; println!( "Found {identity} (on bus {bus_name})", bus_name = player.bus_name(), identity = player.identity(), ); if !player.supports_track_lists() { println!("Player does not support TrackList"); return Ok(()); } loop { let answer = prompt_string("What to do? [q]uit, [g]oto, [l]ist, [a]dd, [r]emove >")?; match answer.as_str() { "q" | "Q" => break, "l" | "L" => print_track_list(&player).context("Failed to list tracks")?, "g" | "G" => goto_track(&player).context("Failed to change track")?, "a" | "A" => add_track(&player).context("Failed to add track")?, "r" | "R" => remove_track(&player).context("Failed to remove track")?, _ => println!("I don't understand \"{}\"", answer), } } Ok(()) } fn print_track_list(player: &Player) -> Result<()> { let track_list = player.get_track_list()?; println!("Track list:\n"); let iter = track_list .metadata_iter(player) .context("Could not load metadata for tracks")?; for (index, metadata) in iter.enumerate() { let title = metadata.title().unwrap_or("Unknown title"); let artist = metadata .artists() .map(|list| list.join(", ")) .unwrap_or_else(|| "Unknown artist".into()); println!("{}. {} - {}", index + 1, artist, title); } Ok(()) } fn select_track(player: &Player, lower_bound: usize) -> Result> { let track_list = player .get_track_list() .context("Could not get track list for player")?; let len = track_list.len(); let answer = prompt_string(&format!( "Select track index [{}-{}, q] > ", lower_bound, len ))?; if answer.is_empty() || answer == "q" { return Ok(None); } let number: usize = answer.parse::().context("Not a valid number")?; if number == 0 { return Ok(None); } let track_id = track_list .get(number - 1) .ok_or_else(|| anyhow!("Not a valid position"))?; Ok(Some(track_id.clone())) } fn goto_track(player: &Player) -> Result<()> { match select_track(player, 1) { Ok(Some(track_id)) => player.go_to(&track_id).map_err(Error::from), Ok(None) => Ok(()), Err(err) => Err(err.context("Failed to select track")), } } fn remove_track(player: &Player) -> Result<()> { match select_track(player, 1) { Ok(Some(track_id)) => player.remove_track(&track_id).map_err(Error::from), Ok(None) => Ok(()), Err(err) => Err(err.context("Failed to select track")), } } fn add_track(player: &Player) -> Result<()> { println!("NOTE: To add local media, start with the \"file://\" protocol. E.x. \"file:///path/to/file.mp3\""); let uri = prompt_string("Enter URI (or nothing to cancel) > ")?; if uri.is_empty() { return Ok(()); } println!( "Will be inserted after selected track. Select no track (0) to insert at the beginning." ); match select_track(player, 0) { Ok(Some(track_id)) => player .add_track(&uri, &track_id, false) .map_err(Error::from), Ok(None) => player.add_track_at_start(&uri, false).map_err(Error::from), Err(err) => Err(err) .context("Failed to select track") .map_err(Error::from), } }