Add mpd() query functionality
This commit is contained in:
parent
34487c02eb
commit
3b756bf0ad
13 changed files with 378 additions and 1 deletions
24
Cargo.lock
generated
24
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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};
|
||||||
|
|
193
mps-interpreter/src/lang/vocabulary/mpd_query.rs
Normal file
193
mps-interpreter/src/lang/vocabulary/mpd_query.rs
Normal 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()
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
106
mps-interpreter/src/processing/mpd.rs
Normal file
106
mps-interpreter/src/processing/mpd.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue