Add custom sorting for playlists
This commit is contained in:
parent
82d229346b
commit
e9e63f961c
4 changed files with 257 additions and 20 deletions
|
@ -1,5 +1,8 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## bliss 0.3.5
|
||||||
|
* Add custom sorting methods for playlist-making.
|
||||||
|
|
||||||
## bliss 0.3.4
|
## bliss 0.3.4
|
||||||
* Bump ffmpeg's version to avoid building ffmpeg when building bliss.
|
* Bump ffmpeg's version to avoid building ffmpeg when building bliss.
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "bliss-audio"
|
name = "bliss-audio"
|
||||||
version = "0.3.4"
|
version = "0.3.5"
|
||||||
authors = ["Polochon-street <polochonstreet@gmx.fr>"]
|
authors = ["Polochon-street <polochonstreet@gmx.fr>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
154
src/distance.rs
154
src/distance.rs
|
@ -7,10 +7,12 @@
|
||||||
//! They will yield different styles of playlists, so don't hesitate to
|
//! They will yield different styles of playlists, so don't hesitate to
|
||||||
//! experiment with them if the default (euclidean distance for now) doesn't
|
//! experiment with them if the default (euclidean distance for now) doesn't
|
||||||
//! suit you.
|
//! suit you.
|
||||||
use crate::NUMBER_FEATURES;
|
|
||||||
#[cfg(doc)]
|
#[cfg(doc)]
|
||||||
use crate::{Library, Song};
|
use crate::Library;
|
||||||
|
use crate::Song;
|
||||||
|
use crate::NUMBER_FEATURES;
|
||||||
use ndarray::{Array, Array1};
|
use ndarray::{Array, Array1};
|
||||||
|
use noisy_float::prelude::*;
|
||||||
|
|
||||||
/// Convenience trait for user-defined distance metrics.
|
/// Convenience trait for user-defined distance metrics.
|
||||||
pub trait DistanceMetric: Fn(&Array1<f32>, &Array1<f32>) -> f32 {}
|
pub trait DistanceMetric: Fn(&Array1<f32>, &Array1<f32>) -> f32 {}
|
||||||
|
@ -36,10 +38,158 @@ pub fn cosine_distance(a: &Array1<f32>, b: &Array1<f32>) -> f32 {
|
||||||
1. - similarity
|
1. - similarity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sort `songs` in place by putting songs close to `first_song` first
|
||||||
|
/// using the `distance` metric. Deduplicate identical songs.
|
||||||
|
pub fn closest_to_first_song(
|
||||||
|
first_song: &Song,
|
||||||
|
songs: &mut Vec<Song>,
|
||||||
|
distance: impl DistanceMetric,
|
||||||
|
) {
|
||||||
|
songs.sort_by_cached_key(|song| n32(first_song.custom_distance(song, &distance)));
|
||||||
|
songs.dedup_by_key(|song| n32(first_song.custom_distance(song, &distance)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sort `songs` in place using the `distance` metric and ordering by
|
||||||
|
/// the smallest distance between each song. Deduplicate identical songs.
|
||||||
|
///
|
||||||
|
/// If the generated playlist is `[song1, song2, song3, song4]`, it means
|
||||||
|
/// song2 is closest to song1, song3 is closest to song2, and song4 is closest
|
||||||
|
/// to song3.
|
||||||
|
pub fn song_to_song(first_song: &Song, songs: &mut Vec<Song>, distance: impl DistanceMetric) {
|
||||||
|
let mut new_songs = vec![first_song.to_owned()];
|
||||||
|
let mut song = first_song.to_owned();
|
||||||
|
loop {
|
||||||
|
if songs.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
songs
|
||||||
|
.retain(|s| n32(song.custom_distance(s, &distance)) != 0.);
|
||||||
|
songs.sort_by_key(|s| n32(song.custom_distance(s, &distance)));
|
||||||
|
song = songs.remove(0);
|
||||||
|
new_songs.push(song.to_owned());
|
||||||
|
}
|
||||||
|
*songs = new_songs;
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::Analysis;
|
||||||
use ndarray::arr1;
|
use ndarray::arr1;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_song_to_song() {
|
||||||
|
let first_song = Song {
|
||||||
|
path: Path::new("path-to-first").to_path_buf(),
|
||||||
|
analysis: Analysis::new([
|
||||||
|
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let first_song_dupe = Song {
|
||||||
|
path: Path::new("path-to-dupe").to_path_buf(),
|
||||||
|
analysis: Analysis::new([
|
||||||
|
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let second_song = Song {
|
||||||
|
path: Path::new("path-to-second").to_path_buf(),
|
||||||
|
analysis: Analysis::new([
|
||||||
|
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 1.9, 1., 1., 1.,
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let third_song = Song {
|
||||||
|
path: Path::new("path-to-third").to_path_buf(),
|
||||||
|
analysis: Analysis::new([
|
||||||
|
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.5, 1., 1., 1.,
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let fourth_song = Song {
|
||||||
|
path: Path::new("path-to-fourth").to_path_buf(),
|
||||||
|
analysis: Analysis::new([
|
||||||
|
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 0., 1., 1., 1.,
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut songs = vec![
|
||||||
|
first_song.to_owned(),
|
||||||
|
first_song_dupe.to_owned(),
|
||||||
|
second_song.to_owned(),
|
||||||
|
third_song.to_owned(),
|
||||||
|
fourth_song.to_owned(),
|
||||||
|
];
|
||||||
|
song_to_song(&first_song, &mut songs, euclidean_distance);
|
||||||
|
assert_eq!(
|
||||||
|
songs,
|
||||||
|
vec![first_song, second_song, third_song, fourth_song],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_sort_closest_to_first_song() {
|
||||||
|
let first_song = Song {
|
||||||
|
path: Path::new("path-to-first").to_path_buf(),
|
||||||
|
analysis: Analysis::new([
|
||||||
|
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let first_song_dupe = Song {
|
||||||
|
path: Path::new("path-to-dupe").to_path_buf(),
|
||||||
|
analysis: Analysis::new([
|
||||||
|
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.,
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let second_song = Song {
|
||||||
|
path: Path::new("path-to-second").to_path_buf(),
|
||||||
|
analysis: Analysis::new([
|
||||||
|
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 1.9, 1., 1., 1.,
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let third_song = Song {
|
||||||
|
path: Path::new("path-to-third").to_path_buf(),
|
||||||
|
analysis: Analysis::new([
|
||||||
|
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.5, 1., 1., 1.,
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let fourth_song = Song {
|
||||||
|
path: Path::new("path-to-fourth").to_path_buf(),
|
||||||
|
analysis: Analysis::new([
|
||||||
|
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 0., 1., 1., 1.,
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let fifth_song = Song {
|
||||||
|
path: Path::new("path-to-fifth").to_path_buf(),
|
||||||
|
analysis: Analysis::new([
|
||||||
|
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 0., 1., 1., 1.,
|
||||||
|
]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut songs = vec![
|
||||||
|
first_song.to_owned(),
|
||||||
|
first_song_dupe.to_owned(),
|
||||||
|
second_song.to_owned(),
|
||||||
|
third_song.to_owned(),
|
||||||
|
fourth_song.to_owned(),
|
||||||
|
fifth_song.to_owned(),
|
||||||
|
];
|
||||||
|
closest_to_first_song(&first_song, &mut songs, euclidean_distance);
|
||||||
|
assert_eq!(
|
||||||
|
songs,
|
||||||
|
vec![first_song, second_song, fourth_song, third_song],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_euclidean_distance() {
|
fn test_euclidean_distance() {
|
||||||
|
|
118
src/library.rs
118
src/library.rs
|
@ -5,10 +5,9 @@
|
||||||
//! MPD](https://github.com/Polochon-street/blissify-rs) could also be useful.
|
//! MPD](https://github.com/Polochon-street/blissify-rs) could also be useful.
|
||||||
#[cfg(doc)]
|
#[cfg(doc)]
|
||||||
use crate::distance;
|
use crate::distance;
|
||||||
use crate::distance::DistanceMetric;
|
use crate::distance::{closest_to_first_song, DistanceMetric, euclidean_distance};
|
||||||
use crate::{BlissError, BlissResult, Song};
|
use crate::{BlissError, BlissResult, Song};
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
use noisy_float::prelude::*;
|
|
||||||
use std::sync::mpsc;
|
use std::sync::mpsc;
|
||||||
use std::sync::mpsc::{Receiver, Sender};
|
use std::sync::mpsc::{Receiver, Sender};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
@ -30,7 +29,8 @@ pub trait Library {
|
||||||
/// once.
|
/// once.
|
||||||
fn get_stored_songs(&self) -> BlissResult<Vec<Song>>;
|
fn get_stored_songs(&self) -> BlissResult<Vec<Song>>;
|
||||||
|
|
||||||
/// Return a list of songs that are similar to ``first_song``.
|
/// Return a list of `playlist_length` songs that are similar
|
||||||
|
/// to ``first_song``, deduplicating identical songs.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
|
@ -40,21 +40,22 @@ pub trait Library {
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
///
|
///
|
||||||
/// A vector of `playlist_length` Songs, including `first_song`, that you
|
/// A vector of `playlist_length` songs, including `first_song`, that you
|
||||||
/// most likely want to plug in your audio player by using something like
|
/// most likely want to plug in your audio player by using something like
|
||||||
/// `ret.map(|song| song.path.to_owned()).collect::<Vec<String>>()`.
|
/// `ret.map(|song| song.path.to_owned()).collect::<Vec<String>>()`.
|
||||||
|
// TODO return an iterator and not a Vec
|
||||||
fn playlist_from_song(
|
fn playlist_from_song(
|
||||||
&self,
|
&self,
|
||||||
first_song: Song,
|
first_song: Song,
|
||||||
playlist_length: usize,
|
playlist_length: usize,
|
||||||
) -> BlissResult<Vec<Song>> {
|
) -> BlissResult<Vec<Song>> {
|
||||||
let mut songs = self.get_stored_songs()?;
|
let playlist = self.playlist_from_song_custom(
|
||||||
songs.sort_by_cached_key(|song| n32(first_song.distance(&song)));
|
first_song,
|
||||||
|
playlist_length,
|
||||||
|
euclidean_distance,
|
||||||
|
closest_to_first_song,
|
||||||
|
)?;
|
||||||
|
|
||||||
let playlist = songs
|
|
||||||
.into_iter()
|
|
||||||
.take(playlist_length)
|
|
||||||
.collect::<Vec<Song>>();
|
|
||||||
debug!(
|
debug!(
|
||||||
"Playlist created: {}",
|
"Playlist created: {}",
|
||||||
playlist
|
playlist
|
||||||
|
@ -67,7 +68,7 @@ pub trait Library {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a list of songs that are similar to ``first_song``, using a
|
/// Return a list of songs that are similar to ``first_song``, using a
|
||||||
/// custom distance metric.
|
/// custom distance metric and deduplicating indentical songs.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
|
@ -98,13 +99,13 @@ pub trait Library {
|
||||||
playlist_length: usize,
|
playlist_length: usize,
|
||||||
distance: impl DistanceMetric,
|
distance: impl DistanceMetric,
|
||||||
) -> BlissResult<Vec<Song>> {
|
) -> BlissResult<Vec<Song>> {
|
||||||
let mut songs = self.get_stored_songs()?;
|
let playlist = self.playlist_from_song_custom(
|
||||||
songs.sort_by_cached_key(|song| n32(first_song.custom_distance(&song, &distance)));
|
first_song,
|
||||||
|
playlist_length,
|
||||||
|
distance,
|
||||||
|
closest_to_first_song,
|
||||||
|
)?;
|
||||||
|
|
||||||
let playlist = songs
|
|
||||||
.into_iter()
|
|
||||||
.take(playlist_length)
|
|
||||||
.collect::<Vec<Song>>();
|
|
||||||
debug!(
|
debug!(
|
||||||
"Playlist created: {}",
|
"Playlist created: {}",
|
||||||
playlist
|
playlist
|
||||||
|
@ -116,6 +117,44 @@ pub trait Library {
|
||||||
Ok(playlist)
|
Ok(playlist)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return a playlist of songs, starting with `first_song`, sorted using
|
||||||
|
/// the custom `sort` function, and the custom `distance` metric.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `first_song` - The song the playlist will be built from.
|
||||||
|
/// * `playlist_length` - The playlist length. If there are not enough
|
||||||
|
/// songs in the library, it will be truncated to the size of the library.
|
||||||
|
/// * `distance` - a user-supplied valid distance metric, either taken
|
||||||
|
/// from the [distance](distance) module, or made from scratch.
|
||||||
|
/// * `sort` - a user-supplied sorting function that uses the `distance`
|
||||||
|
/// metric, either taken from the [distance](module), or made from
|
||||||
|
/// scratch.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// A vector of `playlist_length` Songs, including `first_song`, that you
|
||||||
|
/// most likely want to plug in your audio player by using something like
|
||||||
|
/// `ret.map(|song| song.path.to_owned()).collect::<Vec<String>>()`.
|
||||||
|
fn playlist_from_song_custom<F, G>(
|
||||||
|
&self,
|
||||||
|
first_song: Song,
|
||||||
|
playlist_length: usize,
|
||||||
|
distance: G,
|
||||||
|
mut sort: F,
|
||||||
|
) -> BlissResult<Vec<Song>>
|
||||||
|
where
|
||||||
|
F: FnMut(&Song, &mut Vec<Song>, G),
|
||||||
|
G: DistanceMetric,
|
||||||
|
{
|
||||||
|
let mut songs = self.get_stored_songs()?;
|
||||||
|
sort(&first_song, &mut songs, distance);
|
||||||
|
Ok(songs
|
||||||
|
.into_iter()
|
||||||
|
.take(playlist_length)
|
||||||
|
.collect::<Vec<Song>>())
|
||||||
|
}
|
||||||
|
|
||||||
/// Analyze and store songs in `paths`, using `store_song` and
|
/// Analyze and store songs in `paths`, using `store_song` and
|
||||||
/// `store_error_song` implementations.
|
/// `store_error_song` implementations.
|
||||||
///
|
///
|
||||||
|
@ -595,4 +634,49 @@ mod test {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn custom_sort(_: &Song, songs: &mut Vec<Song>, _: impl DistanceMetric) {
|
||||||
|
songs.sort_by_key(|song| song.path.to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_playlist_from_song_custom() {
|
||||||
|
let mut test_library = TestLibrary::default();
|
||||||
|
let first_song = Song {
|
||||||
|
path: Path::new("path-to-first").to_path_buf(),
|
||||||
|
analysis: Analysis::new([0.; 20]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let second_song = Song {
|
||||||
|
path: Path::new("path-to-second").to_path_buf(),
|
||||||
|
analysis: Analysis::new([0.1; 20]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let third_song = Song {
|
||||||
|
path: Path::new("path-to-third").to_path_buf(),
|
||||||
|
analysis: Analysis::new([10.; 20]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let fourth_song = Song {
|
||||||
|
path: Path::new("path-to-fourth").to_path_buf(),
|
||||||
|
analysis: Analysis::new([20.; 20]),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
test_library.internal_storage = vec![
|
||||||
|
first_song.to_owned(),
|
||||||
|
fourth_song.to_owned(),
|
||||||
|
third_song.to_owned(),
|
||||||
|
second_song.to_owned(),
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
vec![first_song.to_owned(), fourth_song, second_song],
|
||||||
|
test_library
|
||||||
|
.playlist_from_song_custom(first_song, 3, custom_distance, custom_sort)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue