|
@@ -3,7 +3,185 @@ use mpd::search::Term;
|
|
|
use mpd::search::Window;
|
|
|
use mpd::Client;
|
|
|
use mpd::Query;
|
|
|
+use std::cmp::Ordering;
|
|
|
use std::collections::BTreeMap;
|
|
|
+use std::collections::BTreeSet;
|
|
|
+
|
|
|
+#[derive(Clone)]
|
|
|
+pub struct Command {
|
|
|
+ names: &'static [&'static str],
|
|
|
+ description: &'static str,
|
|
|
+ run: &'static dyn Fn(CommandContext) -> Vec<String>,
|
|
|
+}
|
|
|
+
|
|
|
+// Used for sorting the command list shown by the !help command
|
|
|
+impl Ord for Command {
|
|
|
+ fn cmp(&self, other: &Self) -> Ordering {
|
|
|
+ self.names.cmp(&other.names)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl PartialOrd for Command {
|
|
|
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
|
+ Some(self.cmp(other))
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl Eq for Command {}
|
|
|
+
|
|
|
+impl PartialEq for Command {
|
|
|
+ fn eq(&self, other: &Self) -> bool {
|
|
|
+ self.names == other.names && self.description == other.description
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+struct CommandContext<'a> {
|
|
|
+ mpd: &'a mut Client,
|
|
|
+ pages: &'a mut BTreeMap<u32, Vec<String>>,
|
|
|
+ actor: u32,
|
|
|
+ args: &'a mut dyn Iterator<Item = &'a str>,
|
|
|
+ commands: &'a BTreeMap<String, Command>,
|
|
|
+}
|
|
|
+
|
|
|
+pub fn create_commands() -> BTreeMap<String, Command> {
|
|
|
+ let commands = [
|
|
|
+ Command {
|
|
|
+ names: &["more", "m"],
|
|
|
+ description: "Displays next page of command output.",
|
|
|
+ run: &|ctx| {
|
|
|
+ if let Some(lines) = ctx.pages.get(&ctx.actor) {
|
|
|
+ lines.clone()
|
|
|
+ } else {
|
|
|
+ vec!["No more pages :(".to_string()]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ Command {
|
|
|
+ names: &["next", "skip", "s"],
|
|
|
+ description: "Skips the currently playing song.",
|
|
|
+ run: &|ctx| {
|
|
|
+ ctx.mpd.next().unwrap();
|
|
|
+ vec!["Skipping".to_string()]
|
|
|
+ },
|
|
|
+ },
|
|
|
+ Command {
|
|
|
+ names: &["pause"],
|
|
|
+ description: "Pauses playback.",
|
|
|
+ run: &|ctx| {
|
|
|
+ ctx.mpd.pause(true).unwrap();
|
|
|
+ vec!["Pausing".to_string()]
|
|
|
+ },
|
|
|
+ },
|
|
|
+ Command {
|
|
|
+ names: &["play"],
|
|
|
+ description: "Resumes playback.",
|
|
|
+ run: &|ctx| {
|
|
|
+ ctx.mpd.play().unwrap();
|
|
|
+ vec!["Playing".to_string()]
|
|
|
+ },
|
|
|
+ },
|
|
|
+ Command {
|
|
|
+ names: &["toggle", "t"],
|
|
|
+ description: "Toggles playback.",
|
|
|
+ run: &|ctx| {
|
|
|
+ ctx.mpd.toggle_pause().unwrap();
|
|
|
+ vec!["Toggling".to_string()]
|
|
|
+ },
|
|
|
+ },
|
|
|
+ Command {
|
|
|
+ names: &["search", "se"],
|
|
|
+ description: "Searches the database for tracks matching specified query strings.",
|
|
|
+ run: &|ctx| {
|
|
|
+ let mut query = Query::new();
|
|
|
+ let query = ctx.args.fold(&mut query, |a, b| a.and(Term::Any, b));
|
|
|
+
|
|
|
+ if let Ok(result) = ctx.mpd.search(&query, None) {
|
|
|
+ let display = result.iter().filter_map(display_song);
|
|
|
+ vec![format!("{} results:", result.len())]
|
|
|
+ .into_iter()
|
|
|
+ .chain(display)
|
|
|
+ .collect()
|
|
|
+ } else {
|
|
|
+ vec!["Search failed".to_string()]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ Command {
|
|
|
+ names: &["searchplay", "sp"],
|
|
|
+ description: "Searches the database for tracks matching specified query strings and plays the first track found.",
|
|
|
+ run: &|ctx| {
|
|
|
+ let mut query = Query::new();
|
|
|
+ let query = ctx.args.fold(&mut query, |a, b| a.and(Term::Any, b));
|
|
|
+
|
|
|
+ if let Ok(result) = ctx.mpd.search(&query, Window::from((0, 1))) {
|
|
|
+ let target_song = result.first().unwrap();
|
|
|
+ let position =
|
|
|
+ ctx.mpd.queue()
|
|
|
+ .unwrap()
|
|
|
+ .iter()
|
|
|
+ .enumerate()
|
|
|
+ .find_map(|(i, song)| {
|
|
|
+ if song.file == target_song.file {
|
|
|
+ Some(i)
|
|
|
+ } else {
|
|
|
+ None
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if let Some(pos) = position {
|
|
|
+ ctx.mpd.switch(pos as u32).unwrap();
|
|
|
+ vec![format!("Playing {}", display_song(&target_song).unwrap())]
|
|
|
+ } else {
|
|
|
+ vec![format!(
|
|
|
+ "Song not found in queue: {}",
|
|
|
+ display_song(&target_song).unwrap()
|
|
|
+ )]
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ vec!["Search failed".to_string()]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ Command {
|
|
|
+ names: &["queue", "q"],
|
|
|
+ description: "Shows the queue.",
|
|
|
+ run: &|ctx| {
|
|
|
+ ctx.mpd
|
|
|
+ .queue()
|
|
|
+ .unwrap()
|
|
|
+ .iter()
|
|
|
+ .filter_map(display_song)
|
|
|
+ .collect()
|
|
|
+ },
|
|
|
+ },
|
|
|
+ Command {
|
|
|
+ names: &["help", "h"],
|
|
|
+ description: "Display this command listing.",
|
|
|
+ run: &|c| {
|
|
|
+ let command_set: BTreeSet<Command> = c.commands
|
|
|
+ .values()
|
|
|
+ .cloned()
|
|
|
+ .into_iter()
|
|
|
+ .collect();
|
|
|
+ let text: Vec<String> =
|
|
|
+ command_set
|
|
|
+ .into_iter()
|
|
|
+ .map(|c| format!("<b>{}</b>: {}", c.names.join(", "), c.description))
|
|
|
+ .collect();
|
|
|
+ text
|
|
|
+ },
|
|
|
+ }
|
|
|
+ ];
|
|
|
+
|
|
|
+ let mut command_map = BTreeMap::new();
|
|
|
+
|
|
|
+ for command in &commands {
|
|
|
+ for name in command.names.iter() {
|
|
|
+ command_map.insert(name.to_string(), command.clone());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ command_map
|
|
|
+}
|
|
|
|
|
|
fn paginate<K: Ord, V: Clone>(
|
|
|
pages: &mut BTreeMap<K, Vec<V>>,
|
|
@@ -26,101 +204,35 @@ pub fn parse_command(
|
|
|
pages: &mut BTreeMap<u32, Vec<String>>,
|
|
|
actor: u32,
|
|
|
msg: &str,
|
|
|
+ commands: &BTreeMap<String, Command>,
|
|
|
) -> Option<String> {
|
|
|
- let reply = run_command(conn, pages, actor, msg);
|
|
|
- if reply.len() > 50 {
|
|
|
- Some(format!(
|
|
|
- "{} <br> Output has been paginated, use !more to view the rest.",
|
|
|
- paginate(pages, 50, actor, reply).join("<br>")
|
|
|
- ))
|
|
|
- } else {
|
|
|
- pages.remove(&actor);
|
|
|
- Some(reply.join("<br>"))
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-fn run_command(
|
|
|
- conn: &mut Client,
|
|
|
- pages: &mut BTreeMap<u32, Vec<String>>,
|
|
|
- actor: u32,
|
|
|
- msg: &str,
|
|
|
-) -> Vec<String> {
|
|
|
- let mut command = msg.split_whitespace();
|
|
|
- match command.nth(0).unwrap() {
|
|
|
- "!more" => {
|
|
|
- if let Some(lines) = pages.get(&actor) {
|
|
|
- lines.clone()
|
|
|
- } else {
|
|
|
- vec!["No more pages :(".to_string()]
|
|
|
- }
|
|
|
- }
|
|
|
- "!next" | "!skip" => {
|
|
|
- conn.next().unwrap();
|
|
|
- vec!["Skipping".to_string()]
|
|
|
- }
|
|
|
- "!pause" => {
|
|
|
- conn.pause(true).unwrap();
|
|
|
- vec!["Pausing".to_string()]
|
|
|
- }
|
|
|
- "!play" => {
|
|
|
- conn.play().unwrap();
|
|
|
- vec!["Playing".to_string()]
|
|
|
- }
|
|
|
- "!toggle" => {
|
|
|
- conn.toggle_pause().unwrap();
|
|
|
- vec!["Toggling".to_string()]
|
|
|
- }
|
|
|
- "!search" | "!se" => {
|
|
|
- let mut query = Query::new();
|
|
|
- let query = command.fold(&mut query, |a, b| a.and(Term::Any, b));
|
|
|
-
|
|
|
- if let Ok(result) = conn.search(&query, None) {
|
|
|
- let display = result.iter().filter_map(display_song);
|
|
|
- vec![format!("{} results:", result.len())]
|
|
|
- .into_iter()
|
|
|
- .chain(display)
|
|
|
- .collect()
|
|
|
- } else {
|
|
|
- vec!["Search failed".to_string()]
|
|
|
- }
|
|
|
- }
|
|
|
- "!searchplay" | "!sp" => {
|
|
|
- let mut query = Query::new();
|
|
|
- let query = command.fold(&mut query, |a, b| a.and(Term::Any, b));
|
|
|
-
|
|
|
- if let Ok(result) = conn.search(&query, Window::from((0, 1))) {
|
|
|
- let target_song = result.first().unwrap();
|
|
|
- let position = conn
|
|
|
- .queue()
|
|
|
- .unwrap()
|
|
|
- .iter()
|
|
|
- .enumerate()
|
|
|
- .find_map(|(i, song)| {
|
|
|
- if song.file == target_song.file {
|
|
|
- Some(i)
|
|
|
- } else {
|
|
|
- None
|
|
|
- }
|
|
|
- });
|
|
|
- if let Some(pos) = position {
|
|
|
- conn.switch(pos as u32).unwrap();
|
|
|
- vec![format!("Playing {}", display_song(&target_song).unwrap())]
|
|
|
+ let mut words = msg.split_whitespace();
|
|
|
+ if let Some(command_name) = words.nth(0) {
|
|
|
+ if command_name.chars().nth(0).unwrap() == '!' {
|
|
|
+ if let Some(command) = commands.get(&command_name.chars().skip(1).collect::<String>()) {
|
|
|
+ let reply = (*command.run)(CommandContext {
|
|
|
+ mpd: conn,
|
|
|
+ pages,
|
|
|
+ actor,
|
|
|
+ args: &mut words,
|
|
|
+ commands,
|
|
|
+ });
|
|
|
+ if reply.len() > 50 {
|
|
|
+ Some(format!(
|
|
|
+ "{} <br> Output has been paginated, use !more to view the rest.",
|
|
|
+ paginate(pages, 50, actor, reply).join("<br>")
|
|
|
+ ))
|
|
|
} else {
|
|
|
- vec![format!(
|
|
|
- "Song not found in queue: {}",
|
|
|
- display_song(&target_song).unwrap()
|
|
|
- )]
|
|
|
+ pages.remove(&actor);
|
|
|
+ Some(reply.join("<br>"))
|
|
|
}
|
|
|
} else {
|
|
|
- vec!["Search failed".to_string()]
|
|
|
+ Some("Command not found, use !help to list commands.".to_string())
|
|
|
}
|
|
|
+ } else {
|
|
|
+ None
|
|
|
}
|
|
|
- "!queue" => conn
|
|
|
- .queue()
|
|
|
- .unwrap()
|
|
|
- .iter()
|
|
|
- .filter_map(display_song)
|
|
|
- .collect(),
|
|
|
- _ => vec![],
|
|
|
+ } else {
|
|
|
+ None
|
|
|
}
|
|
|
}
|