985 lines
34 KiB
Rust
985 lines
34 KiB
Rust
//! Module containing various functions to build playlists, as well as various
|
||
//! distance metrics.
|
||
//!
|
||
//! All of the distance functions are intended to be used with the
|
||
//! [custom_distance](Song::custom_distance) method, or with
|
||
//!
|
||
//! They will yield different styles of playlists, so don't hesitate to
|
||
//! experiment with them if the default (euclidean distance for now) doesn't
|
||
//! suit you.
|
||
// TODO on the `by_key` functions: maybe Fn(&T) -> &Song is enough? Compared
|
||
// to -> Song
|
||
use crate::{BlissError, BlissResult, Song, NUMBER_FEATURES};
|
||
use ndarray::{Array, Array1, Array2, Axis};
|
||
use ndarray_stats::QuantileExt;
|
||
use noisy_float::prelude::*;
|
||
use std::collections::HashMap;
|
||
|
||
/// Convenience trait for user-defined distance metrics.
|
||
pub trait DistanceMetric: Fn(&Array1<f32>, &Array1<f32>) -> f32 {}
|
||
impl<F> DistanceMetric for F where F: Fn(&Array1<f32>, &Array1<f32>) -> f32 {}
|
||
|
||
/// Return the [euclidean
|
||
/// distance](https://en.wikipedia.org/wiki/Euclidean_distance#Higher_dimensions)
|
||
/// between two vectors.
|
||
pub fn euclidean_distance(a: &Array1<f32>, b: &Array1<f32>) -> f32 {
|
||
// Could be any square symmetric positive semi-definite matrix;
|
||
// just no metric learning has been done yet.
|
||
// See https://lelele.io/thesis.pdf chapter 4.
|
||
let m = Array::eye(NUMBER_FEATURES);
|
||
|
||
(a - b).dot(&m).dot(&(a - b)).sqrt()
|
||
}
|
||
|
||
/// Return the [cosine
|
||
/// distance](https://en.wikipedia.org/wiki/Cosine_similarity#Angular_distance_and_similarity)
|
||
/// between two vectors.
|
||
pub fn cosine_distance(a: &Array1<f32>, b: &Array1<f32>) -> f32 {
|
||
let similarity = a.dot(b) / (a.dot(a).sqrt() * b.dot(b).sqrt());
|
||
1. - similarity
|
||
}
|
||
|
||
/// Sort `songs` in place by putting songs close to `first_song` first
|
||
/// using the `distance` metric.
|
||
pub fn closest_to_first_song(
|
||
first_song: &Song,
|
||
#[allow(clippy::ptr_arg)] songs: &mut Vec<Song>,
|
||
distance: impl DistanceMetric,
|
||
) {
|
||
songs.sort_by_cached_key(|song| n32(first_song.custom_distance(song, &distance)));
|
||
}
|
||
|
||
/// Sort `songs` in place by putting songs close to `first_song` first
|
||
/// using the `distance` metric.
|
||
///
|
||
/// Sort songs with a key extraction function, useful for when you have a
|
||
/// structure like `CustomSong { bliss_song: Song, something_else: bool }`
|
||
pub fn closest_to_first_song_by_key<F, T>(
|
||
first_song: &T,
|
||
#[allow(clippy::ptr_arg)] songs: &mut Vec<T>,
|
||
distance: impl DistanceMetric,
|
||
key_fn: F,
|
||
) where
|
||
F: Fn(&T) -> Song,
|
||
{
|
||
let first_song = key_fn(first_song);
|
||
songs.sort_by_cached_key(|song| n32(first_song.custom_distance(&key_fn(song), &distance)));
|
||
}
|
||
|
||
/// Sort `songs` in place using the `distance` metric and ordering by
|
||
/// the smallest distance between each song.
|
||
///
|
||
/// 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.
|
||
///
|
||
/// Note that this has a tendency to go from one style to the other very fast,
|
||
/// and it can be slow on big libraries.
|
||
pub fn song_to_song(first_song: &Song, songs: &mut Vec<Song>, distance: impl DistanceMetric) {
|
||
let mut new_songs = Vec::with_capacity(songs.len());
|
||
let mut song = first_song.to_owned();
|
||
|
||
while !songs.is_empty() {
|
||
let distances: Array1<f32> =
|
||
Array::from_shape_fn(songs.len(), |i| song.custom_distance(&songs[i], &distance));
|
||
let idx = distances.argmin().unwrap();
|
||
song = songs[idx].to_owned();
|
||
new_songs.push(song.to_owned());
|
||
songs.retain(|s| s != &song);
|
||
}
|
||
*songs = new_songs;
|
||
}
|
||
|
||
/// Sort `songs` in place using the `distance` metric and ordering by
|
||
/// the smallest distance between each song.
|
||
///
|
||
/// 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.
|
||
///
|
||
/// Note that this has a tendency to go from one style to the other very fast,
|
||
/// and it can be slow on big libraries.
|
||
///
|
||
/// Sort songs with a key extraction function, useful for when you have a
|
||
/// structure like `CustomSong { bliss_song: Song, something_else: bool }`
|
||
// TODO: maybe Clone is not needed?
|
||
pub fn song_to_song_by_key<F, T: std::cmp::PartialEq + Clone>(
|
||
first_song: &T,
|
||
songs: &mut Vec<T>,
|
||
distance: impl DistanceMetric,
|
||
key_fn: F,
|
||
) where
|
||
F: Fn(&T) -> Song,
|
||
{
|
||
let mut new_songs: Vec<T> = Vec::with_capacity(songs.len());
|
||
let mut bliss_song = key_fn(&first_song.to_owned());
|
||
|
||
while !songs.is_empty() {
|
||
let distances: Array1<f32> = Array::from_shape_fn(songs.len(), |i| {
|
||
bliss_song.custom_distance(&key_fn(&songs[i]), &distance)
|
||
});
|
||
let idx = distances.argmin().unwrap();
|
||
let song = songs[idx].to_owned();
|
||
bliss_song = key_fn(&songs[idx]).to_owned();
|
||
new_songs.push(song.to_owned());
|
||
songs.retain(|s| s != &song);
|
||
}
|
||
*songs = new_songs;
|
||
}
|
||
|
||
/// Remove duplicate songs from a playlist, in place.
|
||
///
|
||
/// Two songs are considered duplicates if they either have the same,
|
||
/// non-empty title and artist name, or if they are close enough in terms
|
||
/// of distance.
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `songs`: The playlist to remove duplicates from.
|
||
/// * `distance_threshold`: The distance threshold under which two songs are
|
||
/// considered identical. If `None`, a default value of 0.05 will be used.
|
||
pub fn dedup_playlist(songs: &mut Vec<Song>, distance_threshold: Option<f32>) {
|
||
dedup_playlist_custom_distance(songs, distance_threshold, euclidean_distance);
|
||
}
|
||
|
||
/// Remove duplicate songs from a playlist, in place.
|
||
///
|
||
/// Two songs are considered duplicates if they either have the same,
|
||
/// non-empty title and artist name, or if they are close enough in terms
|
||
/// of distance.
|
||
///
|
||
/// Dedup songs with a key extraction function, useful for when you have a
|
||
/// structure like `CustomSong { bliss_song: Song, something_else: bool }` you
|
||
/// want to deduplicate.
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `songs`: The playlist to remove duplicates from.
|
||
/// * `distance_threshold`: The distance threshold under which two songs are
|
||
/// considered identical. If `None`, a default value of 0.05 will be used.
|
||
/// * `key_fn`: A function used to retrieve the bliss [Song] from `T`.
|
||
pub fn dedup_playlist_by_key<T, F>(songs: &mut Vec<T>, distance_threshold: Option<f32>, key_fn: F)
|
||
where
|
||
F: Fn(&T) -> Song,
|
||
{
|
||
dedup_playlist_custom_distance_by_key(songs, distance_threshold, euclidean_distance, key_fn);
|
||
}
|
||
|
||
/// Remove duplicate songs from a playlist, in place, using a custom distance
|
||
/// metric.
|
||
///
|
||
/// Two songs are considered duplicates if they either have the same,
|
||
/// non-empty title and artist name, or if they are close enough in terms
|
||
/// of distance.
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `songs`: The playlist to remove duplicates from.
|
||
/// * `distance_threshold`: The distance threshold under which two songs are
|
||
/// considered identical. If `None`, a default value of 0.05 will be used.
|
||
/// * `distance`: A custom distance metric.
|
||
pub fn dedup_playlist_custom_distance(
|
||
songs: &mut Vec<Song>,
|
||
distance_threshold: Option<f32>,
|
||
distance: impl DistanceMetric,
|
||
) {
|
||
songs.dedup_by(|s1, s2| {
|
||
n32(s1.custom_distance(s2, &distance)) < distance_threshold.unwrap_or(0.05)
|
||
|| (s1.title.is_some()
|
||
&& s2.title.is_some()
|
||
&& s1.artist.is_some()
|
||
&& s2.artist.is_some()
|
||
&& s1.title == s2.title
|
||
&& s1.artist == s2.artist)
|
||
});
|
||
}
|
||
|
||
/// Remove duplicate songs from a playlist, in place, using a custom distance
|
||
/// metric.
|
||
///
|
||
/// Two songs are considered duplicates if they either have the same,
|
||
/// non-empty title and artist name, or if they are close enough in terms
|
||
/// of distance.
|
||
///
|
||
/// Dedup songs with a key extraction function, useful for when you have a
|
||
/// structure like `CustomSong { bliss_song: Song, something_else: bool }`
|
||
/// you want to deduplicate.
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `songs`: The playlist to remove duplicates from.
|
||
/// * `distance_threshold`: The distance threshold under which two songs are
|
||
/// considered identical. If `None`, a default value of 0.05 will be used.
|
||
/// * `distance`: A custom distance metric.
|
||
/// * `key_fn`: A function used to retrieve the bliss [Song] from `T`.
|
||
pub fn dedup_playlist_custom_distance_by_key<F, T>(
|
||
songs: &mut Vec<T>,
|
||
distance_threshold: Option<f32>,
|
||
distance: impl DistanceMetric,
|
||
key_fn: F,
|
||
) where
|
||
F: Fn(&T) -> Song,
|
||
{
|
||
songs.dedup_by(|s1, s2| {
|
||
let s1 = key_fn(s1);
|
||
let s2 = key_fn(s2);
|
||
n32(s1.custom_distance(&s2, &distance)) < distance_threshold.unwrap_or(0.05)
|
||
|| (s1.title.is_some()
|
||
&& s2.title.is_some()
|
||
&& s1.artist.is_some()
|
||
&& s2.artist.is_some()
|
||
&& s1.title == s2.title
|
||
&& s1.artist == s2.artist)
|
||
});
|
||
}
|
||
|
||
/// Return a list of albums in a `pool` of songs that are similar to
|
||
/// songs in `group`, discarding songs that don't belong to an album.
|
||
/// It basically makes an "album" playlist from the `pool` of songs.
|
||
///
|
||
/// `group` should be ordered by track number.
|
||
///
|
||
/// Songs from `group` would usually just be songs from an album, but not
|
||
/// necessarily - they are discarded from `pool` no matter what.
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `group` - A small group of songs, e.g. an album.
|
||
/// * `pool` - A pool of songs to find similar songs in, e.g. a user's song
|
||
/// library.
|
||
///
|
||
/// # Returns
|
||
///
|
||
/// A vector of songs, including `group` at the beginning, 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>>()`.
|
||
pub fn closest_album_to_group(group: Vec<Song>, pool: Vec<Song>) -> BlissResult<Vec<Song>> {
|
||
let mut albums_analysis: HashMap<&str, Array2<f32>> = HashMap::new();
|
||
let mut albums = Vec::new();
|
||
|
||
// Remove songs from the group from the pool.
|
||
let pool = pool
|
||
.into_iter()
|
||
.filter(|s| !group.contains(s))
|
||
.collect::<Vec<_>>();
|
||
for song in &pool {
|
||
if let Some(album) = &song.album {
|
||
if let Some(analysis) = albums_analysis.get_mut(album as &str) {
|
||
analysis
|
||
.push_row(song.analysis.as_arr1().view())
|
||
.map_err(|e| {
|
||
BlissError::ProviderError(format!("while computing distances: {}", e))
|
||
})?;
|
||
} else {
|
||
let mut array = Array::zeros((1, song.analysis.as_arr1().len()));
|
||
array.assign(&song.analysis.as_arr1());
|
||
albums_analysis.insert(album, array);
|
||
}
|
||
}
|
||
}
|
||
let mut group_analysis = Array::zeros((group.len(), NUMBER_FEATURES));
|
||
for (song, mut column) in group.iter().zip(group_analysis.axis_iter_mut(Axis(0))) {
|
||
column.assign(&song.analysis.as_arr1());
|
||
}
|
||
let first_analysis = group_analysis
|
||
.mean_axis(Axis(0))
|
||
.ok_or_else(|| BlissError::ProviderError(String::from("Mean of empty slice")))?;
|
||
for (album, analysis) in albums_analysis.iter() {
|
||
let mean_analysis = analysis
|
||
.mean_axis(Axis(0))
|
||
.ok_or_else(|| BlissError::ProviderError(String::from("Mean of empty slice")))?;
|
||
let album = album.to_owned();
|
||
albums.push((album, mean_analysis.to_owned()));
|
||
}
|
||
|
||
albums.sort_by_key(|(_, analysis)| n32(euclidean_distance(&first_analysis, analysis)));
|
||
let mut playlist = group;
|
||
for (album, _) in albums {
|
||
let mut al = pool
|
||
.iter()
|
||
.filter(|s| s.album.is_some() && s.album.as_ref().unwrap() == &album.to_string())
|
||
.map(|s| s.to_owned())
|
||
.collect::<Vec<Song>>();
|
||
al.sort_by(|s1, s2| {
|
||
let track_number1 = s1
|
||
.track_number
|
||
.to_owned()
|
||
.unwrap_or_else(|| String::from(""));
|
||
let track_number2 = s2
|
||
.track_number
|
||
.to_owned()
|
||
.unwrap_or_else(|| String::from(""));
|
||
if let Ok(x) = track_number1.parse::<i32>() {
|
||
if let Ok(y) = track_number2.parse::<i32>() {
|
||
return x.cmp(&y);
|
||
}
|
||
}
|
||
s1.track_number.cmp(&s2.track_number)
|
||
});
|
||
playlist.extend_from_slice(&al);
|
||
}
|
||
Ok(playlist)
|
||
}
|
||
|
||
/// Return a list of albums in a `pool` of songs that are similar to
|
||
/// songs in `group`, discarding songs that don't belong to an album.
|
||
/// It basically makes an "album" playlist from the `pool` of songs.
|
||
///
|
||
/// `group` should be ordered by track number.
|
||
///
|
||
/// Songs from `group` would usually just be songs from an album, but not
|
||
/// necessarily - they are discarded from `pool` no matter what.
|
||
///
|
||
/// Order songs with a key extraction function, useful for when you have a
|
||
/// structure like `CustomSong { bliss_song: Song, something_else: bool }`
|
||
/// you want to order.
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `group` - A small group of songs, e.g. an album.
|
||
/// * `pool` - A pool of songs to find similar songs in, e.g. a user's song
|
||
/// library.
|
||
/// * `key_fn`: A function used to retrieve the bliss [Song] from `T`.
|
||
///
|
||
/// # Returns
|
||
///
|
||
/// A vector of T, including `group` at the beginning, 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>>()`.
|
||
// TODO: maybe Clone is not needed?
|
||
pub fn closest_album_to_group_by_key<T: PartialEq + Clone, F>(
|
||
group: Vec<T>,
|
||
pool: Vec<T>,
|
||
key_fn: F,
|
||
) -> BlissResult<Vec<T>>
|
||
where
|
||
F: Fn(&T) -> Song,
|
||
{
|
||
let mut albums_analysis: HashMap<String, Array2<f32>> = HashMap::new();
|
||
let mut albums = Vec::new();
|
||
|
||
// Remove songs from the group from the pool.
|
||
let pool = pool
|
||
.into_iter()
|
||
.filter(|s| !group.contains(s))
|
||
.collect::<Vec<_>>();
|
||
for song in &pool {
|
||
let song = key_fn(song);
|
||
if let Some(album) = song.album {
|
||
if let Some(analysis) = albums_analysis.get_mut(&album as &str) {
|
||
analysis
|
||
.push_row(song.analysis.as_arr1().view())
|
||
.map_err(|e| {
|
||
BlissError::ProviderError(format!("while computing distances: {}", e))
|
||
})?;
|
||
} else {
|
||
let mut array = Array::zeros((1, song.analysis.as_arr1().len()));
|
||
array.assign(&song.analysis.as_arr1());
|
||
albums_analysis.insert(album.to_owned(), array);
|
||
}
|
||
}
|
||
}
|
||
let mut group_analysis = Array::zeros((group.len(), NUMBER_FEATURES));
|
||
for (song, mut column) in group.iter().zip(group_analysis.axis_iter_mut(Axis(0))) {
|
||
let song = key_fn(song);
|
||
column.assign(&song.analysis.as_arr1());
|
||
}
|
||
let first_analysis = group_analysis
|
||
.mean_axis(Axis(0))
|
||
.ok_or_else(|| BlissError::ProviderError(String::from("Mean of empty slice")))?;
|
||
for (album, analysis) in albums_analysis.iter() {
|
||
let mean_analysis = analysis
|
||
.mean_axis(Axis(0))
|
||
.ok_or_else(|| BlissError::ProviderError(String::from("Mean of empty slice")))?;
|
||
let album = album.to_owned();
|
||
albums.push((album, mean_analysis.to_owned()));
|
||
}
|
||
|
||
albums.sort_by_key(|(_, analysis)| n32(euclidean_distance(&first_analysis, analysis)));
|
||
let mut playlist = group;
|
||
for (album, _) in albums {
|
||
let mut al = pool
|
||
.iter()
|
||
.filter(|s| {
|
||
let s = key_fn(s);
|
||
s.album.is_some() && s.album.as_ref().unwrap() == &album.to_string()
|
||
})
|
||
.map(|s| s.to_owned())
|
||
.collect::<Vec<T>>();
|
||
al.sort_by(|s1, s2| {
|
||
let s1 = key_fn(s1);
|
||
let s2 = key_fn(s2);
|
||
let track_number1 = s1
|
||
.track_number
|
||
.to_owned()
|
||
.unwrap_or_else(|| String::from(""));
|
||
let track_number2 = s2
|
||
.track_number
|
||
.to_owned()
|
||
.unwrap_or_else(|| String::from(""));
|
||
if let Ok(x) = track_number1.parse::<i32>() {
|
||
if let Ok(y) = track_number2.parse::<i32>() {
|
||
return x.cmp(&y);
|
||
}
|
||
}
|
||
s1.track_number.cmp(&s2.track_number)
|
||
});
|
||
playlist.extend_from_slice(&al);
|
||
}
|
||
Ok(playlist)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod test {
|
||
use super::*;
|
||
use crate::Analysis;
|
||
use ndarray::arr1;
|
||
use std::path::Path;
|
||
|
||
#[derive(Debug, Clone, PartialEq)]
|
||
struct CustomSong {
|
||
something: bool,
|
||
bliss_song: Song,
|
||
}
|
||
|
||
#[test]
|
||
fn test_dedup_playlist_custom_distance() {
|
||
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.,
|
||
]),
|
||
title: Some(String::from("dupe-title")),
|
||
artist: Some(String::from("dupe-artist")),
|
||
..Default::default()
|
||
};
|
||
let third_song = Song {
|
||
path: Path::new("path-to-third").to_path_buf(),
|
||
title: Some(String::from("dupe-title")),
|
||
artist: Some(String::from("dupe-artist")),
|
||
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(),
|
||
artist: Some(String::from("no-dupe-artist")),
|
||
title: Some(String::from("dupe-title")),
|
||
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-fourth").to_path_buf(),
|
||
analysis: Analysis::new([
|
||
2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 0.001, 1., 1., 1.,
|
||
]),
|
||
..Default::default()
|
||
};
|
||
|
||
let mut playlist = 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(),
|
||
];
|
||
dedup_playlist_custom_distance(&mut playlist, None, euclidean_distance);
|
||
assert_eq!(
|
||
playlist,
|
||
vec![
|
||
first_song.to_owned(),
|
||
second_song.to_owned(),
|
||
fourth_song.to_owned(),
|
||
],
|
||
);
|
||
let mut playlist = 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(),
|
||
];
|
||
dedup_playlist_custom_distance(&mut playlist, Some(20.), cosine_distance);
|
||
assert_eq!(playlist, vec![first_song.to_owned()]);
|
||
let mut playlist = 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(),
|
||
];
|
||
dedup_playlist(&mut playlist, Some(20.));
|
||
assert_eq!(playlist, vec![first_song.to_owned()]);
|
||
let mut playlist = 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(),
|
||
];
|
||
dedup_playlist(&mut playlist, None);
|
||
assert_eq!(
|
||
playlist,
|
||
vec![
|
||
first_song.to_owned(),
|
||
second_song.to_owned(),
|
||
fourth_song.to_owned(),
|
||
]
|
||
);
|
||
|
||
let first_song = CustomSong {
|
||
bliss_song: first_song,
|
||
something: true,
|
||
};
|
||
let second_song = CustomSong {
|
||
bliss_song: second_song,
|
||
something: true,
|
||
};
|
||
let first_song_dupe = CustomSong {
|
||
bliss_song: first_song_dupe,
|
||
something: true,
|
||
};
|
||
let third_song = CustomSong {
|
||
bliss_song: third_song,
|
||
something: true,
|
||
};
|
||
let fourth_song = CustomSong {
|
||
bliss_song: fourth_song,
|
||
something: true,
|
||
};
|
||
|
||
let fifth_song = CustomSong {
|
||
bliss_song: fifth_song,
|
||
something: true,
|
||
};
|
||
|
||
let mut playlist = 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(),
|
||
];
|
||
dedup_playlist_custom_distance_by_key(&mut playlist, None, euclidean_distance, |s| {
|
||
s.bliss_song.to_owned()
|
||
});
|
||
assert_eq!(
|
||
playlist,
|
||
vec![
|
||
first_song.to_owned(),
|
||
second_song.to_owned(),
|
||
fourth_song.to_owned(),
|
||
],
|
||
);
|
||
let mut playlist = 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(),
|
||
];
|
||
dedup_playlist_custom_distance_by_key(&mut playlist, Some(20.), cosine_distance, |s| {
|
||
s.bliss_song.to_owned()
|
||
});
|
||
assert_eq!(playlist, vec![first_song.to_owned()]);
|
||
let mut playlist = 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(),
|
||
];
|
||
dedup_playlist_by_key(&mut playlist, Some(20.), |s| s.bliss_song.to_owned());
|
||
assert_eq!(playlist, vec![first_song.to_owned()]);
|
||
let mut playlist = 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(),
|
||
];
|
||
dedup_playlist_by_key(&mut playlist, None, |s| s.bliss_song.to_owned());
|
||
assert_eq!(
|
||
playlist,
|
||
vec![
|
||
first_song.to_owned(),
|
||
second_song.to_owned(),
|
||
fourth_song.to_owned(),
|
||
]
|
||
);
|
||
}
|
||
|
||
#[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(),
|
||
third_song.to_owned(),
|
||
first_song_dupe.to_owned(),
|
||
second_song.to_owned(),
|
||
fourth_song.to_owned(),
|
||
];
|
||
song_to_song(&first_song, &mut songs, euclidean_distance);
|
||
assert_eq!(
|
||
songs,
|
||
vec![
|
||
first_song.to_owned(),
|
||
first_song_dupe.to_owned(),
|
||
second_song.to_owned(),
|
||
third_song.to_owned(),
|
||
fourth_song.to_owned(),
|
||
],
|
||
);
|
||
|
||
let first_song = CustomSong {
|
||
bliss_song: first_song,
|
||
something: true,
|
||
};
|
||
let second_song = CustomSong {
|
||
bliss_song: second_song,
|
||
something: true,
|
||
};
|
||
let first_song_dupe = CustomSong {
|
||
bliss_song: first_song_dupe,
|
||
something: true,
|
||
};
|
||
let third_song = CustomSong {
|
||
bliss_song: third_song,
|
||
something: true,
|
||
};
|
||
let fourth_song = CustomSong {
|
||
bliss_song: fourth_song,
|
||
something: true,
|
||
};
|
||
|
||
let mut songs: Vec<CustomSong> = vec![
|
||
first_song.to_owned(),
|
||
first_song_dupe.to_owned(),
|
||
third_song.to_owned(),
|
||
fourth_song.to_owned(),
|
||
second_song.to_owned(),
|
||
];
|
||
|
||
song_to_song_by_key(&first_song, &mut songs, euclidean_distance, |s| {
|
||
s.bliss_song.to_owned()
|
||
});
|
||
|
||
assert_eq!(
|
||
songs,
|
||
vec![
|
||
first_song,
|
||
first_song_dupe,
|
||
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);
|
||
|
||
let first_song = CustomSong {
|
||
bliss_song: first_song,
|
||
something: true,
|
||
};
|
||
let second_song = CustomSong {
|
||
bliss_song: second_song,
|
||
something: true,
|
||
};
|
||
let first_song_dupe = CustomSong {
|
||
bliss_song: first_song_dupe,
|
||
something: true,
|
||
};
|
||
let third_song = CustomSong {
|
||
bliss_song: third_song,
|
||
something: true,
|
||
};
|
||
let fourth_song = CustomSong {
|
||
bliss_song: fourth_song,
|
||
something: true,
|
||
};
|
||
|
||
let fifth_song = CustomSong {
|
||
bliss_song: fifth_song,
|
||
something: true,
|
||
};
|
||
|
||
let mut songs: Vec<CustomSong> = 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_by_key(&first_song, &mut songs, euclidean_distance, |s| {
|
||
s.bliss_song.to_owned()
|
||
});
|
||
|
||
assert_eq!(
|
||
songs,
|
||
vec![
|
||
first_song,
|
||
first_song_dupe,
|
||
second_song,
|
||
fourth_song,
|
||
fifth_song,
|
||
third_song
|
||
],
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_euclidean_distance() {
|
||
let a = arr1(&[
|
||
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 0.,
|
||
]);
|
||
let b = arr1(&[
|
||
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.,
|
||
]);
|
||
assert_eq!(euclidean_distance(&a, &b), 4.242640687119285);
|
||
|
||
let a = arr1(&[0.5; 20]);
|
||
let b = arr1(&[0.5; 20]);
|
||
assert_eq!(euclidean_distance(&a, &b), 0.);
|
||
assert_eq!(euclidean_distance(&a, &b), 0.);
|
||
}
|
||
|
||
#[test]
|
||
fn test_cosine_distance() {
|
||
let a = arr1(&[
|
||
1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 0.,
|
||
]);
|
||
let b = arr1(&[
|
||
0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0.,
|
||
]);
|
||
assert_eq!(cosine_distance(&a, &b), 0.7705842661294382);
|
||
|
||
let a = arr1(&[0.5; 20]);
|
||
let b = arr1(&[0.5; 20]);
|
||
assert_eq!(cosine_distance(&a, &b), 0.);
|
||
assert_eq!(cosine_distance(&a, &b), 0.);
|
||
}
|
||
|
||
#[test]
|
||
fn test_closest_to_group() {
|
||
let first_song = Song {
|
||
path: Path::new("path-to-first").to_path_buf(),
|
||
analysis: Analysis::new([0.; 20]),
|
||
album: Some(String::from("Album")),
|
||
artist: Some(String::from("Artist")),
|
||
track_number: Some(String::from("01")),
|
||
..Default::default()
|
||
};
|
||
|
||
let second_song = Song {
|
||
path: Path::new("path-to-second").to_path_buf(),
|
||
analysis: Analysis::new([0.1; 20]),
|
||
album: Some(String::from("Another Album")),
|
||
artist: Some(String::from("Artist")),
|
||
track_number: Some(String::from("10")),
|
||
..Default::default()
|
||
};
|
||
|
||
let third_song = Song {
|
||
path: Path::new("path-to-third").to_path_buf(),
|
||
analysis: Analysis::new([10.; 20]),
|
||
album: Some(String::from("Album")),
|
||
artist: Some(String::from("Another Artist")),
|
||
track_number: Some(String::from("02")),
|
||
..Default::default()
|
||
};
|
||
|
||
let fourth_song = Song {
|
||
path: Path::new("path-to-fourth").to_path_buf(),
|
||
analysis: Analysis::new([20.; 20]),
|
||
album: Some(String::from("Another Album")),
|
||
artist: Some(String::from("Another Artist")),
|
||
track_number: Some(String::from("01")),
|
||
..Default::default()
|
||
};
|
||
let fifth_song = Song {
|
||
path: Path::new("path-to-fifth").to_path_buf(),
|
||
analysis: Analysis::new([40.; 20]),
|
||
artist: Some(String::from("Third Artist")),
|
||
album: None,
|
||
..Default::default()
|
||
};
|
||
|
||
let pool = vec![
|
||
first_song.to_owned(),
|
||
fourth_song.to_owned(),
|
||
third_song.to_owned(),
|
||
second_song.to_owned(),
|
||
fifth_song.to_owned(),
|
||
];
|
||
let group = vec![first_song.to_owned(), third_song.to_owned()];
|
||
assert_eq!(
|
||
vec![
|
||
first_song.to_owned(),
|
||
third_song.to_owned(),
|
||
fourth_song.to_owned(),
|
||
second_song.to_owned()
|
||
],
|
||
closest_album_to_group(group, pool.to_owned()).unwrap(),
|
||
);
|
||
|
||
let first_song = CustomSong {
|
||
bliss_song: first_song,
|
||
something: true,
|
||
};
|
||
let second_song = CustomSong {
|
||
bliss_song: second_song,
|
||
something: true,
|
||
};
|
||
let third_song = CustomSong {
|
||
bliss_song: third_song,
|
||
something: true,
|
||
};
|
||
let fourth_song = CustomSong {
|
||
bliss_song: fourth_song,
|
||
something: true,
|
||
};
|
||
|
||
let fifth_song = CustomSong {
|
||
bliss_song: fifth_song,
|
||
something: true,
|
||
};
|
||
|
||
let pool = vec![
|
||
first_song.to_owned(),
|
||
fourth_song.to_owned(),
|
||
third_song.to_owned(),
|
||
second_song.to_owned(),
|
||
fifth_song.to_owned(),
|
||
];
|
||
let group = vec![first_song.to_owned(), third_song.to_owned()];
|
||
assert_eq!(
|
||
vec![
|
||
first_song.to_owned(),
|
||
third_song.to_owned(),
|
||
fourth_song.to_owned(),
|
||
second_song.to_owned()
|
||
],
|
||
closest_album_to_group_by_key(group, pool.to_owned(), |s| s.bliss_song.to_owned())
|
||
.unwrap(),
|
||
);
|
||
}
|
||
}
|