Add bliss_next sorting algorithm, rename bliss sort to bliss_first

This commit is contained in:
NGnius (Graham) 2022-01-27 19:55:43 -05:00
parent bd3d1465df
commit bde7ead3b6
11 changed files with 269 additions and 16 deletions

View file

@ -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

View file

@ -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())

View file

@ -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)
}

View file

@ -20,6 +20,8 @@ pub trait MpsSorter: Clone + Debug + Display {
item_buf: &mut VecDeque<MpsIteratorItem>,
op: &'a mut OpGetter,
) -> Result<(), RuntimeError>;
fn reset(&mut self) {}
}
pub trait MpsSorterFactory<S: MpsSorter + 'static> {
@ -75,6 +77,7 @@ impl<S: MpsSorter + 'static> MpsOp for MpsSortStatement<S> {
fn reset(&mut self) -> Result<(), RuntimeError> {
self.item_cache.clear();
self.orderer.reset();
self.iterable.try_real()?.reset()
}
}

View file

@ -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<Receiver<Option<Result<MpsItem, bliss_audio::BlissError>>>>,
algorithm_done: bool,
}
#[cfg(feature = "bliss-audio")]
impl BlissNextSorter {
fn get_maybe(&mut self, op: &mut OpGetter) -> Option<MpsIteratorItem> {
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<MpsItem>, results: Sender<Option<Result<MpsItem, bliss_audio::BlissError>>>) {
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<MpsItem>, target: Song) -> Result<(Option<Song>, 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<MpsIteratorItem>,
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<D: Display>(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<BlissNextSorter> 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<MpsToken>,
_dict: &MpsLanguageDictionary,
) -> Result<BlissNextSorter, SyntaxError> {
assert_name("advanced", tokens)?;
assert_name("bliss_next", tokens)?;
Ok(BlissNextSorter::default())
}
}
pub type BlissNextSorterStatementFactory = MpsSortStatementFactory<BlissNextSorter, BlissNextSorterFactory>;
#[inline(always)]
pub fn bliss_next_sort() -> BlissNextSorterStatementFactory {
BlissNextSorterStatementFactory::new(BlissNextSorterFactory)
}

View file

@ -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<BlissSorter> 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<BlissSorter> for BlissSorterFactory {
_dict: &MpsLanguageDictionary,
) -> Result<BlissSorter, SyntaxError> {
assert_name("advanced", tokens)?;
assert_name("bliss", tokens)?;
assert_name("bliss_first", tokens)?;
Ok(BlissSorter::default())
}
}

View file

@ -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};

View file

@ -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;

View file

@ -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>;

View file

@ -360,9 +360,18 @@ fn execute_fieldsort_line() -> Result<(), Box<dyn MpsLanguageError>> {
}
#[test]
fn execute_blisssort_line() -> Result<(), Box<dyn MpsLanguageError>> {
fn execute_blissfirstsort_line() -> Result<(), Box<dyn MpsLanguageError>> {
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<dyn MpsLanguageError>> {
execute_single_line(
"files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`)~(advanced bliss_next)",
false,
true,
)

View file

@ -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.";