use crate::clients::PlayerStatus;
use futures::future::Future;
use log::{debug, info};
use serde::{Deserialize, Serialize};
use snafu::{OptionExt, Snafu};
use tokio::process::Command;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::path::PathBuf;
#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("The path `{}' cannot be converted to a UTF-8 string", pth.display()))]
BadPath { pth: PathBuf },
#[snafu(display("Parameter {} is a duplciate track argument", index))]
DuplicateTrackArgument { index: usize },
#[snafu(display("Missing parameter {} & it it is not marked as default", index))]
MissingParameter { index: usize },
#[snafu(display("The current track was requested, but there is not current track"))]
NoCurrentTrack,
#[snafu(display(
"This command is marked as needing to update the affected track only, but \"
there is no track given"
))]
NoTrackToUpdate,
#[snafu(display(
"The template string `{}' has a trailing '%' character, which is illegal.",
template
))]
TrailingPercent { template: String },
#[snafu(display("Unknown replacement parameter `{}'", param))]
UnknownParameter { param: String },
}
type Result<T> = std::result::Result<T, Error>;
pub fn process_replacements(templ: &str, params: &HashMap<String, String>) -> Result<String> {
let mut out = String::new();
let mut c = templ.chars().peekable();
loop {
let a = match c.next() {
Some(x) => x,
None => {
break;
}
};
if a != '%' {
out.push(a);
} else {
let b = c.peek().context(TrailingPercent {
template: String::from(templ),
})?;
if *b == '%' {
c.next();
out.push('%');
} else {
let mut terminal = None;
let t: String = c
.by_ref()
.take_while(|x| {
if x.is_alphanumeric() || x == &'-' || x == &'_' {
true
} else {
terminal = Some(x.clone());
false
}
})
.collect();
out.push_str(params.get(&t).context(UnknownParameter {
param: String::from(t),
})?);
match terminal {
Some(x) => out.push(x),
None => {
break;
}
}
}
}
}
Ok(out)
}
#[cfg(test)]
mod test_replacement_strings {
#[test]
fn test_process_replacements() {
use super::process_replacements;
use std::collections::HashMap;
let mut p: HashMap<String, String> = HashMap::new();
p.insert(String::from("rating"), String::from("255"));
assert_eq!(
"rating is 255",
process_replacements("rating is %rating", &p).unwrap()
);
p.insert(String::from("full-path"), String::from("has spaces"));
assert_eq!(
"\"has spaces\" has rating 255",
process_replacements("\"%full-path\" has rating %rating", &p).unwrap()
);
}
}
pub type PinnedCmdFut =
std::pin::Pin<Box<dyn Future<Output = tokio::io::Result<std::process::Output>>>>;
pub fn spawn<S, I>(cmd: S, args: I, params: &HashMap<String, String>) -> Result<PinnedCmdFut>
where
I: Iterator<Item = String>,
S: AsRef<OsStr> + std::fmt::Debug,
{
let args: std::result::Result<Vec<_>, _> =
args.map(|x| process_replacements(&x, ¶ms)).collect();
match args {
Ok(a) => {
info!("Running command `{:#?}' with args {:#?}", &cmd, &a);
Ok(Box::pin(Command::new(&cmd).args(a).output()))
}
Err(err) => Err(Error::from(err)),
}
}
use pin_project::pin_project;
use std::pin::Pin;
#[pin_project]
pub struct TaggedCommandFuture {
#[pin]
fut: PinnedCmdFut,
upd: Option<String>,
}
impl TaggedCommandFuture {
pub fn new(fut: PinnedCmdFut, upd: Option<String>) -> TaggedCommandFuture {
TaggedCommandFuture { fut: fut, upd: upd }
}
pub fn pin(fut: PinnedCmdFut, upd: Option<String>) -> std::pin::Pin<Box<TaggedCommandFuture>> {
Box::pin(TaggedCommandFuture::new(fut, upd))
}
}
pub struct TaggedCommandOutput {
pub out: tokio::io::Result<std::process::Output>,
pub upd: Option<String>,
}
impl std::future::Future for TaggedCommandFuture {
type Output = TaggedCommandOutput;
fn poll(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context,
) -> std::task::Poll<Self::Output> {
let this = self.project();
let fut: Pin<&mut PinnedCmdFut> = this.fut;
let upd: &mut Option<String> = this.upd;
match fut.poll(cx) {
std::task::Poll::Pending => std::task::Poll::Pending,
std::task::Poll::Ready(out) => std::task::Poll::Ready(TaggedCommandOutput {
out: out,
upd: upd.clone(),
}),
}
}
}
pub type PinnedTaggedCmdFuture = std::pin::Pin<std::boxed::Box<TaggedCommandFuture>>;
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum FormalParameter {
Literal,
Track,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum Update {
NoUpdate,
TrackOnly,
FullDatabase,
}
pub struct GeneralizedCommand {
formal_parameters: Vec<FormalParameter>,
default_after: usize,
music_dir: PathBuf,
cmd: PathBuf,
args: Vec<String>,
update: Update,
}
impl GeneralizedCommand {
pub fn new<I1, I2>(
formal_params: I1,
default_after: usize,
music_dir: &str,
cmd: &PathBuf,
args: I2,
update: Update,
) -> GeneralizedCommand
where
I1: Iterator<Item = FormalParameter>,
I2: Iterator<Item = String>,
{
GeneralizedCommand {
formal_parameters: formal_params.collect(),
default_after: default_after,
music_dir: PathBuf::from(music_dir),
cmd: cmd.clone(),
args: args.collect(),
update: update,
}
}
pub fn execute<'a, I>(&self, tokens: I, state: &PlayerStatus) -> Result<PinnedTaggedCmdFuture>
where
I: Iterator<Item = &'a str>,
{
let mut params = HashMap::<String, String>::new();
let current_file = match state {
PlayerStatus::Stopped => None,
PlayerStatus::Play(curr) | PlayerStatus::Pause(curr) => {
let mut cfp = self.music_dir.clone();
cfp.push(&curr.file);
let cfs = cfp
.to_str()
.context(BadPath { pth: cfp.clone() })?
.to_string();
params.insert("current-file".to_string(), cfs.clone());
debug!("current-file is: {}", cfs);
Some(cfs)
}
};
let mut i: usize = 1;
let mut saw_track = false;
let mut full_file: Option<String> = None;
let mut act_params = tokens.into_iter();
for form_param in &self.formal_parameters {
let act_param = act_params.next();
match (form_param, act_param) {
(FormalParameter::Literal, Some(token)) => {
params.insert(format!("{}", i), token.into());
}
(FormalParameter::Literal, None) => {
if i < self.default_after {
return Err(Error::MissingParameter { index: i });
}
debug!("%{} is: nil", i);
params.insert(format!("{}", i), String::from(""));
}
(FormalParameter::Track, Some(token)) => {
if saw_track {
return Err(Error::DuplicateTrackArgument { index: i });
}
let mut ffp = self.music_dir.clone();
ffp.push(PathBuf::from(token));
let ffs = ffp.to_str().context(BadPath { pth: ffp.clone() })?;
params.insert(format!("{}", i), ffs.to_string());
params.insert("full-file".to_string(), ffs.to_string());
full_file = Some(ffs.to_string());
saw_track = true;
}
(FormalParameter::Track, None) => {
if i < self.default_after {
return Err(Error::MissingParameter { index: i });
}
if saw_track {
return Err(Error::DuplicateTrackArgument { index: i });
}
match ¤t_file {
Some(cf) => {
full_file = Some(cf.clone());
params.insert(format!("{}", i), cf.clone());
params.insert("full-file".to_string(), cf.to_string());
}
None => {
return Err(Error::NoCurrentTrack);
}
}
saw_track = true;
}
}
i += 1;
}
Ok(TaggedCommandFuture::pin(
spawn(&self.cmd, self.args.iter().cloned(), ¶ms)?,
match self.update {
Update::NoUpdate => None,
Update::TrackOnly => {
match full_file {
Some(x) => Some(format!("song {}", x)),
None => match current_file {
Some(x) => Some(format!("song {}", x)),
None => return Err(Error::NoTrackToUpdate),
},
}
}
Update::FullDatabase => Some(String::from("")),
},
))
}
}