2025-06-13 13:09:18 +02:00
|
|
|
extern crate libnotify;
|
2025-06-07 11:24:20 +02:00
|
|
|
extern crate mpd;
|
|
|
|
|
2025-06-08 22:53:08 +02:00
|
|
|
use std::time::Duration;
|
2025-06-13 13:09:18 +02:00
|
|
|
use std::{io, usize};
|
2025-06-08 22:53:08 +02:00
|
|
|
|
2025-06-07 23:50:30 +02:00
|
|
|
use mpd::{Client, Query, Song, Term};
|
2025-06-07 11:24:20 +02:00
|
|
|
|
2025-06-07 11:43:42 +02:00
|
|
|
use clap::{Parser, Subcommand};
|
2025-06-07 11:24:20 +02:00
|
|
|
|
2025-06-07 11:43:42 +02:00
|
|
|
#[derive(Parser)]
|
2025-06-08 22:53:08 +02:00
|
|
|
#[command(author, version, about, long_about = "Banaan")]
|
2025-06-07 11:43:42 +02:00
|
|
|
#[command(propagate_version = true)]
|
|
|
|
|
|
|
|
struct Cli {
|
|
|
|
#[command(subcommand)]
|
2025-06-07 23:50:30 +02:00
|
|
|
command: Option<Commands>,
|
2025-06-08 22:53:08 +02:00
|
|
|
|
|
|
|
#[arg(short, long, global = true)]
|
|
|
|
verbose: bool,
|
2025-06-13 13:09:18 +02:00
|
|
|
///hostname where MPD listens at
|
2025-06-08 22:53:08 +02:00
|
|
|
#[arg(short = 'H', long, global = true)]
|
|
|
|
host: Option<String>,
|
2025-06-07 11:43:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Subcommand)]
|
|
|
|
enum Commands {
|
2025-06-07 23:50:30 +02:00
|
|
|
/// Toggle MPD stream
|
|
|
|
Toggle {},
|
|
|
|
/// Skip to the next track
|
|
|
|
Next {},
|
|
|
|
/// Revert to the previous track
|
|
|
|
Prev {},
|
|
|
|
/// Stops playing
|
|
|
|
Stop {},
|
|
|
|
/// Play queueueu
|
2025-06-08 22:53:08 +02:00
|
|
|
Play { track: Option<u32> },
|
2025-06-07 23:50:30 +02:00
|
|
|
/// Set or get crossfade
|
2025-06-08 22:53:08 +02:00
|
|
|
Crossfade { seconds: Option<i64> },
|
|
|
|
///Update still needs some work
|
2025-06-07 23:50:30 +02:00
|
|
|
Update {},
|
|
|
|
/// Return currently playing song
|
|
|
|
Current {},
|
|
|
|
/// Query database
|
|
|
|
Search {
|
2025-06-08 22:53:08 +02:00
|
|
|
///Search query
|
2025-06-07 23:50:30 +02:00
|
|
|
#[arg(trailing_var_arg = true)]
|
|
|
|
query: Vec<String>,
|
2025-06-08 22:53:08 +02:00
|
|
|
///Only return the first n results
|
|
|
|
#[arg(short, long)]
|
|
|
|
max: Option<u32>,
|
2025-06-13 13:09:18 +02:00
|
|
|
#[arg(short, long)]
|
|
|
|
append: bool,
|
|
|
|
#[arg(short, long)]
|
|
|
|
insert: Option<u32>,
|
|
|
|
},
|
|
|
|
/// Query database differently
|
|
|
|
Find {
|
|
|
|
///Search query
|
|
|
|
#[arg(trailing_var_arg = true)]
|
|
|
|
query: Vec<String>,
|
|
|
|
///Only return the first n results
|
|
|
|
#[arg(short, long)]
|
|
|
|
max: Option<u32>,
|
2025-06-07 23:50:30 +02:00
|
|
|
},
|
2025-06-13 13:09:18 +02:00
|
|
|
/// List items in the current queueueu
|
2025-06-07 23:50:30 +02:00
|
|
|
List {},
|
2025-06-13 13:09:18 +02:00
|
|
|
Add {
|
|
|
|
#[arg(short, long)]
|
|
|
|
position: Option<u32>,
|
|
|
|
},
|
2025-06-07 11:24:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
fn main() {
|
2025-06-07 11:43:42 +02:00
|
|
|
let cli = Cli::parse();
|
2025-06-07 23:50:30 +02:00
|
|
|
|
2025-06-08 22:53:08 +02:00
|
|
|
let host = match cli.host {
|
|
|
|
Some(host) => host,
|
|
|
|
None => String::from("localhost:6600"),
|
|
|
|
};
|
|
|
|
|
2025-06-13 13:09:18 +02:00
|
|
|
// let mut conn = Client::connect(host).unwrap();
|
|
|
|
|
|
|
|
let mut conn = Client::connect(host).expect("Things");
|
|
|
|
|
|
|
|
// let mut conn = match Client::connect(host) {
|
|
|
|
// Ok(conn) => conn,
|
|
|
|
// Err(error) => panic!("yo fucked up: {}", error.to_string()),
|
|
|
|
// };
|
2025-06-08 22:53:08 +02:00
|
|
|
|
2025-06-07 23:50:30 +02:00
|
|
|
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(),
|
|
|
|
|
2025-06-13 13:09:18 +02:00
|
|
|
Commands::Toggle {} => {
|
|
|
|
println!(
|
|
|
|
"{}",
|
|
|
|
match conn.status().unwrap().state {
|
|
|
|
mpd::State::Pause => "Resuming track...",
|
|
|
|
mpd::State::Play => "Pausing track...",
|
|
|
|
mpd::State::Stop => "Track stopped",
|
|
|
|
}
|
|
|
|
);
|
|
|
|
conn.toggle_pause().unwrap();
|
|
|
|
let status = get_status(conn, cli.verbose);
|
|
|
|
println!("{status}");
|
|
|
|
}
|
2025-06-07 23:50:30 +02:00
|
|
|
|
|
|
|
Commands::Next {} => conn.next().unwrap(),
|
|
|
|
|
|
|
|
Commands::Prev {} => conn.prev().unwrap(),
|
|
|
|
|
|
|
|
Commands::List {} => print_songs(conn.queue().unwrap()),
|
2025-06-13 13:09:18 +02:00
|
|
|
// Commands::List {} => {
|
|
|
|
// conn.insert(first_song, usize::try_from(1).unwrap());
|
|
|
|
// println!("{:?}", conn.queue().unwrap().first());
|
|
|
|
// }
|
2025-06-07 23:50:30 +02:00
|
|
|
Commands::Update {} => {
|
|
|
|
let thing = conn.update().unwrap();
|
2025-06-13 13:09:18 +02:00
|
|
|
println!("{thing}")
|
2025-06-07 23:50:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Commands::Current {} => {
|
2025-06-13 13:09:18 +02:00
|
|
|
let status = get_status(conn, cli.verbose);
|
|
|
|
println!("{status}");
|
2025-06-07 23:50:30 +02:00
|
|
|
}
|
|
|
|
|
2025-06-13 13:09:18 +02:00
|
|
|
Commands::Search {
|
|
|
|
query,
|
|
|
|
max,
|
|
|
|
append,
|
|
|
|
insert,
|
|
|
|
} => {
|
2025-06-07 23:50:30 +02:00
|
|
|
let mut result: Vec<String> = vec![];
|
2025-06-13 13:09:18 +02:00
|
|
|
let mut songs: Vec<Song> = vec![];
|
2025-06-07 23:50:30 +02:00
|
|
|
let query = query.join(" ");
|
2025-06-08 22:53:08 +02:00
|
|
|
// I want to return all results by default
|
|
|
|
let window = (
|
|
|
|
0,
|
|
|
|
if let Some(n) = max {
|
|
|
|
*n
|
|
|
|
} else {
|
|
|
|
conn.stats().unwrap().songs
|
|
|
|
},
|
|
|
|
);
|
2025-06-13 13:09:18 +02:00
|
|
|
if *append {
|
|
|
|
let queue_length = conn.status().unwrap().queue_len;
|
|
|
|
let _song = conn
|
|
|
|
.search(&Query::new().and(Term::Any, query), window)
|
|
|
|
.unwrap()
|
|
|
|
.iter()
|
|
|
|
.map(|x| songs.push(x.clone()));
|
|
|
|
conn.insert(
|
|
|
|
songs.first().unwrap(),
|
|
|
|
usize::try_from(queue_length).unwrap(),
|
|
|
|
)
|
|
|
|
.unwrap();
|
|
|
|
} else {
|
|
|
|
conn.search(&Query::new().and(Term::Any, query), window)
|
|
|
|
.unwrap()
|
|
|
|
.iter()
|
|
|
|
.for_each(|x| result.push(x.file.clone()));
|
|
|
|
result.iter().for_each(|x| println!("{x}"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Commands::Find { query, max } => {
|
|
|
|
let mut result: Vec<String> = vec![];
|
|
|
|
let query = query.join(" ");
|
|
|
|
let window = (
|
|
|
|
0,
|
|
|
|
if let Some(n) = max {
|
|
|
|
*n
|
|
|
|
} else {
|
|
|
|
conn.stats().unwrap().songs
|
|
|
|
},
|
|
|
|
);
|
|
|
|
conn.find(&Query::new().and(Term::Any, query), window)
|
2025-06-07 23:50:30 +02:00
|
|
|
.unwrap()
|
|
|
|
.iter()
|
|
|
|
.for_each(|x| result.push(x.file.clone()));
|
2025-06-13 13:09:18 +02:00
|
|
|
result.iter().for_each(|x| println!("{x}"))
|
2025-06-07 23:50:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Commands::Crossfade { seconds } => {
|
|
|
|
if let Some(crossfade) = seconds {
|
|
|
|
conn.crossfade(*crossfade).unwrap()
|
|
|
|
} else {
|
|
|
|
if let Some(crossfade) = conn.status().unwrap().crossfade {
|
|
|
|
println!("Crossfade: {}", crossfade.as_secs());
|
|
|
|
} else {
|
|
|
|
println!("Crossfade disabled")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2025-06-13 13:09:18 +02:00
|
|
|
|
|
|
|
Commands::Add { position } => {
|
|
|
|
// insert_or_append(conn, *position);
|
|
|
|
println!("{:?}", position);
|
|
|
|
}
|
2025-06-07 11:43:42 +02:00
|
|
|
}
|
2025-06-07 23:50:30 +02:00
|
|
|
} else {
|
2025-06-13 13:09:18 +02:00
|
|
|
let status = get_status(conn, cli.verbose);
|
|
|
|
println!("{}", status);
|
|
|
|
// println!("{:?}", conn.status().unwrap());
|
|
|
|
|
|
|
|
libnotify::init("noise").unwrap();
|
|
|
|
let n = libnotify::Notification::new("Noise", None, None);
|
|
|
|
n.show().unwrap();
|
|
|
|
|
|
|
|
libnotify::uninit();
|
2025-06-07 11:43:42 +02:00
|
|
|
}
|
2025-06-07 11:24:20 +02:00
|
|
|
}
|
2025-06-07 23:50:30 +02:00
|
|
|
|
2025-06-13 13:09:18 +02:00
|
|
|
fn stdin_reader() -> String {
|
|
|
|
"fiets".to_string()
|
|
|
|
}
|
|
|
|
|
|
|
|
// fn insert_or_append(mut client: Client, position: Option<u32>) {
|
|
|
|
// if let Some(pos) = position {
|
|
|
|
// client.insert(usize::try_from(1).unwrap(), usize::try_from(pos).unwrap());
|
|
|
|
// // let position = usize::try_from(position.unwrap()).unwrap();
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
2025-06-07 23:50:30 +02:00
|
|
|
fn print_songs(songs: Vec<Song>) -> () {
|
|
|
|
songs
|
|
|
|
.iter()
|
|
|
|
.for_each(|x| println!("{}", format_song(x.clone())));
|
|
|
|
}
|
|
|
|
|
|
|
|
fn format_song(song: Song) -> String {
|
|
|
|
format!("{} - {}", song.artist.unwrap(), song.title.unwrap())
|
|
|
|
}
|
2025-06-08 22:53:08 +02:00
|
|
|
|
|
|
|
///Bool to String
|
|
|
|
fn bts(b: bool) -> &'static str {
|
|
|
|
b.then(|| "on").unwrap_or("off")
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_status(mut client: Client, verbose: bool) -> String {
|
|
|
|
let status = client.status().unwrap();
|
|
|
|
|
|
|
|
let repeat = bts(status.repeat);
|
|
|
|
let single = bts(status.single);
|
|
|
|
let random = bts(status.random);
|
|
|
|
let consume = bts(status.consume);
|
|
|
|
let duration = match status.time {
|
|
|
|
Some((stamp, total)) => {
|
|
|
|
let stamp = to_mins_string(stamp);
|
|
|
|
let total = to_mins_string(total);
|
2025-06-13 13:09:18 +02:00
|
|
|
format!("{stamp}/{total}")
|
2025-06-08 22:53:08 +02:00
|
|
|
}
|
|
|
|
None => "".to_string(),
|
|
|
|
};
|
|
|
|
let mut output = vec![];
|
|
|
|
|
|
|
|
if let Some(song) = client.currentsong().unwrap() {
|
|
|
|
output.push(format_song(song))
|
|
|
|
} else {
|
|
|
|
output.push("No song playing".to_string())
|
|
|
|
}
|
|
|
|
|
|
|
|
if verbose {
|
|
|
|
output.push(format!(
|
|
|
|
"volume: {vol:<6} repeat: {repeat:<6} random: {rnd:<6} single: {single:<6} consume: {consume:<6}",
|
|
|
|
vol = {format!("{}%", status.volume)},
|
|
|
|
repeat = repeat,
|
|
|
|
rnd = random,
|
|
|
|
single = single,
|
|
|
|
consume = consume
|
|
|
|
));
|
|
|
|
|
|
|
|
output.push(format!(
|
2025-06-13 13:09:18 +02:00
|
|
|
"[{status}] {duration}",
|
|
|
|
status = {
|
|
|
|
match client.status().unwrap().state {
|
|
|
|
mpd::State::Stop => "stopped",
|
|
|
|
mpd::State::Pause => "paused",
|
|
|
|
mpd::State::Play => "playing",
|
|
|
|
}
|
|
|
|
}
|
2025-06-08 22:53:08 +02:00
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
output.join("\n")
|
|
|
|
}
|
|
|
|
|
|
|
|
fn to_mins_string(dur: Duration) -> String {
|
|
|
|
let seconds = dur.as_secs();
|
|
|
|
|
|
|
|
let minutes = seconds / 60;
|
|
|
|
let rem_seconds = seconds % 60;
|
|
|
|
|
2025-06-13 13:09:18 +02:00
|
|
|
format!("{minutes:02}:{rem_seconds:02}")
|
2025-06-08 22:53:08 +02:00
|
|
|
}
|