diff --git a/Cargo.lock b/Cargo.lock index 0d07812..b372674 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,6 +212,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bufstream" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" + [[package]] name = "bumpalo" version = "3.9.1" @@ -1182,6 +1188,17 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mpd" +version = "0.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a20784da57fa01bf7910a5da686d9f39ff37feaa774856b71f050e4331bf82" +dependencies = [ + "bufstream", + "rustc-serialize", + "time", +] + [[package]] name = "mpris-player" version = "0.6.1" @@ -1206,6 +1223,7 @@ dependencies = [ "bliss-audio-symphonia", "criterion", "dirs", + "mpd", "rand", "regex 1.5.5", "rusqlite", @@ -1906,6 +1924,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-serialize" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" + [[package]] name = "rustc_version" version = "0.4.0" diff --git a/mps-interpreter/Cargo.toml b/mps-interpreter/Cargo.toml index 5ba83c2..044518c 100644 --- a/mps-interpreter/Cargo.toml +++ b/mps-interpreter/Cargo.toml @@ -16,6 +16,7 @@ regex = { version = "1" } rand = { version = "0.8" } shellexpand = { version = "2.1", optional = true } bliss-audio-symphonia = { version = "0.4", optional = true, path = "../bliss-rs" } +mpd = { version = "0.0.12", optional = true } [dev-dependencies] criterion = "0.3" @@ -26,6 +27,6 @@ harness = false [features] default = [ "music_library", "ergonomics", "advanced" ] -music_library = [ "symphonia" ] # song metadata parsing and database auto-population +music_library = [ "symphonia", "mpd" ] # song metadata parsing and database auto-population ergonomics = ["shellexpand"] # niceties like ~ in pathes advanced = ["bliss-audio-symphonia"] # advanced language features like bliss playlist generation diff --git a/mps-interpreter/README.md b/mps-interpreter/README.md index c2ce80e..293f90d 100644 --- a/mps-interpreter/README.md +++ b/mps-interpreter/README.md @@ -91,6 +91,10 @@ Replace items matching the filter with operation1 and replace items not matching Keep only items which are do not duplicate another item, or keep only items whoes specified field does not duplicate another item's same field. The first non-duplicated instance of an item is always the one that is kept. +#### ?? -- e.g. `iterable.(??);` + +Keep only the items that contain at least one field (not including the filename field). + ### Functions Similar to most other languages: `function_name(param1, param2, etc.);`. These always return an iterable which can be manipulated with other syntax (filters, sorters, etc.). @@ -131,6 +135,10 @@ Repeat the iterable count times, or infinite times if count is omitted. Retrieve all files from a folder, matching a regex pattern. +#### mpd(address, term = value, term2 = value2, ...); + +Retrieve songs from a music player daemon at `address`. If compiled without the `music_library` feature, this is equivalent to `empty()`. + #### reset(iterable); Explicitly reset an iterable. This useful for reusing an iterable variable. diff --git a/mps-interpreter/src/context.rs b/mps-interpreter/src/context.rs index 6ace5c8..8d5fc03 100644 --- a/mps-interpreter/src/context.rs +++ b/mps-interpreter/src/context.rs @@ -1,6 +1,8 @@ #[cfg(feature = "advanced")] use super::processing::advanced::{MpsDefaultAnalyzer, MpsMusicAnalyzer}; use super::processing::database::{MpsDatabaseQuerier, MpsSQLiteExecutor}; +#[cfg(feature = "mpd")] +use super::processing::database::{MpsMpdQuerier, MpsMpdExecutor}; use super::processing::general::{ MpsFilesystemExecutor, MpsFilesystemQuerier, MpsOpStorage, MpsVariableStorer, }; @@ -13,6 +15,8 @@ pub struct MpsContext { pub filesystem: Box, #[cfg(feature = "advanced")] pub analysis: Box, + #[cfg(feature = "mpd")] + pub mpd_database: Box, } impl Default for MpsContext { @@ -23,6 +27,8 @@ impl Default for MpsContext { filesystem: Box::new(MpsFilesystemExecutor::default()), #[cfg(feature = "advanced")] analysis: Box::new(MpsDefaultAnalyzer::default()), + #[cfg(feature = "mpd")] + mpd_database: Box::new(MpsMpdExecutor::default()), } } } diff --git a/mps-interpreter/src/interpretor.rs b/mps-interpreter/src/interpretor.rs index f3dace5..67a566a 100644 --- a/mps-interpreter/src/interpretor.rs +++ b/mps-interpreter/src/interpretor.rs @@ -48,6 +48,7 @@ pub(crate) fn standard_vocab(vocabulary: &mut MpsLanguageDictionary) { // functions don't enforce bracket coherence // -- function().() is valid despite the ).( in between brackets .add(crate::lang::vocabulary::sql_function_factory()) + .add(crate::lang::vocabulary::mpd_query_function_factory()) .add(crate::lang::vocabulary::simple_sql_function_factory()) .add(crate::lang::vocabulary::repeat_function_factory()) .add(crate::lang::vocabulary::AssignStatementFactory) diff --git a/mps-interpreter/src/item.rs b/mps-interpreter/src/item.rs index 71b2e50..76521c1 100644 --- a/mps-interpreter/src/item.rs +++ b/mps-interpreter/src/item.rs @@ -29,6 +29,11 @@ impl MpsItem { self } + pub fn set_field_chain2(mut self, name: &str, value: MpsTypePrimitive) -> Self { + self.set_field(name, value); + self + } + pub fn remove_field(&mut self, name: &str) -> Option { self.fields.remove(name) } diff --git a/mps-interpreter/src/lang/vocabulary/mod.rs b/mps-interpreter/src/lang/vocabulary/mod.rs index a1c716c..b89b224 100644 --- a/mps-interpreter/src/lang/vocabulary/mod.rs +++ b/mps-interpreter/src/lang/vocabulary/mod.rs @@ -2,6 +2,7 @@ mod empties; pub(crate) mod empty; mod files; mod intersection; +mod mpd_query; mod repeat; mod reset; mod sql_init; @@ -14,6 +15,7 @@ pub use empties::{empties_function_factory, EmptiesStatementFactory}; pub use empty::{empty_function_factory, EmptyStatementFactory}; pub use files::{files_function_factory, FilesStatementFactory}; pub use intersection::{intersection_function_factory, IntersectionStatementFactory}; +pub use mpd_query::{mpd_query_function_factory, MpdQueryStatementFactory}; pub use repeat::{repeat_function_factory, RepeatStatementFactory}; pub use reset::{reset_function_factory, ResetStatementFactory}; pub use sql_init::{sql_init_function_factory, SqlInitStatementFactory}; diff --git a/mps-interpreter/src/lang/vocabulary/mpd_query.rs b/mps-interpreter/src/lang/vocabulary/mpd_query.rs new file mode 100644 index 0000000..1c20e37 --- /dev/null +++ b/mps-interpreter/src/lang/vocabulary/mpd_query.rs @@ -0,0 +1,193 @@ +use std::collections::VecDeque; +use std::fmt::{Debug, Display, Error, Formatter}; +use std::iter::Iterator; +use std::net::SocketAddr; + +use crate::tokens::MpsToken; +use crate::MpsContext; + +use crate::lang::{MpsLanguageDictionary, repeated_tokens, Lookup}; +use crate::lang::{MpsFunctionFactory, MpsFunctionStatementFactory, MpsIteratorItem, MpsOp, PseudoOp}; +use crate::lang::{RuntimeError, SyntaxError, RuntimeOp}; +use crate::lang::utility::{assert_token, assert_token_raw}; +use crate::lang::MpsTypePrimitive; +use crate::processing::general::MpsType; +use crate::MpsItem; + +#[cfg(feature = "mpd")] +#[derive(Debug)] +pub struct MpdQueryStatement { + context: Option, + addr: Lookup, + params: Vec<(String, Lookup)>, + results: Option>, +} + +#[cfg(feature = "mpd")] +impl Display for MpdQueryStatement { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "empty()") + } +} + +#[cfg(feature = "mpd")] +impl std::clone::Clone for MpdQueryStatement { + fn clone(&self) -> Self { + Self { + context: None, + addr: self.addr.clone(), + params: self.params.clone(), + results: None, + } + } +} + +#[cfg(feature = "mpd")] +impl Iterator for MpdQueryStatement { + type Item = MpsIteratorItem; + + fn next(&mut self) -> Option { + //let ctx = self.context.as_mut().unwrap(); + if self.results.is_none() { + self.results = Some(VecDeque::with_capacity(0)); // in case of failure + // build address + let addr_str = match self.addr.get(self.context.as_mut().unwrap()) { + Ok(MpsType::Primitive(a)) => a.as_str(), + Ok(x) => return Some(Err( + RuntimeError { + line: 0, + msg: format!("Cannot use non-primitive `{}` as IP address", x), + op: PseudoOp::from_printable(self), + } + )), + Err(e) => return Some(Err(e.with(RuntimeOp(PseudoOp::from_printable(self))))), + }; + let addr: SocketAddr = match addr_str.parse() { + Ok(a) => a, + Err(e) => return Some(Err(RuntimeError { + line: 0, + op: PseudoOp::from_printable(self), + msg: format!("Cannot convert `{}` to IP Address: {}", addr_str, e), + })) + }; + // build params + let mut new_params = Vec::<(&str, String)>::with_capacity(self.params.len()); + for (term, value) in self.params.iter() { + let static_val = match value.get(self.context.as_mut().unwrap()) { + Ok(MpsType::Primitive(a)) => a.as_str(), + Ok(x) => return Some(Err( + RuntimeError { + line: 0, + msg: format!("Cannot use non-primitive `{}` MPS query value", x), + op: PseudoOp::from_printable(self), + } + )), + Err(e) => return Some(Err(e.with(RuntimeOp(PseudoOp::from_printable(self))))), + }; + new_params.push((term, static_val)); + } + self.results = Some(match self.context.as_mut().unwrap().mpd_database.one_shot_search(addr, new_params) { + Ok(items) => items, + Err(e) => return Some(Err(e.with(RuntimeOp(PseudoOp::from_printable(self))))) + }); + } + let results = self.results.as_mut().unwrap(); + results.pop_front().map(|x| Ok(x)) + } + + fn size_hint(&self) -> (usize, Option) { + (0, Some(0)) + } +} + +#[cfg(feature = "mpd")] +impl MpsOp for MpdQueryStatement { + fn enter(&mut self, ctx: MpsContext) { + self.context = Some(ctx) + } + + fn escape(&mut self) -> MpsContext { + self.context.take().unwrap() + } + + fn is_resetable(&self) -> bool { + true + } + + fn reset(&mut self) -> Result<(), RuntimeError> { + Ok(()) + } + + fn dup(&self) -> Box { + Box::new(self.clone()) + } +} + +#[cfg(feature = "mpd")] +pub struct MpdQueryFunctionFactory; + +#[cfg(feature = "mpd")] +impl MpsFunctionFactory for MpdQueryFunctionFactory { + fn is_function(&self, name: &str) -> bool { + name == "mpd" + } + + fn build_function_params( + &self, + _name: String, + tokens: &mut VecDeque, + _dict: &MpsLanguageDictionary, + ) -> Result { + // mpd(address, term = value, ...) + let addr_lookup = Lookup::parse(tokens)?; + if tokens.is_empty() { + Ok(MpdQueryStatement { + context: None, + addr: addr_lookup, + params: vec![("any".to_string(), Lookup::Static(MpsType::Primitive(MpsTypePrimitive::String("".to_owned()))))], + results: None, + }) + } else { + assert_token_raw(MpsToken::Comma, tokens)?; + let keyword_params = repeated_tokens( + |tokens| { + let term = assert_token( + |t| match t { + MpsToken::Name(n) => Some(n), + _ => None, + }, + MpsToken::Name("term".to_string()), + tokens)?; + assert_token_raw(MpsToken::Equals, tokens)?; + let val = Lookup::parse(tokens)?; + Ok(Some((term, val))) + }, + MpsToken::Comma + ).ingest_all(tokens)?; + Ok(MpdQueryStatement { + context: None, + addr: addr_lookup, + params: keyword_params, + results: None, + }) + } + } +} + +#[cfg(feature = "mpd")] +pub type MpdQueryStatementFactory = MpsFunctionStatementFactory; + +#[cfg(feature = "mpd")] +#[inline(always)] +pub fn mpd_query_function_factory() -> MpdQueryStatementFactory { + MpdQueryStatementFactory::new(MpdQueryFunctionFactory) +} + +#[cfg(not(feature = "mpd"))] +pub type MpdQueryStatementFactory = super::EmptyStatementFactory; + +#[cfg(not(feature = "mpd"))] +#[inline(always)] +pub fn mpd_query_function_factory() -> MpdQueryStatementFactory { + super::empty_function_factory() +} diff --git a/mps-interpreter/src/lib.rs b/mps-interpreter/src/lib.rs index 50c2664..ae561e7 100644 --- a/mps-interpreter/src/lib.rs +++ b/mps-interpreter/src/lib.rs @@ -133,6 +133,10 @@ //! //! Retrieve all files from a folder, matching a regex pattern. //! +//! ### mpd(address, term = value, term2 = value2, ...); +//! +//! Retrieve songs from a music player daemon at `address`. If compiled without the `music_library` feature, this is equivalent to `empty()`. +//! //! ### reset(iterable); //! //! Explicitly reset an iterable. This useful for reusing an iterable variable. diff --git a/mps-interpreter/src/processing/mod.rs b/mps-interpreter/src/processing/mod.rs index 9e09080..55ea245 100644 --- a/mps-interpreter/src/processing/mod.rs +++ b/mps-interpreter/src/processing/mod.rs @@ -1,6 +1,8 @@ mod filesystem; #[cfg(feature = "advanced")] mod music_analysis; +#[cfg(feature = "mpd")] +mod mpd; mod sql; mod variables; @@ -8,6 +10,8 @@ mod variables; pub mod database { pub use super::sql::{MpsDatabaseQuerier, MpsSQLiteExecutor, QueryResult}; + #[cfg(feature = "mpd")] + pub use super::mpd::{MpsMpdQuerier, MpsMpdExecutor}; } pub mod general { diff --git a/mps-interpreter/src/processing/mpd.rs b/mps-interpreter/src/processing/mpd.rs new file mode 100644 index 0000000..1a40e4d --- /dev/null +++ b/mps-interpreter/src/processing/mpd.rs @@ -0,0 +1,106 @@ +use core::fmt::Debug; +use std::collections::VecDeque; +use std::net::{SocketAddr, TcpStream}; +use std::iter::Iterator; + +use mpd::Client; +use mpd::{Query, Term, Song}; + +use crate::lang::RuntimeMsg; +use crate::MpsItem; +use crate::lang::MpsTypePrimitive; + +/// Music Player Daemon interface for interacting with it's database +pub trait MpsMpdQuerier: Debug { + fn connect(&mut self, addr: SocketAddr) -> Result<(), RuntimeMsg>; + + fn search(&mut self, params: Vec<(&str, String)>) -> Result, RuntimeMsg>; + + fn one_shot_search(&self, addr: SocketAddr, params: Vec<(&str, String)>) -> Result, RuntimeMsg>; +} + +#[derive(Default, Debug)] +pub struct MpsMpdExecutor { + connection: Option>, +} + +impl MpsMpdQuerier for MpsMpdExecutor { + fn connect(&mut self, addr: SocketAddr) -> Result<(), RuntimeMsg> { + self.connection = Some(Client::connect(addr).map_err(|e| RuntimeMsg(format!("MPD connection error: {}", e)))?); + Ok(()) + } + + fn search(&mut self, params: Vec<(&str, String)>) -> Result, RuntimeMsg> { + if self.connection.is_none() { + return Err(RuntimeMsg("MPD not connected".to_string())); + } + //let music_dir = self.connection.as_mut().unwrap().music_directory().map_err(|e| RuntimeMsg(format!("MPD command error: {}", e)))?; + let mut query = Query::new(); + let mut query_mut = &mut query; + for (term, value) in params { + query_mut = query_mut.and(str_to_term(term), value); + } + let songs = self.connection.as_mut().unwrap().search(query_mut, None).map_err(|e| RuntimeMsg(format!("MPD search error: {}", e)))?; + Ok(songs.into_iter().map(|x| song_to_item(x)).collect()) + } + + fn one_shot_search(&self, addr: SocketAddr, params: Vec<(&str, String)>) -> Result, RuntimeMsg> { + let mut connection = Client::connect(addr).map_err(|e| RuntimeMsg(format!("MPD connection error: {}", e)))?; + //let music_dir = connection.music_directory().map_err(|e| RuntimeMsg(format!("MPD command error: {}", e)))?; + let mut query = Query::new(); + let mut query_mut = &mut query; + for (term, value) in params { + query_mut = query_mut.and(str_to_term(term), value); + } + let songs = connection.search(query_mut, None).map_err(|e| RuntimeMsg(format!("MPD search error: {}", e)))?; + Ok(songs.into_iter().map(|x| song_to_item(x)).collect()) + } +} + +#[inline] +fn song_to_item(song: Song) -> MpsItem { + let mut item = MpsItem::new(); + //item.set_field("filename", format!("{}{}{}", root_dir, std::path::MAIN_SEPARATOR, song.file).into()); + item.set_field("filename", format!("mpd://{}", song.file).into()); + if let Some(name) = song.name { + item.set_field("name", name.into()); + } + if let Some(title) = song.title { + item.set_field("title", title.into()); + } + + /* + if let Some(last_mod) = song.last_mod { + item.set_field("last_modified", last_modified.into()); + } + */ + + if let Some(dur) = song.duration { + item.set_field("duration", dur.num_seconds().into()); + } + if let Some(place) = song.place { + item.set_field("tracknumber", (place.pos as u64).into()); + } + + /* + if let Some(range) = song.range { + item.set_field("range", range.into()); + } + */ + + for (tag, value) in song.tags { + item.set_field(&tag, MpsTypePrimitive::parse(value)); + } + item +} + +#[inline] +fn str_to_term<'a>(s: &'a str) -> Term<'a> { + match s { + "any" => Term::Any, + "file" => Term::File, + "base" => Term::Base, + "lastmod" => Term::LastMod, + x => Term::Tag(x.into()) + } +} diff --git a/mps-interpreter/tests/single_line.rs b/mps-interpreter/tests/single_line.rs index 40b0b42..c6370fd 100644 --- a/mps-interpreter/tests/single_line.rs +++ b/mps-interpreter/tests/single_line.rs @@ -829,3 +829,22 @@ fn execute_nonemptyfilter_line() -> Result<(), MpsError> { true, ) } + +#[test] +fn execute_mpdfunction_line() -> Result<(), MpsError> { + execute_single_line( + "mpd(`127.0.0.1:6600`, artist=`Bruno Mars`)", + false, + true, + )?; + execute_single_line( + "mpd(`127.0.0.1:6600`, title=`something very long that should match absolutely nothing, probably, hopefully...`)", + true, + true, + )?; + execute_single_line( + "mpd(`127.0.0.1:6600`)", + false, + true, + ) +} diff --git a/src/help.rs b/src/help.rs index 485b97b..2576e74 100644 --- a/src/help.rs +++ b/src/help.rs @@ -33,6 +33,10 @@ These always return an iterable which can be manipulated. files(folder = `path/to/music`, recursive = true|false, regex = `pattern`) Retrieve all files from a folder, matching a regex pattern. + mpd(address, term = value, term2 = value2, ...); + + Retrieve songs from a music player daemon at address. If compiled without the music_library feature, this is equivalent to the empty() function. + reset(iterable) Explicitly reset an iterable. This useful for reusing an iterable variable.