Add mpd() query functionality

This commit is contained in:
NGnius (Graham) 2022-05-14 14:24:18 -04:00
parent 34487c02eb
commit 3b756bf0ad
13 changed files with 378 additions and 1 deletions

24
Cargo.lock generated
View file

@ -212,6 +212,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "bufstream"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8"
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.9.1" version = "3.9.1"
@ -1182,6 +1188,17 @@ dependencies = [
"autocfg", "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]] [[package]]
name = "mpris-player" name = "mpris-player"
version = "0.6.1" version = "0.6.1"
@ -1206,6 +1223,7 @@ dependencies = [
"bliss-audio-symphonia", "bliss-audio-symphonia",
"criterion", "criterion",
"dirs", "dirs",
"mpd",
"rand", "rand",
"regex 1.5.5", "regex 1.5.5",
"rusqlite", "rusqlite",
@ -1906,6 +1924,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-serialize"
version = "0.3.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.0" version = "0.4.0"

View file

@ -16,6 +16,7 @@ regex = { version = "1" }
rand = { version = "0.8" } rand = { version = "0.8" }
shellexpand = { version = "2.1", optional = true } shellexpand = { version = "2.1", optional = true }
bliss-audio-symphonia = { version = "0.4", optional = true, path = "../bliss-rs" } bliss-audio-symphonia = { version = "0.4", optional = true, path = "../bliss-rs" }
mpd = { version = "0.0.12", optional = true }
[dev-dependencies] [dev-dependencies]
criterion = "0.3" criterion = "0.3"
@ -26,6 +27,6 @@ harness = false
[features] [features]
default = [ "music_library", "ergonomics", "advanced" ] 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 ergonomics = ["shellexpand"] # niceties like ~ in pathes
advanced = ["bliss-audio-symphonia"] # advanced language features like bliss playlist generation advanced = ["bliss-audio-symphonia"] # advanced language features like bliss playlist generation

View file

@ -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. 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 ### Functions
Similar to most other languages: `function_name(param1, param2, etc.);`. 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.). 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. 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); #### reset(iterable);
Explicitly reset an iterable. This useful for reusing an iterable variable. Explicitly reset an iterable. This useful for reusing an iterable variable.

View file

@ -1,6 +1,8 @@
#[cfg(feature = "advanced")] #[cfg(feature = "advanced")]
use super::processing::advanced::{MpsDefaultAnalyzer, MpsMusicAnalyzer}; use super::processing::advanced::{MpsDefaultAnalyzer, MpsMusicAnalyzer};
use super::processing::database::{MpsDatabaseQuerier, MpsSQLiteExecutor}; use super::processing::database::{MpsDatabaseQuerier, MpsSQLiteExecutor};
#[cfg(feature = "mpd")]
use super::processing::database::{MpsMpdQuerier, MpsMpdExecutor};
use super::processing::general::{ use super::processing::general::{
MpsFilesystemExecutor, MpsFilesystemQuerier, MpsOpStorage, MpsVariableStorer, MpsFilesystemExecutor, MpsFilesystemQuerier, MpsOpStorage, MpsVariableStorer,
}; };
@ -13,6 +15,8 @@ pub struct MpsContext {
pub filesystem: Box<dyn MpsFilesystemQuerier>, pub filesystem: Box<dyn MpsFilesystemQuerier>,
#[cfg(feature = "advanced")] #[cfg(feature = "advanced")]
pub analysis: Box<dyn MpsMusicAnalyzer>, pub analysis: Box<dyn MpsMusicAnalyzer>,
#[cfg(feature = "mpd")]
pub mpd_database: Box<dyn MpsMpdQuerier>,
} }
impl Default for MpsContext { impl Default for MpsContext {
@ -23,6 +27,8 @@ impl Default for MpsContext {
filesystem: Box::new(MpsFilesystemExecutor::default()), filesystem: Box::new(MpsFilesystemExecutor::default()),
#[cfg(feature = "advanced")] #[cfg(feature = "advanced")]
analysis: Box::new(MpsDefaultAnalyzer::default()), analysis: Box::new(MpsDefaultAnalyzer::default()),
#[cfg(feature = "mpd")]
mpd_database: Box::new(MpsMpdExecutor::default()),
} }
} }
} }

View file

@ -48,6 +48,7 @@ pub(crate) fn standard_vocab(vocabulary: &mut MpsLanguageDictionary) {
// functions don't enforce bracket coherence // functions don't enforce bracket coherence
// -- function().() is valid despite the ).( in between brackets // -- function().() is valid despite the ).( in between brackets
.add(crate::lang::vocabulary::sql_function_factory()) .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::simple_sql_function_factory())
.add(crate::lang::vocabulary::repeat_function_factory()) .add(crate::lang::vocabulary::repeat_function_factory())
.add(crate::lang::vocabulary::AssignStatementFactory) .add(crate::lang::vocabulary::AssignStatementFactory)

