diff --git a/interpreter/src/lang/vocabulary/filters/field_filter.rs b/interpreter/src/lang/vocabulary/filters/field_filter.rs index a49c92b..e12a9ed 100644 --- a/interpreter/src/lang/vocabulary/filters/field_filter.rs +++ b/interpreter/src/lang/vocabulary/filters/field_filter.rs @@ -2,7 +2,7 @@ use std::collections::VecDeque; use std::fmt::{Debug, Display, Error, Formatter}; use super::utility::{assert_comparison_operator, comparison_op}; -use crate::lang::utility::{assert_token, assert_type, check_is_type}; +use crate::lang::utility::{assert_token, assert_type, check_is_type, assert_token_raw}; use crate::lang::LanguageDictionary; use crate::lang::TypePrimitive; use crate::lang::{FilterFactory, FilterPredicate, FilterStatementFactory}; @@ -39,7 +39,7 @@ impl Display for FieldFilter { let comp_op = comparison_op(&self.comparison); match &self.val { VariableOrValue::Variable(name) => { - write!(f, "{} {} {}", self.field_name, comp_op, name) + write!(f, ".{} {} {}", self.field_name, comp_op, name) } VariableOrValue::Value(t) => write!(f, "{} {} {}", self.field_name, comp_op, t), } @@ -100,15 +100,18 @@ pub struct FieldFilterFactory; impl FilterFactory for FieldFilterFactory { fn is_filter(&self, tokens: &VecDeque<&Token>) -> bool { let tokens_len = tokens.len(); - (tokens_len >= 2 - // field > variable OR field < variable - && tokens[0].is_name() - && (tokens[1].is_open_angle_bracket() || tokens[1].is_close_angle_bracket())) - || (tokens_len >= 3 // field >= variable OR field <= variable OR field != variable - && tokens[0].is_name() - && (tokens[1].is_open_angle_bracket() || tokens[1].is_close_angle_bracket() || tokens[1].is_equals() || tokens[1].is_exclamation()) - && tokens[2].is_equals() - && !(tokens_len > 3 && tokens[3].is_equals())) + (tokens_len >= 3 + // .field > variable OR .field < variable + && tokens[0].is_dot() + && tokens[1].is_name() + && (tokens[2].is_open_angle_bracket() || tokens[2].is_close_angle_bracket())) + || (tokens_len >= 4 // .field >= variable OR .field <= variable OR .field != variable OR .field == variable + && tokens[0].is_dot() + && tokens[1].is_name() + && (tokens[2].is_open_angle_bracket() || tokens[2].is_close_angle_bracket() || tokens[2].is_equals() || tokens[2].is_exclamation()) + && tokens[3].is_equals() + && !(tokens_len > 4 && tokens[4].is_equals()) + ) } fn build_filter( @@ -116,6 +119,7 @@ impl FilterFactory for FieldFilterFactory { tokens: &mut VecDeque, _dict: &LanguageDictionary, ) -> Result { + assert_token_raw(Token::Dot, tokens)?; let field = assert_token( |t| match t { Token::Name(n) => Some(n), diff --git a/interpreter/src/lang/vocabulary/filters/field_filter_maybe.rs b/interpreter/src/lang/vocabulary/filters/field_filter_maybe.rs index 6e5f35e..eb8ec67 100644 --- a/interpreter/src/lang/vocabulary/filters/field_filter_maybe.rs +++ b/interpreter/src/lang/vocabulary/filters/field_filter_maybe.rs @@ -13,15 +13,17 @@ pub struct FieldFilterMaybeFactory; impl FilterFactory for FieldFilterMaybeFactory { fn is_filter(&self, tokens: &VecDeque<&Token>) -> bool { let tokens_len = tokens.len(); - (tokens_len >= 3 // field > variable OR field < variable - && tokens[0].is_name() - && (tokens[1].is_interrogation() || tokens[1].is_exclamation()) - && (tokens[2].is_open_angle_bracket() || tokens[2].is_close_angle_bracket())) - || (tokens_len >= 4 // field >= variable OR field <= variable OR field != variable - && tokens[0].is_name() - && (tokens[1].is_interrogation() || tokens[1].is_exclamation()) - && (tokens[2].is_open_angle_bracket() || tokens[2].is_close_angle_bracket() || tokens[2].is_equals() || tokens[2].is_exclamation()) - && tokens[3].is_equals()) + (tokens_len >= 4 // .field > variable OR .field < variable + && tokens[0].is_dot() + && tokens[1].is_name() + && (tokens[2].is_interrogation() || tokens[2].is_exclamation()) + && (tokens[3].is_open_angle_bracket() || tokens[3].is_close_angle_bracket())) + || (tokens_len >= 5 // .field >= variable OR .field <= variable OR .field != variable OR .field == variable + && tokens[0].is_dot() + && tokens[1].is_name() + && (tokens[2].is_interrogation() || tokens[2].is_exclamation()) + && (tokens[3].is_open_angle_bracket() || tokens[3].is_close_angle_bracket() || tokens[3].is_equals() || tokens[3].is_exclamation()) + && tokens[4].is_equals()) } fn build_filter( @@ -29,6 +31,7 @@ impl FilterFactory for FieldFilterMaybeFactory { tokens: &mut VecDeque, _dict: &LanguageDictionary, ) -> Result { + assert_token_raw(Token::Dot, tokens)?; let field = assert_token( |t| match t { Token::Name(n) => Some(n), @@ -65,7 +68,7 @@ impl FilterFactory for FieldFilterMaybeFactory { Token::Name(n) => Some(n), _ => None, }, - Token::Name("variable_name".into()), + Token::Name("variable|literal".into()), tokens, )?); //assert_empty(tokens)?; diff --git a/interpreter/src/lang/vocabulary/filters/field_like_filter.rs b/interpreter/src/lang/vocabulary/filters/field_like_filter.rs index cbb65f3..49331c6 100644 --- a/interpreter/src/lang/vocabulary/filters/field_like_filter.rs +++ b/interpreter/src/lang/vocabulary/filters/field_like_filter.rs @@ -29,8 +29,8 @@ impl FieldLikeFilter { impl Display for FieldLikeFilter { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { match &self.val { - VariableOrValue::Variable(name) => write!(f, "{} like {}", self.field_name, name), - VariableOrValue::Value(t) => write!(f, "{} like {}", self.field_name, t), + VariableOrValue::Variable(name) => write!(f, ".{} like {}", self.field_name, name), + VariableOrValue::Value(t) => write!(f, ".{} like {}", self.field_name, t), } } } @@ -81,13 +81,15 @@ pub struct FieldLikeFilterFactory; impl FilterFactory for FieldLikeFilterFactory { fn is_filter(&self, tokens: &VecDeque<&Token>) -> bool { let tokens_len = tokens.len(); - (tokens_len >= 2 // field like variable - && tokens[0].is_name() - && (check_name("like", tokens[1]) || check_name("unlike", tokens[1]))) - || (tokens_len >= 3 // field? like variable OR field! like variable - && tokens[0].is_name() - && (tokens[1].is_interrogation() || tokens[1].is_exclamation()) + (tokens_len >= 3 // field like variable + && tokens[0].is_dot() + && tokens[1].is_name() && (check_name("like", tokens[2]) || check_name("unlike", tokens[2]))) + || (tokens_len >= 4 // field? like variable OR field! like variable + && tokens[0].is_dot() + && tokens[1].is_name() + && (tokens[2].is_interrogation() || tokens[2].is_exclamation()) + && (check_name("like", tokens[3]) || check_name("unlike", tokens[3]))) } fn build_filter( @@ -95,6 +97,7 @@ impl FilterFactory for FieldLikeFilterFactory { tokens: &mut VecDeque, _dict: &LanguageDictionary, ) -> Result { + assert_token_raw(Token::Dot, tokens)?; let field = assert_token( |t| match t { Token::Name(n) => Some(n), diff --git a/interpreter/src/lang/vocabulary/filters/field_match_filter.rs b/interpreter/src/lang/vocabulary/filters/field_match_filter.rs index 853e571..adab26d 100644 --- a/interpreter/src/lang/vocabulary/filters/field_match_filter.rs +++ b/interpreter/src/lang/vocabulary/filters/field_match_filter.rs @@ -26,8 +26,8 @@ pub struct FieldRegexFilter { impl Display for FieldRegexFilter { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { match &self.val { - VariableOrValue::Variable(name) => write!(f, "{} matches {}", self.field_name, name), - VariableOrValue::Value(t) => write!(f, "{} matches {}", self.field_name, t), + VariableOrValue::Variable(name) => write!(f, ".{} matches {}", self.field_name, name), + VariableOrValue::Value(t) => write!(f, ".{} matches {}", self.field_name, t), } } } @@ -90,13 +90,15 @@ pub struct FieldRegexFilterFactory; impl FilterFactory for FieldRegexFilterFactory { fn is_filter(&self, tokens: &VecDeque<&Token>) -> bool { let tokens_len = tokens.len(); - (tokens_len >= 2 // field like variable - && tokens[0].is_name() - && check_name("matches", tokens[1])) - || (tokens_len >= 3 // field? like variable OR field! like variable - && tokens[0].is_name() - && (tokens[1].is_interrogation() || tokens[1].is_exclamation()) + (tokens_len >= 3 // .field like variable + && tokens[0].is_dot() + && tokens[1].is_name() && check_name("matches", tokens[2])) + || (tokens_len >= 4 // .field? like variable OR .field! like variable + && tokens[0].is_dot() + && tokens[1].is_name() + && (tokens[2].is_interrogation() || tokens[2].is_exclamation()) + && check_name("matches", tokens[3])) } fn build_filter( @@ -104,6 +106,7 @@ impl FilterFactory for FieldRegexFilterFactory { tokens: &mut VecDeque, _dict: &LanguageDictionary, ) -> Result { + assert_token_raw(Token::Dot, tokens)?; let field = assert_token( |t| match t { Token::Name(n) => Some(n), diff --git a/interpreter/src/lang/vocabulary/filters/unique.rs b/interpreter/src/lang/vocabulary/filters/unique.rs index f1fb8ce..5fba264 100644 --- a/interpreter/src/lang/vocabulary/filters/unique.rs +++ b/interpreter/src/lang/vocabulary/filters/unique.rs @@ -20,7 +20,7 @@ pub struct UniqueFieldFilter { impl Display for UniqueFieldFilter { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { - write!(f, "unique {}", &self.field) + write!(f, "unique .{}", &self.field) } } @@ -90,7 +90,7 @@ pub struct UniqueFilterFactory; impl FilterFactory for UniqueFilterFactory { fn is_filter(&self, tokens: &VecDeque<&Token>) -> bool { - tokens.len() >= 2 && check_name("unique", tokens[0]) + tokens.len() >= 2 && check_name("unique", tokens[0]) && tokens[1].is_dot() } fn build_filter( @@ -99,6 +99,7 @@ impl FilterFactory for UniqueFilterFactory { _dict: &LanguageDictionary, ) -> Result { assert_name("unique", tokens)?; + assert_token_raw(Token::Dot, tokens)?; let field_name = assert_token( |t| match t { Token::Name(s) => Some(s), diff --git a/interpreter/src/lang/vocabulary/sorters/field_sorter.rs b/interpreter/src/lang/vocabulary/sorters/field_sorter.rs index 5d1b5ef..99cf918 100644 --- a/interpreter/src/lang/vocabulary/sorters/field_sorter.rs +++ b/interpreter/src/lang/vocabulary/sorters/field_sorter.rs @@ -2,7 +2,7 @@ use std::cmp::Ordering; use std::collections::VecDeque; use std::fmt::{Debug, Display, Error, Formatter}; -use crate::lang::utility::assert_token; +use crate::lang::utility::{assert_token, assert_token_raw}; use crate::lang::{IteratorItem, LanguageDictionary, Op}; use crate::lang::{RuntimeMsg, SyntaxError}; use crate::lang::{SortStatementFactory, Sorter, SorterFactory}; @@ -52,7 +52,7 @@ impl Sorter for FieldSorter { impl Display for FieldSorter { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { - write!(f, "{}", self.field_name) + write!(f, ".{}", self.field_name) } } @@ -60,7 +60,7 @@ pub struct FieldSorterFactory; impl SorterFactory for FieldSorterFactory { fn is_sorter(&self, tokens: &VecDeque<&Token>) -> bool { - !tokens.is_empty() && tokens[0].is_name() + !tokens.is_empty() && tokens[0].is_dot() } fn build_sorter( @@ -68,6 +68,7 @@ impl SorterFactory for FieldSorterFactory { tokens: &mut VecDeque, _dict: &LanguageDictionary, ) -> Result { + assert_token_raw(Token::Dot, tokens)?; let name = assert_token( |t| match t { Token::Name(s) => Some(s), diff --git a/interpreter/src/lang/vocabulary/sorters/shuffle.rs b/interpreter/src/lang/vocabulary/sorters/shuffle.rs index 98a5d19..495ce12 100644 --- a/interpreter/src/lang/vocabulary/sorters/shuffle.rs +++ b/interpreter/src/lang/vocabulary/sorters/shuffle.rs @@ -3,7 +3,7 @@ use std::fmt::{Debug, Display, Error, Formatter}; use rand::{thread_rng, Rng}; -use crate::lang::utility::{assert_name, check_name}; +use crate::lang::utility::{assert_name, check_name, assert_token_raw}; use crate::lang::{IteratorItem, LanguageDictionary, Op}; use crate::lang::{RuntimeMsg, SyntaxError}; use crate::lang::{SortStatementFactory, Sorter, SorterFactory}; @@ -71,10 +71,11 @@ pub struct ShuffleSorterFactory; impl SorterFactory for ShuffleSorterFactory { fn is_sorter(&self, tokens: &VecDeque<&Token>) -> bool { - (!tokens.is_empty() && check_name("shuffle", tokens[0])) - || (tokens.len() > 1 - && check_name("random", tokens[0]) - && check_name("shuffle", tokens[1])) + (!tokens.len() > 1 && tokens[0].is_tilde() && check_name("shuffle", tokens[1])) + || (tokens.len() > 2 + && tokens[0].is_tilde() + && check_name("random", tokens[1]) + && check_name("shuffle", tokens[2])) } fn build_sorter( @@ -82,6 +83,7 @@ impl SorterFactory for ShuffleSorterFactory { tokens: &mut VecDeque, _dict: &LanguageDictionary, ) -> Result { + assert_token_raw(Token::Tilde, tokens)?; if check_name("random", &tokens[0]) { assert_name("random", tokens)?; } diff --git a/interpreter/src/lib.rs b/interpreter/src/lib.rs index 460f617..963991d 100644 --- a/interpreter/src/lib.rs +++ b/interpreter/src/lib.rs @@ -41,23 +41,23 @@ //! Filters are statements of the format `something.(predicate)`, where "something" is a variable name or another valid statement, and "predicate" is a valid filter predicate (see below). //! E.g. `files(folder="~/Music/", recursive=true).(title == "Romantic Traffic");` is valid filter syntax to filter all songs in the Music folder for songs named "Romantic Traffic" (probably just one song). //! -//! ### field == something +//! ### .field == something //! -//! ### field like something +//! ### .field like something //! -//! ### field unlike something +//! ### .field unlike something //! -//! ### field matches some_regex +//! ### .field matches some_regex //! -//! ### field != something +//! ### .field != something //! -//! ### field >= something +//! ### .field >= something //! -//! ### field > something +//! ### .field > something //! -//! ### field <= something +//! ### .field <= something //! -//! ### field < something -- e.g. `iterable.(title == "Romantic Traffic");` +//! ### .field < something -- e.g. `iterable.(.title == "Romantic Traffic");` //! //! Compare all items, keeping only those that match the condition. Valid field names change depending on what information is available when the Item is populated, but usually title, artist, album, genre, track, filename are valid fields. Optionally, a ? or ! can be added to the end of the field name to skip items whose field is missing/incomparable, or keep all items whose field is missing/incomparable (respectively). //! @@ -87,7 +87,7 @@ //! Replace items matching the filter with operation1 and replace items not matching the filter with operation2. The `else operation2` part may be omitted to preserve items not matching the filter. To perform operations with the current item, use the special variable `item`. The replacement filter may not contain || -- instead, use multiple filters chained together. //! //! ### unique -//! ### unique field -- e.g. `iterable.(unique title);` +//! ### unique field -- e.g. `iterable.(unique .title);` //! //! 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. //! @@ -166,15 +166,19 @@ //! ## Sorters //! Operations to sort the items in an iterable: `iterable~(sorter)` OR `iterable.sort(sorter)`. //! -//! ### field -- e.g. `iterable~(filename);` +//! ### .field -- e.g. `iterable~(.filename);` //! //! Sort by an Item field. Valid field names change depending on what information is available when the Item is populated, but usually title, artist, album, genre, track, filename are valid fields. Items with a missing/incomparable fields will be sorted to the end. //! -//! ### shuffle -//! ### random shuffle -- e.g. `iterable~(shuffle);` +//! ### ~shuffle +//! ### random shuffle -- e.g. `iterable~(~shuffle);` //! //! Shuffle the songs in the iterator. This is random for up to 2^16 items, and then the randomness degrades (but at that point you won't notice). The more verbose syntax is allowed in preparation for future randomisation strategies. //! +//! ### ~radio +//! ### ~radio qualifier -- e.g. `iterable~(~radio)` +//! Sort by musical similarity, starting with a random first song from the iterator. The optional qualifier may be `chroma`, `loudness`, `spectrum`, or `tempo`. When the qualifier is omitted, they are all considered for comparing audio similarity. +//! //! ### advanced bliss_first -- e.g. `iterable~(advanced bliss_first);` //! //! Sort by the distance (similarity) from the first song in the iterator. Songs which are more similar (lower distance) to the first song in the iterator will be placed closer to the first song, while less similar songs will be sorted to the end. This uses the [bliss music analyser](https://github.com/polochon-street/bliss-rs), which is a very slow operation and can cause music playback interruptions for large iterators. This requires the `advanced` feature to be enabled (without the feature enabled this is still valid syntax but doesn't change the order). diff --git a/interpreter/tests/single_line.rs b/interpreter/tests/single_line.rs index 28af02f..f971aec 100644 --- a/interpreter/tests/single_line.rs +++ b/interpreter/tests/single_line.rs @@ -175,22 +175,22 @@ fn execute_emptyfilter_line() -> Result<(), InterpreterError> { #[test] fn execute_fieldfilter_line() -> Result<(), InterpreterError> { execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(date >= 2000)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(.date >= 2000)", false, true, )?; execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(date <= 2020)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(.date <= 2020)", false, true, )?; execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(date == 2016)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(.date == 2016)", false, true, )?; execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(date != 2048)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(.date != 2048)", false, true, ) @@ -199,22 +199,22 @@ fn execute_fieldfilter_line() -> Result<(), InterpreterError> { #[test] fn execute_fieldfiltermaybe_line() -> Result<(), InterpreterError> { execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(date? >= 2000)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(.date? >= 2000)", false, true, )?; execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(date? <= 2020)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(.date? <= 2020)", false, true, )?; execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(date! == 2016)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(.date! == 2016)", false, true, )?; execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(date! != `test`)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(.date! != `test`)", false, true, ) @@ -291,12 +291,12 @@ fn execute_orfilter_line() -> Result<(), InterpreterError> { true, )?; execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(date != 2020 || 5)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(.date != 2020 || 5)", false, true, )?; execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(date != 2020 || 5 || 4 || 12)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(.date != 2020 || 5 || 4 || 12)", false, true, ) @@ -338,22 +338,22 @@ fn execute_emptysort_line() -> Result<(), InterpreterError> { #[test] fn execute_likefilter_line() -> Result<(), InterpreterError> { execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(not_a_field? like `24K Magic`)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(.not_a_field? like `24K Magic`)", true, true, )?; execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(not_a_field! like `24K Magic`)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(.not_a_field! like `24K Magic`)", false, true, )?; execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(album like `24K Magic`)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(.album like `24K Magic`)", false, true, )?; execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(album unlike `24K Magic`)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(.album unlike `24K Magic`)", true, true, ) @@ -362,12 +362,12 @@ fn execute_likefilter_line() -> Result<(), InterpreterError> { #[test] fn execute_fieldsort_line() -> Result<(), InterpreterError> { execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`)~(title)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`)~(.title)", false, true, )?; execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).sort(not_a_field)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).sort(.not_a_field)", false, true, ) @@ -423,16 +423,16 @@ fn execute_resetfn_line() -> Result<(), InterpreterError> { #[test] fn execute_shufflesort_line() -> Result<(), InterpreterError> { execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`)~(random shuffle)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`)~(~random shuffle)", false, true, )?; execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`)~(shuffle)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`)~(~shuffle)", false, true, )?; - execute_single_line("empty()~(shuffle)", true, true) + execute_single_line("empty()~(~shuffle)", true, true) } #[test] @@ -462,18 +462,18 @@ fn execute_unionfn_line() -> Result<(), InterpreterError> { #[test] fn execute_regexfilter_line() -> Result<(), InterpreterError> { execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(title matches `24K\\\\s+Magic`)", // note: quad-escape not required in scripts + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(.title matches `24K\\\\s+Magic`)", // note: quad-escape not required in scripts false, true, )?; execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(artist? matches `Bruno Mars`)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(.artist? matches `Bruno Mars`)", false, true, )?; // regex options execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(artist? matches `bruno mars`, `i`)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(.artist? matches `bruno mars`, `i`)", false, true, ) @@ -789,17 +789,17 @@ fn execute_commentitemop_line() -> Result<(), InterpreterError> { #[test] fn execute_uniquefieldfilter_line() -> Result<(), InterpreterError> { execute_single_line( - "repeat(files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`), 3).(unique title?)", + "repeat(files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`), 3).(unique .title?)", false, true, )?; execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(unique album!)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(unique .album!)", false, true, )?; execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(unique album)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`).(unique .album)", false, true, ) diff --git a/player/src/os_controls.rs b/player/src/os_controls.rs index be7de8c..3a235e9 100644 --- a/player/src/os_controls.rs +++ b/player/src/os_controls.rs @@ -209,18 +209,23 @@ impl SystemControlWrapper { .and_then(|x| x.to_owned().to_str()); let cover_url = if let Some(art) = &cover_art { const DATA_START: usize = 23; + const DATA_PREVIEW: usize = 32; + const DATA_PREVIEW_OFFSET: usize = 128; + let preview_slice_start = (DATA_START+DATA_PREVIEW_OFFSET).clamp(0, art.len()-2); + let preview_slide_end = (DATA_START+DATA_PREVIEW+DATA_PREVIEW_OFFSET).clamp(preview_slice_start, art.len()); let path = format!("{}/muss-cover-{}.jpg", std::env::var("HOME").map(|home| home + "/.cache").unwrap_or_else(|_| "/tmp".to_owned()), - &art[DATA_START..DATA_START+16].replace("/", "")); - let pathbuf = std::path::PathBuf::from(&path); - if !pathbuf.exists() { - base64::decode(&art[DATA_START..]).ok() - .and_then(|decoded| std::fs::File::create(&path).ok().map(|file| (decoded, file))) - .and_then(|(decoded, mut file)| file.write(&decoded).ok()) - .map(|_| path) + &art[preview_slice_start..preview_slide_end].replace("/", "")); + //let pathbuf = std::path::PathBuf::from(&path); + /*if !pathbuf.exists() { + } else { Some(path) - } + }*/ + base64::decode(&art[DATA_START..]).ok() + .and_then(|decoded| std::fs::File::create(&path).ok().map(|file| (decoded, file))) + .and_then(|(decoded, mut file)| file.write(&decoded).ok()) + .map(|_| path) } else { None }; diff --git a/src/help.rs b/src/help.rs index 6300599..314ff1c 100644 --- a/src/help.rs +++ b/src/help.rs @@ -60,15 +60,15 @@ pub const FILTERS: &str = "FILTERS (?filters) Operations to reduce the items in an iterable: iterable.(filter) - field == something - field like something - field unlike something - field matches some_regex - field != something - field >= something - field > something - field <= something - field < something -- e.g. iterable.(title == `Romantic Traffic`) + .field == something + .field like something + .field unlike something + .field matches some_regex + .field != something + .field >= something + .field > something + .field <= something + .field < something -- e.g. iterable.(.title == `Romantic Traffic`) Compare all items, keeping only those that match the condition. Valid field names change depending on what information is available when the Item is populated, but usually title, artist, album, genre, track, filename are valid fields. Optionally, a ? or ! can be added to the end of the field name to skip items whose field is missing/incomparable, or keep all items whose field is missing/incomparable (respectively). start..end -- e.g. iterable.(0..42) @@ -100,13 +100,17 @@ pub const SORTERS: &str = "SORTERS (?sorters) Operations to sort the items in an iterable: iterable~(sorter) OR iterable.sort(sorter) - field -- e.g. iterable~(filename) + .field -- e.g. iterable~(.filename) Sort by an Item field. Valid field names change depending on what information is available when the Item is populated, but usually title, artist, album, genre, track, filename are valid fields. Items with a missing/incomparable fields will be sorted to the end. shuffle random shuffle -- e.g. iterable~(shuffle) Shuffle the songs in the iterator. This is random for up to 2^16 items, and then the randomness degrades (but at that point you won't notice). + ~radio + ~radio qualifier -- e.g. iterable~(~radio) + Sort by musical similarity, starting with a random first song from the iterator. The optional qualifier may be chroma, loudness, spectrum, or tempo. When the qualifier is omitted, they are all considered for comparing audio similarity. + advanced bliss_first -- e.g. iterable~(advanced bliss_first) Sort by the distance (similarity) from the first song in the iterator. Songs which are more similar (lower distance) to the first song in the iterator will be placed closer to the first song, while less similar songs will be sorted to the end. This uses the bliss music analyser, which is a very slow operation and can cause music playback interruptions for large iterators. Requires `advanced` interpreter feature.