Add bliss music sorting (sort by song similarity -- songs which are more similar (smaller distance) come first)
This commit is contained in:
parent
0a6dae930f
commit
41c8c8cbf1
9 changed files with 998 additions and 35 deletions
759
Cargo.lock
generated
759
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -13,8 +13,10 @@ symphonia = { version = "0.4.0", optional = true, features = [
|
|||
dirs = { version = "4.0.0" }
|
||||
regex = { version = "1" }
|
||||
shellexpand = { version = "2.1", optional = true }
|
||||
bliss-audio = { version = "0.4", optional = true }
|
||||
|
||||
[features]
|
||||
default = [ "music_library", "ergonomics" ]
|
||||
default = [ "music_library", "ergonomics", "advanced" ]
|
||||
music_library = [ "symphonia" ] # song metadata parsing and database auto-population
|
||||
ergonomics = ["shellexpand"]
|
||||
ergonomics = ["shellexpand"] # niceties like ~ in pathes
|
||||
advanced = ["bliss-audio"] # advanced language features like bliss playlist generation
|
||||
|
|
|
@ -166,6 +166,7 @@ pub(crate) fn standard_vocab(vocabulary: &mut MpsLanguageDictionary) {
|
|||
// sorters
|
||||
.add(crate::lang::vocabulary::sorters::empty_sort())
|
||||
.add(crate::lang::vocabulary::sorters::field_sort())
|
||||
.add(crate::lang::vocabulary::sorters::bliss_sort())
|
||||
// functions and misc
|
||||
.add(crate::lang::vocabulary::sql_function_factory())
|
||||
.add(crate::lang::vocabulary::simple_sql_function_factory())
|
||||
|
|
|
@ -7,16 +7,18 @@ use crate::lang::utility::{assert_name, assert_token_raw, check_name};
|
|||
use crate::lang::MpsLanguageDictionary;
|
||||
use crate::lang::{BoxedMpsOpFactory, MpsIteratorItem, MpsOp, PseudoOp};
|
||||
use crate::lang::{RuntimeError, SyntaxError};
|
||||
use crate::processing::OpGetter;
|
||||
use crate::tokens::MpsToken;
|
||||
use crate::MpsContext;
|
||||
|
||||
const SORTER_ITEM_CACHE_SIZE: usize = 8;
|
||||
|
||||
pub trait MpsSorter: Clone + Debug + Display {
|
||||
fn sort(
|
||||
fn sort<'a>(
|
||||
&mut self,
|
||||
iterator: &mut dyn MpsOp,
|
||||
item_buf: &mut VecDeque<MpsIteratorItem>,
|
||||
op: &'a mut OpGetter,
|
||||
) -> Result<(), RuntimeError>;
|
||||
}
|
||||
|
||||
|
@ -81,11 +83,16 @@ impl<S: MpsSorter + 'static> Iterator for MpsSortStatement<S> {
|
|||
type Item = MpsIteratorItem;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let pseudo_self = PseudoOp::from_printable(self);
|
||||
let real_op = match self.iterable.try_real() {
|
||||
Ok(op) => op,
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
match self.orderer.sort(real_op.as_mut(), &mut self.item_cache) {
|
||||
match self
|
||||
.orderer
|
||||
.sort(real_op.as_mut(), &mut self.item_cache, &mut move || {
|
||||
pseudo_self.clone()
|
||||
}) {
|
||||
Ok(_) => {}
|
||||
Err(e) => return Some(Err(e)),
|
||||
}
|
||||
|
|
236
mps-interpreter/src/lang/vocabulary/sorters/bliss_sorter.rs
Normal file
236
mps-interpreter/src/lang/vocabulary/sorters/bliss_sorter.rs
Normal file
|
@ -0,0 +1,236 @@
|
|||
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};
|
||||
|
||||
#[cfg(feature = "bliss-audio")]
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[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")]
|
||||
const DEFAULT_ORDER: std::cmp::Ordering = std::cmp::Ordering::Greater;
|
||||
|
||||
#[cfg(feature = "bliss-audio")]
|
||||
#[derive(Debug)]
|
||||
pub struct BlissSorter {
|
||||
up_to: usize,
|
||||
float_map: HashMap<String, f32>,
|
||||
first_song: Option<String>,
|
||||
rx: Option<Receiver<Result<(String, f32), bliss_audio::BlissError>>>,
|
||||
errors: Vec<bliss_audio::BlissError>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "bliss-audio")]
|
||||
impl BlissSorter {
|
||||
fn get_or_wait(&mut self, path: &str) -> Option<f32> {
|
||||
if let Some(distance) = self.float_map.get(path) {
|
||||
Some(*distance)
|
||||
} else {
|
||||
// wait on threads until matching result is found
|
||||
for result in self.rx.as_ref().unwrap() {
|
||||
match result {
|
||||
Ok((key, distance)) => {
|
||||
if path == key {
|
||||
self.float_map.insert(key, distance);
|
||||
return Some(distance);
|
||||
} else {
|
||||
self.float_map.insert(key, distance);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
self.errors.push(e);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn compare_songs(
|
||||
song1: Song,
|
||||
path_2: String,
|
||||
) -> Result<(String, f32), bliss_audio::BlissError> {
|
||||
let song2 = Song::new(&path_2)?;
|
||||
let distance = song1.distance(&song2);
|
||||
Ok((path_2, distance))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "bliss-audio")]
|
||||
impl std::clone::Clone for BlissSorter {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
up_to: self.up_to.clone(),
|
||||
float_map: self.float_map.clone(),
|
||||
first_song: self.first_song.clone(),
|
||||
rx: None,
|
||||
errors: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "bliss-audio")]
|
||||
impl Default for BlissSorter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
up_to: usize::MAX,
|
||||
float_map: HashMap::new(),
|
||||
first_song: None,
|
||||
rx: None,
|
||||
errors: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "bliss-audio")]
|
||||
impl MpsSorter for BlissSorter {
|
||||
fn sort(
|
||||
&mut self,
|
||||
iterator: &mut dyn MpsOp,
|
||||
item_buf: &mut VecDeque<MpsIteratorItem>,
|
||||
op: &mut OpGetter,
|
||||
) -> Result<(), RuntimeError> {
|
||||
let buf_len_old = item_buf.len(); // save buffer length before modifying buffer
|
||||
if item_buf.len() < self.up_to {
|
||||
for item in iterator {
|
||||
item_buf.push_back(item);
|
||||
if item_buf.len() >= self.up_to {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if buf_len_old != item_buf.len() && !item_buf.is_empty() {
|
||||
// when buf_len_old == item_buf.len(), iterator was already complete
|
||||
// no need to sort in that case, since buffer was sorted in last call to sort or buffer never had any items to sort
|
||||
if self.first_song.is_none() {
|
||||
let (tx_chann, rx_chann) = channel();
|
||||
let mut item_paths = Vec::with_capacity(item_buf.len() - 1);
|
||||
for item in item_buf.iter() {
|
||||
if let Ok(item) = item {
|
||||
// build comparison table
|
||||
if let Some(MpsTypePrimitive::String(path)) = item.field("filename") {
|
||||
if self.first_song.is_none() {
|
||||
// find first valid song (ie first item with field "filename")
|
||||
self.first_song = Some(path.to_owned());
|
||||
//self.first_song = Some(Song::new(path).map_err(|e| bliss_err(e, op))?);
|
||||
self.float_map.insert(path.to_owned(), 0.0); // distance to itself should be 0
|
||||
} else {
|
||||
item_paths.push(path.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(first_song_path) = &self.first_song {
|
||||
// spawn threads for processing song distances
|
||||
let path1_clone = first_song_path.to_owned();
|
||||
std::thread::spawn(move || match Song::new(path1_clone) {
|
||||
Err(e) => tx_chann.send(Err(e)).unwrap_or(()),
|
||||
Ok(song1) => {
|
||||
for path2 in item_paths {
|
||||
let result_chann = tx_chann.clone();
|
||||
let song1_clone = song1.clone();
|
||||
std::thread::spawn(move || {
|
||||
result_chann
|
||||
.send(Self::compare_songs(song1_clone, path2))
|
||||
.unwrap_or(());
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
self.rx = Some(rx_chann);
|
||||
// unordered list returned on first call to this function
|
||||
// note that only the first item will be used by sorter,
|
||||
// since the second time this function is called the remaining items are sorted properly
|
||||
}
|
||||
} else if self.first_song.is_some() {
|
||||
// Sort songs on second call to this function
|
||||
self.first_song = None;
|
||||
item_buf.make_contiguous().sort_by(|a, b| {
|
||||
if let Ok(a) = a {
|
||||
if let Some(MpsTypePrimitive::String(a_path)) = a.field("filename") {
|
||||
if let Ok(b) = b {
|
||||
if let Some(MpsTypePrimitive::String(b_path)) = b.field("filename") {
|
||||
if let Some(float_a) = self.get_or_wait(a_path) {
|
||||
if let Some(float_b) = self.get_or_wait(b_path) {
|
||||
println!(
|
||||
"A:{} distance {}, B:{} distance {}",
|
||||
a_path, float_a, b_path, float_b
|
||||
);
|
||||
return float_a
|
||||
.partial_cmp(&float_b)
|
||||
.unwrap_or(DEFAULT_ORDER);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DEFAULT_ORDER
|
||||
});
|
||||
}
|
||||
if self.errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(bliss_err(self.errors.pop().unwrap(), op))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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 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")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BlissSorterFactory;
|
||||
|
||||
impl MpsSorterFactory<BlissSorter> for BlissSorterFactory {
|
||||
fn is_sorter(&self, tokens: &VecDeque<&MpsToken>) -> bool {
|
||||
tokens.len() == 2 && check_name("distance", &tokens[0]) && check_name("bliss", &tokens[1])
|
||||
}
|
||||
|
||||
fn build_sorter(
|
||||
&self,
|
||||
tokens: &mut VecDeque<MpsToken>,
|
||||
_dict: &MpsLanguageDictionary,
|
||||
) -> Result<BlissSorter, SyntaxError> {
|
||||
assert_name("distance", tokens)?;
|
||||
assert_name("bliss", tokens)?;
|
||||
Ok(BlissSorter::default())
|
||||
}
|
||||
}
|
||||
|
||||
pub type BlissSorterStatementFactory = MpsSortStatementFactory<BlissSorter, BlissSorterFactory>;
|
||||
|
||||
#[inline(always)]
|
||||
pub fn bliss_sort() -> BlissSorterStatementFactory {
|
||||
BlissSorterStatementFactory::new(BlissSorterFactory)
|
||||
}
|
|
@ -4,9 +4,10 @@ use std::fmt::{Debug, Display, Error, Formatter};
|
|||
use crate::lang::{MpsIteratorItem, MpsLanguageDictionary, MpsOp};
|
||||
use crate::lang::{MpsSortStatementFactory, MpsSorter, MpsSorterFactory};
|
||||
use crate::lang::{RuntimeError, SyntaxError};
|
||||
use crate::processing::OpGetter;
|
||||
use crate::tokens::MpsToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct EmptySorter;
|
||||
|
||||
impl MpsSorter for EmptySorter {
|
||||
|
@ -14,6 +15,7 @@ impl MpsSorter for EmptySorter {
|
|||
&mut self,
|
||||
iterator: &mut dyn MpsOp,
|
||||
item_buf: &mut VecDeque<MpsIteratorItem>,
|
||||
_op: &mut OpGetter,
|
||||
) -> Result<(), RuntimeError> {
|
||||
if let Some(item) = iterator.next() {
|
||||
item_buf.push_back(item)
|
||||
|
|
|
@ -6,6 +6,7 @@ use crate::lang::utility::assert_token;
|
|||
use crate::lang::{MpsIteratorItem, MpsLanguageDictionary, MpsOp};
|
||||
use crate::lang::{MpsSortStatementFactory, MpsSorter, MpsSorterFactory};
|
||||
use crate::lang::{RuntimeError, SyntaxError};
|
||||
use crate::processing::OpGetter;
|
||||
use crate::tokens::MpsToken;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -20,6 +21,7 @@ impl MpsSorter for FieldSorter {
|
|||
&mut self,
|
||||
iterator: &mut dyn MpsOp,
|
||||
item_buf: &mut VecDeque<MpsIteratorItem>,
|
||||
_op: &mut OpGetter,
|
||||
) -> Result<(), RuntimeError> {
|
||||
let buf_len_old = item_buf.len(); // save buffer length before modifying buffer
|
||||
if item_buf.len() < self.up_to {
|
||||
|
@ -45,7 +47,6 @@ impl MpsSorter for FieldSorter {
|
|||
}
|
||||
self.default_order
|
||||
});
|
||||
println!("Field-sorted item_buf: {:?}", item_buf);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -80,7 +81,7 @@ impl MpsSorterFactory<FieldSorter> for FieldSorterFactory {
|
|||
Ok(FieldSorter {
|
||||
field_name: name,
|
||||
up_to: usize::MAX,
|
||||
default_order: Ordering::Equal,
|
||||
default_order: Ordering::Greater,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
mod bliss_sorter;
|
||||
mod empty_sorter;
|
||||
mod field_sorter;
|
||||
|
||||
pub use bliss_sorter::{bliss_sort, BlissSorter, BlissSorterFactory, BlissSorterStatementFactory};
|
||||
pub use empty_sorter::{empty_sort, EmptySorter, EmptySorterFactory, EmptySorterStatementFactory};
|
||||
pub use field_sorter::{field_sort, FieldSorter, FieldSorterFactory, FieldSorterStatementFactory};
|
||||
|
|
|
@ -358,3 +358,12 @@ fn execute_fieldsort_line() -> Result<(), Box<dyn MpsLanguageError>> {
|
|||
true,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execute_blisssort_line() -> Result<(), Box<dyn MpsLanguageError>> {
|
||||
execute_single_line(
|
||||
"files(`~/Music/MusicFlac/Bruno Mars/24K Magic/`)~(distance bliss)",
|
||||
false,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue