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
|
||||
|
||||
## bliss 0.3.5
|
||||
* Add custom sorting methods for playlist-making.
|
||||
|
||||
## bliss 0.3.4
|
||||
* Bump ffmpeg's version to avoid building ffmpeg when building bliss.
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "bliss-audio"
|
||||
version = "0.3.4"
|
||||
version = "0.3.5"
|
||||
authors = ["Polochon-street <polochonstreet@gmx.fr>"]
|
||||
edition = "2018"
|
||||
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
|
||||
//! experiment with them if the default (euclidean distance for now) doesn't
|
||||
//! suit you.
|
||||
use crate::NUMBER_FEATURES;
|
||||
#[cfg(doc)]
|
||||
use crate::{Library, Song};
|
||||
use crate::Library;
|
||||
use crate::Song;
|
||||
use crate::NUMBER_FEATURES;
|
||||
use ndarray::{Array, Array1};
|
||||
use noisy_float::prelude::*;
|
||||
|
||||
/// Convenience trait for user-defined distance metrics.
|
||||
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
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::Analysis;
|
||||
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]
|
||||
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.
|
||||
#[cfg(doc)]
|
||||
use crate::distance;
|
||||
use crate::distance::DistanceMetric;
|
||||
use crate::distance::{closest_to_first_song, DistanceMetric, euclidean_distance};
|
||||
use crate::{BlissError, BlissResult, Song};
|
||||
use log::{debug, error, info};
|
||||
use noisy_float::prelude::*;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::mpsc::{Receiver, Sender};
|
||||
use std::thread;
|
||||
|
@ -30,7 +29,8 @@ pub trait Library {
|
|||
/// once.
|
||||
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
|
||||
///
|
||||
|
@ -40,21 +40,22 @@ pub trait Library {
|
|||
///
|
||||
/// # 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
|
||||
/// `ret.map(|song| song.path.to_owned()).collect::<Vec<String>>()`.
|
||||
// TODO return an iterator and not a Vec
|
||||
fn playlist_from_song(
|
||||
&self,
|
||||
first_song: Song,
|
||||
playlist_length: usize,
|
||||
) -> BlissResult<Vec<Song>> {
|
||||
let mut songs = self.get_stored_songs()?;
|
||||
songs.sort_by_cached_key(|song| n32(first_song.distance(&song)));
|
||||
let playlist = self.playlist_from_song_custom(
|
||||
first_song,
|
||||
playlist_length,
|
||||
euclidean_distance,
|
||||
closest_to_first_song,
|
||||
)?;
|
||||
|
||||
let playlist = songs
|
||||
.into_iter()
|
||||
.take(playlist_length)
|
||||
.collect::<Vec<Song>>();
|
||||
debug!(
|
||||
"Playlist created: {}",
|
||||
playlist
|
||||
|
@ -67,7 +68,7 @@ pub trait Library {
|
|||
}
|
||||
|
||||
/// Return a list of songs that are similar to ``first_song``, using a
|
||||
/// custom distance metric.
|
||||
/// custom distance metric and deduplicating indentical songs.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
|
@ -98,13 +99,13 @@ pub trait Library {
|
|||
playlist_length: usize,
|
||||
distance: impl DistanceMetric,
|
||||
) -> BlissResult<Vec<Song>> {
|
||||
let mut songs = self.get_stored_songs()?;
|
||||
songs.sort_by_cached_key(|song| n32(first_song.custom_distance(&song, &distance)));
|
||||
let playlist = self.playlist_from_song_custom(
|
||||
first_song,
|
||||
playlist_length,
|
||||
distance,
|
||||
closest_to_first_song,
|
||||
)?;
|
||||
|
||||
let playlist = songs
|
||||
.into_iter()
|
||||
.take(playlist_length)
|
||||
.collect::<Vec<Song>>();
|
||||
debug!(
|
||||
"Playlist created: {}",
|
||||
playlist
|
||||
|
@ -116,6 +117,44 @@ pub trait Library {
|
|||
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
|
||||
/// `store_error_song` implementations.
|
||||
///
|
||||
|
@ -595,4 +634,49 @@ mod test {
|
|||
.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