extern crate libnotify; extern crate mpd; use clap::{Parser, Subcommand}; use libnotify::Notification; use mpd::{Client, Query, Song, State, Term}; use std::time::Duration; #[derive(Parser)] #[command(author, version, about, long_about = "Banaan")] #[command(propagate_version = true)] struct Cli { #[command(subcommand)] command: Option, #[arg(short, long, global = true)] verbose: bool, ///hostname where MPD listens at #[arg(short = 'H', long, global = true)] host: Option, } #[derive(Subcommand)] enum Commands { /// Toggle MPD stream Toggle, /// Skip to the next track Next, /// Revert to the previous track Prev, /// Stops playing Stop, /// Play queueueu Play { track: Option }, /// Set or get crossfade Crossfade { seconds: Option }, ///Update still needs some work Update, /// Return currently playing song Current, /// Clear current queueueu Clear, /// Query database Search { ///Search query #[arg(trailing_var_arg = true)] query: Vec, ///Only return the first n results #[arg(short, long)] max: Option, // #[arg(short, long)] // append: bool, // #[arg(short, long)] // insert: Option, }, /// Query database differently Find { ///Search query #[arg(trailing_var_arg = true)] query: Vec, ///Only return the first n results #[arg(short, long)] max: Option, #[arg(short, long)] append: bool, }, /// List items in the current queueueu List { #[arg(short, long)] file: bool, }, // Add { // #[arg(short, long)] // position: Option, // }, /// Shuffles the current queueue Shuffle, } fn main() { libnotify::init("noise").unwrap(); let n = libnotify::Notification::new("Noise", None, None); let cli = Cli::parse(); let host = match cli.host { Some(host) => host, None => String::from("localhost:6600"), }; let mut conn = Client::connect(host).expect("Connection failed"); if let Some(cmd) = &cli.command { match cmd { Commands::Play { track } => { if let Some(i) = track { conn.switch(*i).unwrap(); } else { conn.play().unwrap(); } } Commands::Stop => { conn.stop().unwrap(); } Commands::Toggle => { if conn.status().unwrap().state == State::Stop { conn.play().unwrap(); } else { conn.toggle_pause().unwrap(); } } Commands::Next => conn.next().unwrap(), Commands::Prev => conn.prev().unwrap(), Commands::List { file } => conn.queue().unwrap().iter().for_each(|x| { println!( "{:<02} {}", x.place.unwrap().pos, if *file { x.file.clone() } else { format_song(x.clone()) } ) }), Commands::Update => { let thing = conn.update().unwrap(); println!("{thing}") } Commands::Current => (), // this is basically the same as having no command Commands::Clear => conn.clear().unwrap(), Commands::Search { query, max } => { let query = query.join(" "); let max = max.unwrap_or(conn.stats().unwrap().songs); conn.search(&Query::new().and(Term::Any, query), (0, max)) .unwrap() .iter() .for_each(|x| println!("{}", x.file)); } Commands::Find { query, max, append } => { let query = query.join(" "); let max = max.unwrap_or(conn.stats().unwrap().songs); if *append { conn.findadd(&Query::new().and(Term::Any, query)).unwrap(); } else { conn.find(&Query::new().and(Term::Any, query), (0, max)) .unwrap() .iter() .for_each(|x| println!("{}", x.file)); } } Commands::Crossfade { seconds } => { if let Some(crossfade) = seconds { conn.crossfade(*crossfade).unwrap(); } else { println!( "Crossfade: {}", conn.status() .unwrap() .crossfade .map_or("Disabled".into(), |d| d.as_secs().to_string()) ); } } Commands::Shuffle => { send_notification("Shuffling queueueu...", &n); println!("Shuffling queueueu..."); conn.shuffle(..).unwrap(); } } if let Commands::Stop | Commands::List { .. } | Commands::Update | Commands::Clear | Commands::Search { .. } | Commands::Find { .. } | Commands::Crossfade { .. } | Commands::Shuffle = cmd { return; } } println!("{}", get_status(conn, cli.verbose, n)); } fn format_song(song: Song) -> String { format!("{} - {}", song.artist.unwrap(), song.title.unwrap()) } trait QuickFmt { fn quick_fmt(&self) -> &'static str; } impl QuickFmt for bool { fn quick_fmt(&self) -> &'static str { if *self { "on" } else { "off" } } } fn get_status(mut client: Client, verbose: bool, notify: Notification) -> String { let current_song = if let Some(song) = client.currentsong().unwrap() { format_song(song) } else { "No song playing".into() }; send_notification(¤t_song, ¬ify); if !verbose { return current_song; } let mut output = Vec::with_capacity(3); output.push(current_song); let status = client.status().unwrap(); output.push(format!( "volume: {vol:<6} repeat: {repeat:<6} random: {rnd:<6} single: {single:<6} consume: {consume:<6}", vol = format!("{}%", status.volume), repeat = status.repeat.quick_fmt(), rnd = status.random.quick_fmt(), single = status.single.quick_fmt(), consume = status.consume.quick_fmt() )); let state = match status.state { State::Stop => "stopped", State::Pause => "paused", State::Play => "playing", }; let duration = status.time.map_or("".into(), |(stamp, total)| { let stamp = to_mins_string(stamp); let total = to_mins_string(total); format!("{stamp}/{total}") }); output.push(format!("[{state}] {duration}")); output.join("\n") } fn send_notification(body: &str, notify: &Notification) { let _ = notify.update("Noise", body, None); notify.show().unwrap(); } fn to_mins_string(dur: Duration) -> String { let seconds = dur.as_secs(); let minutes = seconds / 60; let rem_seconds = seconds % 60; format!("{minutes:02}:{rem_seconds:02}") }