View file

@ -29,6 +29,11 @@ impl MpsItem {
self 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<MpsTypePrimitive> { pub fn remove_field(&mut self, name: &str) -> Option<MpsTypePrimitive> {
self.fields.remove(name) self.fields.remove(name)
} }

View file

@ -2,6 +2,7 @@ mod empties;
pub(crate) mod empty; pub(crate) mod empty;
mod files; mod files;
mod intersection; mod intersection;
mod mpd_query;
mod repeat; mod repeat;
mod reset; mod reset;
mod sql_init; mod sql_init;
@ -14,6 +15,7 @@ pub use empties::{empties_function_factory, EmptiesStatementFactory};
pub use empty::{empty_function_factory, EmptyStatementFactory}; pub use empty::{empty_function_factory, EmptyStatementFactory};
pub use files::{files_function_factory, FilesStatementFactory}; pub use files::{files_function_factory, FilesStatementFactory};
pub use intersection::{intersection_function_factory, IntersectionStatementFactory}; 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 repeat::{repeat_function_factory, RepeatStatementFactory};
pub use reset::{reset_function_factory, ResetStatementFactory}; pub use reset::{reset_function_factory, ResetStatementFactory};
pub use sql_init::{sql_init_function_factory, SqlInitStatementFactory}; pub use sql_init::{sql_init_function_factory, SqlInitStatementFactory};

View file

@ -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<MpsContext>,
addr: Lookup,
params: Vec<(String, Lookup)>,
results: Option<VecDeque<MpsItem>>,
}
#[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<Self::Item> {
//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<usize>) {
(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<dyn MpsOp> {
Box::new(self.clone())
}
}
#[cfg(feature = "mpd")]
pub struct MpdQueryFunctionFactory;
#[cfg(feature = "mpd")]
impl MpsFunctionFactory<MpdQueryStatement> for MpdQueryFunctionFactory {
fn is_function(&self, name: &str) -> bool {
name == "mpd"
}
fn build_function_params(
&self,
_name: String,
tokens: &mut VecDeque<MpsToken>,
_dict: &MpsLanguageDictionary,
) -> Result<MpdQueryStatement, SyntaxError> {
// 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<MpdQueryStatement, MpdQueryFunctionFactory>;
#[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()
}

View file

@ -133,6 +133,10 @@
//! //!
//! Retrieve all files from a folder, matching a 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 `empty()`.
//!
//! ### reset(iterable); //! ### reset(iterable);
//! //!
//! Explicitly reset an iterable. This useful for reusing an iterable variable. //! Explicitly reset an iterable. This useful for reusing an iterable variable.

View file

@ -1,6 +1,8 @@
mod filesystem; mod filesystem;
#[cfg(feature = "advanced")] #[cfg(feature = "advanced")]
mod music_analysis; mod music_analysis;
#[cfg(feature = "mpd")]
mod mpd;
mod sql; mod sql;
mod variables; mod variables;
@ -8,6 +10,8 @@ mod variables;
pub mod database { pub mod database {
pub use super::sql::{MpsDatabaseQuerier, MpsSQLiteExecutor, QueryResult}; pub use super::sql::{MpsDatabaseQuerier, MpsSQLiteExecutor, QueryResult};
#[cfg(feature = "mpd")]
pub use super::mpd::{MpsMpdQuerier, MpsMpdExecutor};
} }
pub mod general { pub mod general {

View file

@ -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<VecDeque<MpsItem>, RuntimeMsg>;
fn one_shot_search(&self, addr: SocketAddr, params: Vec<(&str, String)>) -> Result<VecDeque<MpsItem>, RuntimeMsg>;
}
#[derive(Default, Debug)]
pub struct MpsMpdExecutor {
connection: Option<Client<TcpStream>>,
}
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<VecDeque<MpsItem>, 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<VecDeque<MpsItem>, 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())
}
}

View file

@ -829,3 +829,22 @@ fn execute_nonemptyfilter_line() -> Result<(), MpsError> {
true, 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,
)
}

View file

@ -33,6 +33,10 @@ These always return an iterable which can be manipulated.
files(folder = `path/to/music`, recursive = true|false, regex = `pattern`) files(folder = `path/to/music`, recursive = true|false, regex = `pattern`)
Retrieve all files from a folder, matching a 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) reset(iterable)
Explicitly reset an iterable. This useful for reusing an iterable variable. Explicitly reset an iterable. This useful for reusing an iterable variable.