diff --git a/mps-interpreter/README.md b/mps-interpreter/README.md index ea7b371..9ea73ec 100644 --- a/mps-interpreter/README.md +++ b/mps-interpreter/README.md @@ -137,9 +137,13 @@ Operations to sort the items in an iterable: iterable~(sorter) OR iterable.sort( Sort by a MpsItem field. Valid field names change depending on what information is available when the MpsItem 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. -#### advanced bliss -- e.g. iterable~(advanced bliss) +#### advanced bliss_first -- e.g. iterable~(advanced bliss_first) -Sort by the distance (similarity) between songs. 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). +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). + +#### advanced bliss_next -- e.g. iterable~(advanced bliss_next) + +Sort by the distance (similarity) between the last played song in the iterator. Similar to bliss_first. The song which is the most similar (lower distance) to the previous song in the iterator will be placed next to it, then the process is repeated. 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). License: LGPL-2.1-only OR GPL-2.0-or-later diff --git a/mps-interpreter/src/interpretor.rs b/mps-interpreter/src/interpretor.rs index 182d675..adf2c8e 100644 --- a/mps-interpreter/src/interpretor.rs +++ b/mps-interpreter/src/interpretor.rs @@ -167,6 +167,7 @@ pub(crate) fn standard_vocab(vocabulary: &mut MpsLanguageDictionary) { .add(crate::lang::vocabulary::sorters::empty_sort()) .add(crate::lang::vocabulary::sorters::field_sort()) .add(crate::lang::vocabulary::sorters::bliss_sort()) + .add(crate::lang::vocabulary::sorters::bliss_next_sort()) // functions and misc .add(crate::lang::vocabulary::sql_function_factory()) .add(crate::lang::vocabulary::simple_sql_function_factory()) diff --git a/mps-interpreter/src/item.rs b/mps-interpreter/src/item.rs index dfcd3be..4a60bbc 100644 --- a/mps-interpreter/src/item.rs +++ b/mps-interpreter/src/item.rs @@ -16,7 +16,7 @@ impl MpsItem { Self::default() } - pub fn field(&self, name: &str) -> Option<&MpsTypePrimitive> { + pub fn field(&self, name: &str) -> Option<&'_ MpsTypePrimitive> { self.fields.get(name) } diff --git a/mps-interpreter/src/lang/sorter.rs b/mps-interpreter/src/lang/sorter.rs index c479ed6..c32a4d3 100644 --- a/mps-interpreter/src/lang/sorter.rs +++ b/mps-interpreter/src/lang/sorter.rs @@ -20,6 +20,8 @@ pub trait MpsSorter: Clone + Debug + Display { item_buf: &mut VecDeque, op: &'a mut OpGetter, ) -> Result<(), RuntimeError>; + + fn reset(&mut self) {} } pub trait MpsSorterFactory { @@ -75,6 +77,7 @@ impl MpsOp for MpsSortStatement { fn reset(&mut self) -> Result<(), RuntimeError> { self.item_cache.clear(); + self.orderer.reset(); self.iterable.try_real()?.reset() } } diff --git a/mps-interpreter/src/lang/vocabulary/sorters/bliss_next_sorter.rs b/mps-interpreter/src/lang/vocabulary/sorters/bliss_next_sorter.rs new file mode 100644 index 0000000..4d52f46 --- /dev/null +++ b/mps-interpreter/src/lang/vocabulary/sorters/bliss_next_sorter.rs @@ -0,0 +1,227 @@ +use std::collections::VecDeque; +#[cfg(feature = "bliss-audio")] +use std::fmt::{Debug, Display, Error, Formatter}; +#[cfg(feature = "bliss-audio")] +use std::sync::mpsc::{channel, Receiver, Sender}; + +#[cfg(feature = "bliss-audio")] +use bliss_audio::Song; + +use crate::lang::utility::{assert_name, check_name}; +use crate::lang::SyntaxError; +#[cfg(feature = "bliss-audio")] +use crate::lang::{MpsIteratorItem, MpsOp, MpsSorter, MpsTypePrimitive, RuntimeError}; +use crate::lang::{MpsLanguageDictionary, MpsSortStatementFactory, MpsSorterFactory}; +#[cfg(feature = "bliss-audio")] +use crate::processing::OpGetter; +use crate::tokens::MpsToken; +#[cfg(feature = "bliss-audio")] +use crate::MpsItem; + +#[cfg(feature = "bliss-audio")] +#[derive(Debug)] +pub struct BlissNextSorter { + up_to: usize, + rx: Option>>>, + algorithm_done: bool, +} + +#[cfg(feature = "bliss-audio")] +impl BlissNextSorter { + fn get_maybe(&mut self, op: &mut OpGetter) -> Option { + if self.algorithm_done { + None + } else { + if let Ok(Some(item)) = self.rx.as_ref().unwrap().recv() { + Some(item.map_err(|e| bliss_err(e, op))) + } else { + self.algorithm_done = true; + None + } + } + } + + fn algorithm(mut items: VecDeque, results: Sender>>) { + let mut song_cache: Option<(Song, String)> = None; + let items_len = items.len(); + for i in 0..items_len { + let item = items.pop_front().unwrap(); + if let Some(MpsTypePrimitive::String(path)) = item.field("filename") { + if let Err(_) = results.send(Some(Ok(item.clone()))) {break;} + if i+2 < items_len { + let target_song = if let Some((_, ref cached_filename)) = song_cache { + if cached_filename == path { + Ok(song_cache.take().unwrap().0) + } else { + Song::new(path) + } + } else { + Song::new(path) + }; + let target_song = match target_song { + Ok(x) => x, + Err(e) => { + results.send(Some(Err(e))).unwrap_or(()); + break; + } + }; + match Self::find_best(&items, target_song) { + Err(e) => { + results.send(Some(Err(e))).unwrap_or(()); + break; + }, + Ok((next_song, index)) => { + if let Some(next_song) = next_song { + if index != 0 { + items.swap(0, index); + } + song_cache = Some((next_song, path.to_owned())); + } else {break;} + } + } + } + } + } + results.send(None).unwrap_or(()); + } + + fn find_best(items: &VecDeque, target: Song) -> Result<(Option, usize), bliss_audio::BlissError> { + let mut best = None; + let mut best_index = 0; + let mut best_distance = f32::MAX; + let (tx, rx) = channel(); + let mut threads_spawned = 0; + for i in 0..items.len() { + if let Some(MpsTypePrimitive::String(path)) = items[i].field("filename") { + let result_chann = tx.clone(); + let target_clone = target.clone(); + let path_clone = path.to_owned(); + std::thread::spawn(move || { + match Song::new(path_clone) { + Err(e) => result_chann + .send(Err(e)) + .unwrap_or(()), + Ok(song) => result_chann + .send(Ok((i, target_clone.distance(&song), song))) + .unwrap_or(()), + } + }); + threads_spawned += 1; + } + } + for _ in 0..threads_spawned { + if let Ok(result) = rx.recv() { + let (index, distance, song) = result?; + if distance < best_distance { + best = Some(song); + best_index = index; + best_distance = distance; + } + } else {break;} + } + Ok((best, best_index)) + } +} + +#[cfg(feature = "bliss-audio")] +impl std::clone::Clone for BlissNextSorter { + fn clone(&self) -> Self { + Self { + up_to: self.up_to.clone(), + rx: None, + algorithm_done: self.algorithm_done, + } + } +} + +#[cfg(feature = "bliss-audio")] +impl Default for BlissNextSorter { + fn default() -> Self { + Self { + up_to: usize::MAX, + rx: None, + algorithm_done: false, + } + } +} + +#[cfg(feature = "bliss-audio")] +impl MpsSorter for BlissNextSorter { + fn sort( + &mut self, + iterator: &mut dyn MpsOp, + item_buf: &mut VecDeque, + op: &mut OpGetter, + ) -> Result<(), RuntimeError> { + if self.rx.is_none() { + // first run + let mut items = VecDeque::new(); + for item in iterator { + match item { + Ok(item) => items.push_back(item), + Err(e) => item_buf.push_back(Err(e)), + } + if items.len() + item_buf.len() >= self.up_to { + break; + } + } + // start algorithm + let (tx, rx) = channel(); + std::thread::spawn(move || Self::algorithm(items, tx)); + self.rx = Some(rx); + } + if let Some(item) = self.get_maybe(op) { + item_buf.push_back(item); + } + Ok(()) + } + + fn reset(&mut self) { + self.algorithm_done = false; + self.rx = None; + } +} + +#[cfg(feature = "bliss-audio")] +fn bliss_err(error: D, op: &mut OpGetter) -> RuntimeError { + RuntimeError { + line: 0, + op: op(), + msg: format!("Bliss error: {}", error), + } +} + +#[cfg(not(feature = "bliss-audio"))] +pub type BlissNextSorter = crate::lang::vocabulary::sorters::EmptySorter; + +#[cfg(feature = "bliss-audio")] +impl Display for BlissNextSorter { + fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { + write!(f, "advanced bliss_next") + } +} + +pub struct BlissNextSorterFactory; + +impl MpsSorterFactory for BlissNextSorterFactory { + fn is_sorter(&self, tokens: &VecDeque<&MpsToken>) -> bool { + tokens.len() == 2 && check_name("advanced", &tokens[0]) && check_name("bliss_next", &tokens[1]) + } + + fn build_sorter( + &self, + tokens: &mut VecDeque, + _dict: &MpsLanguageDictionary, + ) -> Result { + assert_name("advanced", tokens)?; + assert_name("bliss_next", tokens)?; + Ok(BlissNextSorter::default()) + } +} + +pub type BlissNextSorterStatementFactory = MpsSortStatementFactory; + +#[inline(always)] +pub fn bliss_next_sort() -> BlissNextSorterStatementFactory { + BlissNextSorterStatementFactory::new(BlissNextSorterFactory) +} diff --git a/mps-interpreter/src/lang/vocabulary/sorters/bliss_sorter.rs b/mps-interpreter/src/lang/vocabulary/sorters/bliss_sorter.rs index cda895f..3a84626 100644 --- a/mps-interpreter/src/lang/vocabulary/sorters/bliss_sorter.rs +++ b/mps-interpreter/src/lang/vocabulary/sorters/bliss_sorter.rs @@ -202,7 +202,7 @@ pub type BlissSorter = crate::lang::vocabulary::sorters::EmptySorter; #[cfg(feature = "bliss-audio")] impl Display for BlissSorter { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { - write!(f, "distance bliss") + write!(f, "advanced bliss_first") } } @@ -210,7 +210,7 @@ pub struct BlissSorterFactory; impl MpsSorterFactory for BlissSorterFactory { fn is_sorter(&self, tokens: &VecDeque<&MpsToken>) -> bool { - tokens.len() == 2 && check_name("advanced", &tokens[0]) && check_name("bliss", &tokens[1]) + tokens.len() == 2 && check_name("advanced", &tokens[0]) && check_name("bliss_first", &tokens[1]) } fn build_sorter( @@ -219,7 +219,7 @@ impl MpsSorterFactory for BlissSorterFactory { _dict: &MpsLanguageDictionary, ) -> Result { assert_name("advanced", tokens)?; - assert_name("bliss", tokens)?; + assert_name("bliss_first", tokens)?; Ok(BlissSorter::default()) } } diff --git a/mps-interpreter/src/lang/vocabulary/sorters/mod.rs b/mps-interpreter/src/lang/vocabulary/sorters/mod.rs index dd90e5b..5267d02 100644 --- a/mps-interpreter/src/lang/vocabulary/sorters/mod.rs +++ b/mps-interpreter/src/lang/vocabulary/sorters/mod.rs @@ -1,7 +1,9 @@ mod bliss_sorter; +mod bliss_next_sorter; mod empty_sorter; mod field_sorter; pub use bliss_sorter::{bliss_sort, BlissSorter, BlissSorterFactory, BlissSorterStatementFactory}; +pub use bliss_next_sorter::{bliss_next_sort, BlissNextSorter, BlissNextSorterFactory, BlissNextSorterStatementFactory}; pub use empty_sorter::{empty_sort, EmptySorter, EmptySorterFactory, EmptySorterStatementFactory}; pub use field_sorter::{field_sort, FieldSorter, FieldSorterFactory, FieldSorterStatementFactory}; diff --git a/mps-interpreter/src/lib.rs b/mps-interpreter/src/lib.rs index ba828b2..1abaa70 100644 --- a/mps-interpreter/src/lib.rs +++ b/mps-interpreter/src/lib.rs @@ -135,9 +135,13 @@ //! //! Sort by a MpsItem field. Valid field names change depending on what information is available when the MpsItem 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. //! -//! ### advanced bliss -- e.g. iterable~(advanced bliss) +//! ### advanced bliss_first -- e.g. iterable~(advanced bliss_first) //! -//! Sort by the distance (similarity) between songs. 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). +//! 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). +//! +//! ### advanced bliss_next -- e.g. iterable~(advanced bliss_next) +//! +//! Sort by the distance (similarity) between the last played song in the iterator. Similar to bliss_first. The song which is the most similar (lower distance) to the previous song in the iterator will be placed next to it, then the process is repeated. 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). //! mod context; diff --git a/mps-interpreter/src/processing/variables.rs b/mps-interpreter/src/processing/variables.rs index fe1ee7b..4a14e3c 100644 --- a/mps-interpreter/src/processing/variables.rs +++ b/mps-interpreter/src/processing/variables.rs @@ -24,7 +24,7 @@ impl Display for MpsType { } pub trait MpsVariableStorer: Debug { - fn get(&self, name: &str, op: &mut OpGetter) -> Result<&MpsType, RuntimeError> { + fn get(&self, name: &str, op: &mut OpGetter) -> Result<&'_ MpsType, RuntimeError> { match self.get_opt(name) { Some(item) => Ok(item), None => Err(RuntimeError { @@ -35,9 +35,9 @@ pub trait MpsVariableStorer: Debug { } } - fn get_opt(&self, name: &str) -> Option<&MpsType>; + fn get_opt(&self, name: &str) -> Option<&'_ MpsType>; - fn get_mut(&mut self, name: &str, op: &mut OpGetter) -> Result<&mut MpsType, RuntimeError> { + fn get_mut(&mut self, name: &str, op: &mut OpGetter) -> Result<&'_ mut MpsType, RuntimeError> { match self.get_mut_opt(name) { Some(item) => Ok(item), None => Err(RuntimeError { @@ -48,7 +48,7 @@ pub trait MpsVariableStorer: Debug { } } - fn get_mut_opt(&mut self, name: &str) -> Option<&mut MpsType>; + fn get_mut_opt(&mut self, name: &str) -> Option<&'_ mut MpsType>; fn assign(&mut self, name: &str, value: MpsType, op: &mut OpGetter) -> Result<(), RuntimeError>; diff --git a/mps-interpreter/tests/single_line.rs b/mps-interpreter/tests/single_line.rs index d17e50f..abf4c3c 100644 --- a/mps-interpreter/tests/single_line.rs +++ b/mps-interpreter/tests/single_line.rs @@ -360,9 +360,18 @@ fn execute_fieldsort_line() -> Result<(), Box> { } #[test] -fn execute_blisssort_line() -> Result<(), Box> { +fn execute_blissfirstsort_line() -> Result<(), Box> { execute_single_line( - "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`)~(advanced bliss)", + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`)~(advanced bliss_first)", + false, + true, + ) +} + +#[test] +fn execute_blissnextsort_line() -> Result<(), Box> { + execute_single_line( + "files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`)~(advanced bliss_next)", false, true, ) diff --git a/src/help.rs b/src/help.rs index 451e465..72f34af 100644 --- a/src/help.rs +++ b/src/help.rs @@ -74,5 +74,8 @@ Operations to sort the items in an iterable: iterable~(sorter) OR iterable.sort( field -- e.g. iterable~(filename) Sort by a MpsItem field. Valid field names change depending on what information is available when the MpsItem 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. - advanced bliss -- e.g. iterable~(advanced bliss) - Sort by the distance (similarity) between songs. 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` mps-interpreter feature."; + 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` mps-interpreter feature. + + advanced bliss_next -- e.g. iterable~(advanced bliss_next) + Sort by the distance (similarity) between the last played song in the iterator. Similar to bliss_first. 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` mps-interpreter feature.";