bliss-rs/src/playlist.rs
2022-09-27 18:12:11 +02:00

985 lines
34 KiB
Rust
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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(),
);
}
}