use crate::{
clients::{Client, IdleClient, PlayerStatus},
commands::{GeneralizedCommand, PinnedTaggedCmdFuture},
filters::ExpressionParser,
filters_ast::{FilterStickerNames, evaluate},
playcounts::{set_last_played, set_play_count},
ratings::{RatedTrack, RatingRequest, set_rating},
};
use backtrace::Backtrace;
use boolinator::Boolinator;
use tracing::debug;
use std::collections::{HashMap, VecDeque};
use std::convert::TryFrom;
use std::iter::FromIterator;
use std::path::PathBuf;
#[derive(Debug)]
pub enum Error {
BadPath {
pth: PathBuf,
},
FilterParseError {
msg: String,
},
InvalidChar {
c: u8,
},
NoClosingQuotes,
NoCommand,
NotImplemented {
feature: String,
},
PlayerStopped,
TrailingBackslash,
UnknownChannel {
chan: String,
back: Backtrace,
},
UnknownCommand {
name: String,
back: Backtrace,
},
Client {
source: crate::clients::Error,
back: Backtrace,
},
Ratings {
source: crate::ratings::Error,
back: Backtrace,
},
Playcount {
source: crate::playcounts::Error,
back: Backtrace,
},
Filter {
source: crate::filters_ast::Error,
back: Backtrace,
},
Command {
source: crate::commands::Error,
back: Backtrace,
},
Utf8 {
source: std::str::Utf8Error,
buf: Vec<u8>,
back: Backtrace,
},
ExpectedInt {
source: std::num::ParseIntError,
text: String,
back: Backtrace,
},
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Error::BadPath { pth } => write!(f, "Bad path: {:?}", pth),
Error::FilterParseError { msg } => write!(f, "Parse error: ``{}''", msg),
Error::InvalidChar { c } => write!(f, "Invalid unquoted character {}", c),
Error::NoClosingQuotes => write!(f, "Missing closing quotes"),
Error::NoCommand => write!(f, "No command specified"),
Error::NotImplemented { feature } => write!(f, "`{}' not implemented, yet", feature),
Error::PlayerStopped => write!(
f,
"Can't operate on the current track when the player is stopped"
),
Error::TrailingBackslash => write!(f, "Trailing backslash"),
Error::UnknownChannel { chan, back: _ } => write!(
f,
"We received messages for an unknown channel `{}'; this is likely a bug; please consider filing a report to sp1ff@pobox.com",
chan
),
Error::UnknownCommand { name, back: _ } => {
write!(f, "We received an unknown message ``{}''", name)
}
Error::Client { source, back: _ } => write!(f, "Client error: {}", source),
Error::Ratings { source, back: _ } => write!(f, "Ratings eror: {}", source),
Error::Playcount { source, back: _ } => write!(f, "Playcount error: {}", source),
Error::Filter { source, back: _ } => write!(f, "Filter error: {}", source),
Error::Command { source, back: _ } => write!(f, "Command error: {}", source),
Error::Utf8 {
source,
buf,
back: _,
} => write!(f, "UTF8 error {} ({:#?})", source, buf),
Error::ExpectedInt {
source,
text,
back: _,
} => write!(f, "``{}''L {}", source, text),
}
}
}
pub type Result<T> = std::result::Result<T, Error>;
pub fn tokenize<'a>(buf: &'a mut [u8]) -> impl Iterator<Item = Result<&'a [u8]>> {
TokenIterator::new(buf)
}
struct TokenIterator<'a> {
slice: &'a mut [u8],
input: usize,
}
impl<'a> TokenIterator<'a> {
pub fn new(slice: &'a mut [u8]) -> TokenIterator<'a> {
let input = match slice.iter().position(|&x| x > 0x20) {
Some(n) => n,
None => slice.len(),
};
TokenIterator {
slice: slice,
input: input,
}
}
}
impl<'a> Iterator for TokenIterator<'a> {
type Item = Result<&'a [u8]>;
fn next<'s>(&'s mut self) -> Option<Self::Item> {
let nslice = self.slice.len();
if self.slice.is_empty() || self.input == nslice {
None
} else {
if '"' == self.slice[self.input] as char {
let mut inp = self.input + 1;
let mut out = self.input;
while self.slice[inp] as char != '"' {
if '\\' == self.slice[inp] as char {
inp += 1;
if inp == nslice {
return Some(Err(Error::TrailingBackslash));
}
}
self.slice[out] = self.slice[inp];
out += 1;
inp += 1;
if inp == nslice {
return Some(Err(Error::NoClosingQuotes));
}
}
let tmp = std::mem::replace(&mut self.slice, &mut []);
let (_, tmp) = tmp.split_at_mut(self.input);
let (result, new_slice) = tmp.split_at_mut(out - self.input);
self.slice = new_slice;
self.input = inp - out + 1; while self.input < self.slice.len() && self.slice[self.input] as char == ' ' {
self.input += 1;
}
Some(Ok(result))
} else {
let mut i = self.input;
while i < nslice {
if 0x20 >= self.slice[i] {
break;
}
if self.slice[i] as char == '"' || self.slice[i] as char == '\'' {
return Some(Err(Error::InvalidChar { c: self.slice[i] }));
}
i += 1;
}
let tmp = std::mem::replace(&mut self.slice, &mut []);
let (_, tmp) = tmp.split_at_mut(self.input);
let (result, new_slice) = tmp.split_at_mut(i - self.input);
self.slice = new_slice;
self.input = match self.slice.iter().position(|&x| x > 0x20) {
Some(n) => n,
None => self.slice.len(),
};
Some(Ok(result))
}
}
}
}
#[cfg(test)]
mod tokenize_tests {
use super::Result;
use super::tokenize;
#[test]
fn tokenize_smoke() {
let mut buf1 = String::from("some-command").into_bytes();
let x1: Vec<&[u8]> = tokenize(&mut buf1).collect::<Result<Vec<&[u8]>>>().unwrap();
assert_eq!(x1[0], b"some-command");
let mut buf2 = String::from("a b").into_bytes();
let x2: Vec<&[u8]> = tokenize(&mut buf2).collect::<Result<Vec<&[u8]>>>().unwrap();
assert_eq!(x2[0], b"a");
assert_eq!(x2[1], b"b");
let mut buf3 = String::from("a \"b c\"").into_bytes();
let x3: Vec<&[u8]> = tokenize(&mut buf3).collect::<Result<Vec<&[u8]>>>().unwrap();
assert_eq!(x3[0], b"a");
assert_eq!(x3[1], b"b c");
let mut buf4 = String::from("a \"b c\" d").into_bytes();
let x4: Vec<&[u8]> = tokenize(&mut buf4).collect::<Result<Vec<&[u8]>>>().unwrap();
assert_eq!(x4[0], b"a");
assert_eq!(x4[1], b"b c");
assert_eq!(x4[2], b"d");
let mut buf5 = String::from("simple-command \"with space\" \"with '\"").into_bytes();
let x5: Vec<&[u8]> = tokenize(&mut buf5).collect::<Result<Vec<&[u8]>>>().unwrap();
assert_eq!(x5[0], b"simple-command");
assert_eq!(x5[1], b"with space");
assert_eq!(x5[2], b"with '");
let mut buf6 = String::from("cmd \"with\\\\slash and space\"").into_bytes();
let x6: Vec<&[u8]> = tokenize(&mut buf6).collect::<Result<Vec<&[u8]>>>().unwrap();
assert_eq!(x6[0], b"cmd");
assert_eq!(x6[1], b"with\\slash and space");
let mut buf7 = String::from(" cmd \"with\\\\slash and space\" ").into_bytes();
let x7: Vec<&[u8]> = tokenize(&mut buf7).collect::<Result<Vec<&[u8]>>>().unwrap();
assert_eq!(x7[0], b"cmd");
assert_eq!(x7[1], b"with\\slash and space");
}
#[test]
fn tokenize_filter() {
let mut buf1 = String::from(r#""(artist =~ \"foo\\\\bar\\\"\")""#).into_bytes();
let x1: Vec<&[u8]> = tokenize(&mut buf1).collect::<Result<Vec<&[u8]>>>().unwrap();
assert_eq!(1, x1.len());
eprintln!("x1[0] is ``{}''", std::str::from_utf8(x1[0]).unwrap());
assert_eq!(
std::str::from_utf8(x1[0]).unwrap(),
r#"(artist =~ "foo\\bar\"")"#
);
}
}
pub struct MessageProcessor<'a, I1, I2>
where
I1: Iterator<Item = String> + Clone,
I2: Iterator<Item = String> + Clone,
{
music_dir: &'a str,
rating_sticker: &'a str,
ratings_cmd: &'a str,
ratings_cmd_args: I1,
playcount_sticker: &'a str,
playcount_cmd: &'a str,
playcount_cmd_args: I2,
lastplayed_sticker: &'a str,
gen_cmds: HashMap<String, GeneralizedCommand>,
}
impl<I1, I2> MessageProcessor<'_, I1, I2>
where
I1: Iterator<Item = String> + Clone,
I2: Iterator<Item = String> + Clone,
{
pub fn new<'a, IGC>(
music_dir: &'a str,
rating_sticker: &'a str,
ratings_cmd: &'a str,
ratings_cmd_args: I1,
playcount_sticker: &'a str,
playcount_cmd: &'a str,
playcount_cmd_args: I2,
lastplayed_sticker: &'a str,
gen_cmds: IGC,
) -> MessageProcessor<'a, I1, I2>
where
IGC: Iterator<Item = (String, GeneralizedCommand)>,
{
MessageProcessor {
music_dir: music_dir,
rating_sticker: rating_sticker,
ratings_cmd: ratings_cmd,
ratings_cmd_args: ratings_cmd_args.clone(),
playcount_sticker: playcount_sticker,
playcount_cmd: playcount_cmd,
playcount_cmd_args: playcount_cmd_args.clone(),
lastplayed_sticker: lastplayed_sticker,
gen_cmds: HashMap::from_iter(gen_cmds),
}
}
pub async fn check_messages<'a, E>(
&self,
client: &mut Client,
idle_client: &mut IdleClient,
state: PlayerStatus,
command_chan: &str,
cmds: &mut E,
stickers: &FilterStickerNames<'a>,
) -> Result<()>
where
E: Extend<PinnedTaggedCmdFuture>,
{
let m = idle_client
.get_messages()
.await
.map_err(|err| Error::Client {
source: err,
back: Backtrace::new(),
})?;
for (chan, msgs) in m {
(chan == command_chan).ok_or_else(|| Error::UnknownChannel {
chan: String::from(chan),
back: Backtrace::new(),
})?;
for msg in msgs {
cmds.extend(self.process(msg, client, &state, stickers).await?);
}
}
Ok(())
}
pub async fn process<'a>(
&self,
msg: String,
client: &mut Client,
state: &PlayerStatus,
stickers: &FilterStickerNames<'a>,
) -> Result<Option<PinnedTaggedCmdFuture>> {
if msg.starts_with("rate ") {
self.rate(&msg[5..], client, state).await
} else if msg.starts_with("setpc ") {
self.setpc(&msg[6..], client, state).await
} else if msg.starts_with("setlp ") {
self.setlp(&msg[6..], client, state).await
} else if msg.starts_with("findadd ") {
self.findadd(msg[8..].to_string(), client, stickers, state)
.await
} else if msg.starts_with("searchadd ") {
self.searchadd(msg[10..].to_string(), client, stickers, state)
.await
} else {
self.maybe_handle_generalized_command(msg, state).await
}
}
async fn rate(
&self,
msg: &str,
client: &mut Client,
state: &PlayerStatus,
) -> Result<Option<PinnedTaggedCmdFuture>> {
let req = RatingRequest::try_from(msg).map_err(|err| Error::Ratings {
source: err,
back: Backtrace::new(),
})?;
let pathb = match req.track {
RatedTrack::Current => match state {
PlayerStatus::Stopped => {
return Err(Error::PlayerStopped {});
}
PlayerStatus::Play(curr) | PlayerStatus::Pause(curr) => curr.file.clone(),
},
RatedTrack::File(p) => p,
RatedTrack::Relative(_i) => {
return Err(Error::NotImplemented {
feature: String::from("Relative track position"),
});
}
};
let path: &str = pathb
.to_str()
.ok_or_else(|| Error::BadPath { pth: pathb.clone() })?;
debug!("Setting a rating of {} for `{}'.", req.rating, path);
Ok(set_rating(
client,
self.rating_sticker,
path,
req.rating,
self.ratings_cmd,
self.ratings_cmd_args.clone(),
self.music_dir,
)
.await
.map_err(|err| Error::Ratings {
source: err,
back: Backtrace::new(),
})?)
}
async fn setpc(
&self,
msg: &str,
client: &mut Client,
state: &PlayerStatus,
) -> Result<Option<PinnedTaggedCmdFuture>> {
let text = msg.trim();
let (pc, track) = match text.find(char::is_whitespace) {
Some(idx) => (
text[..idx]
.parse::<usize>()
.map_err(|err| Error::ExpectedInt {
source: err,
text: String::from(text),
back: Backtrace::new(),
})?,
&text[idx + 1..],
),
None => (
text.parse::<usize>().map_err(|err| Error::ExpectedInt {
source: err,
text: String::from(text),
back: Backtrace::new(),
})?,
"",
),
};
let file = if track.is_empty() {
match state {
PlayerStatus::Stopped => {
return Err(Error::PlayerStopped {});
}
PlayerStatus::Play(curr) | PlayerStatus::Pause(curr) => curr
.file
.to_str()
.ok_or_else(|| Error::BadPath {
pth: curr.file.clone(),
})?
.to_string(),
}
} else {
track.to_string()
};
if self.playcount_cmd.is_empty() {
return Ok(None);
}
Ok(set_play_count(
client,
self.playcount_sticker,
&file,
pc,
self.playcount_cmd,
&mut self.playcount_cmd_args.clone(),
self.music_dir,
)
.await
.map_err(|err| Error::Playcount {
source: err,
back: Backtrace::new(),
})?)
}
async fn setlp(
&self,
msg: &str,
client: &mut Client,
state: &PlayerStatus,
) -> Result<Option<PinnedTaggedCmdFuture>> {
let text = msg.trim();
let (lp, track) = match text.find(char::is_whitespace) {
Some(idx) => (
text[..idx]
.parse::<u64>()
.map_err(|err| Error::ExpectedInt {
source: err,
text: String::from(text),
back: Backtrace::new(),
})?,
&text[idx + 1..],
),
None => (
text.parse::<u64>().map_err(|err| Error::ExpectedInt {
source: err,
text: String::from(text),
back: Backtrace::new(),
})?,
"",
),
};
let file = if track.is_empty() {
match state {
PlayerStatus::Stopped => {
return Err(Error::PlayerStopped {});
}
PlayerStatus::Play(curr) | PlayerStatus::Pause(curr) => curr
.file
.to_str()
.ok_or_else(|| Error::BadPath {
pth: curr.file.clone(),
})?
.to_string(),
}
} else {
track.to_string()
};
set_last_played(client, self.lastplayed_sticker, &file, lp)
.await
.map_err(|err| Error::Playcount {
source: err,
back: Backtrace::new(),
})?;
Ok(None)
}
async fn findadd<'a>(
&self,
msg: String,
client: &mut Client,
stickers: &FilterStickerNames<'a>,
_state: &PlayerStatus,
) -> Result<Option<PinnedTaggedCmdFuture>> {
let mut buf = msg.into_bytes();
let args: VecDeque<&str> = tokenize(&mut buf)
.map(|r| match r {
Ok(buf) => Ok(std::str::from_utf8(buf).map_err(|err| Error::Utf8 {
source: err,
buf: buf.to_vec(),
back: Backtrace::new(),
})?),
Err(err) => Err(err),
})
.collect::<Result<VecDeque<&str>>>()?;
debug!("findadd arguments: {:#?}", args);
let ast = match ExpressionParser::new().parse(args[0]) {
Ok(ast) => ast,
Err(err) => {
return Err(Error::FilterParseError {
msg: format!("{}", err),
});
}
};
debug!("ast: {:#?}", ast);
let mut results = Vec::new();
for song in evaluate(&ast, true, client, stickers)
.await
.map_err(|err| Error::Filter {
source: err,
back: Backtrace::new(),
})?
{
results.push(client.add(&song).await);
}
match results
.into_iter()
.collect::<std::result::Result<Vec<()>, crate::clients::Error>>()
{
Ok(_) => Ok(None),
Err(err) => Err(Error::Client {
source: err,
back: Backtrace::new(),
}),
}
}
async fn searchadd<'a>(
&self,
msg: String,
client: &mut Client,
stickers: &FilterStickerNames<'a>,
_state: &PlayerStatus,
) -> Result<Option<PinnedTaggedCmdFuture>> {
let mut buf = msg.into_bytes();
let args: VecDeque<&str> = tokenize(&mut buf)
.map(|r| match r {
Ok(buf) => Ok(std::str::from_utf8(buf).map_err(|err| Error::Utf8 {
source: err,
buf: buf.to_vec(),
back: Backtrace::new(),
})?),
Err(err) => Err(err),
})
.collect::<Result<VecDeque<&str>>>()?;
debug!("searchadd arguments: {:#?}", args);
let ast = match ExpressionParser::new().parse(args[0]) {
Ok(ast) => ast,
Err(err) => {
return Err(Error::FilterParseError {
msg: format!("{}", err),
});
}
};
debug!("ast: {:#?}", ast);
let mut results = Vec::new();
for song in evaluate(&ast, false, client, stickers)
.await
.map_err(|err| Error::Filter {
source: err,
back: Backtrace::new(),
})?
{
results.push(client.add(&song).await);
}
match results
.into_iter()
.collect::<std::result::Result<Vec<()>, crate::clients::Error>>()
{
Ok(_) => Ok(None),
Err(err) => Err(Error::Client {
source: err,
back: Backtrace::new(),
}),
}
}
async fn maybe_handle_generalized_command(
&self,
msg: String,
state: &PlayerStatus,
) -> Result<Option<PinnedTaggedCmdFuture>> {
let mut buf = msg.into_bytes();
let mut args: VecDeque<&str> = tokenize(&mut buf)
.map(|r| match r {
Ok(buf) => Ok(std::str::from_utf8(buf).map_err(|err| Error::Utf8 {
source: err,
buf: buf.to_vec(),
back: Backtrace::new(),
})?),
Err(err) => Err(err),
})
.collect::<Result<VecDeque<&str>>>()?;
let cmd = match args.pop_front() {
Some(x) => x,
None => {
return Err(Error::NoCommand);
}
};
let gen_cmd = self
.gen_cmds
.get(cmd)
.ok_or_else(|| Error::UnknownCommand {
name: String::from(cmd),
back: Backtrace::new(),
})?;
Ok(Some(
gen_cmd
.execute(args.iter().cloned(), &state)
.map_err(|err| Error::Command {
source: err,
back: Backtrace::new(),
})?,
))
}
}