Add an option to make a playlist of albums
This commit is contained in:
parent
80a9e7c446
commit
be0a3e5290
4 changed files with 174 additions and 28 deletions
|
@ -1,5 +1,8 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## bliss 0.4.1
|
||||||
|
* Add a function to make album playlists.
|
||||||
|
|
||||||
## bliss 0.4.0
|
## bliss 0.4.0
|
||||||
* Make the song-to-song custom sorting method faster.
|
* Make the song-to-song custom sorting method faster.
|
||||||
* Rename `to_vec` and `to_arr1` to `as_vec` and `as_arr1` .
|
* Rename `to_vec` and `to_arr1` to `as_vec` and `as_arr1` .
|
||||||
|
|
54
Cargo.lock
generated
54
Cargo.lock
generated
|
@ -28,9 +28,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.43"
|
version = "1.0.44"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf"
|
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atty"
|
name = "atty"
|
||||||
|
@ -77,7 +77,7 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bliss-audio"
|
name = "bliss-audio"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bliss-audio-aubio-rs",
|
"bliss-audio-aubio-rs",
|
||||||
"crossbeam",
|
"crossbeam",
|
||||||
|
@ -183,9 +183,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.69"
|
version = "1.0.70"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2"
|
checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jobserver",
|
"jobserver",
|
||||||
]
|
]
|
||||||
|
@ -403,9 +403,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.0.20"
|
version = "1.0.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0"
|
checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
|
@ -554,9 +554,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.100"
|
version = "0.2.103"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a1fa8cddc8fbbee11227ef194b5317ed014b8acbf15139bd716a18ad3fe99ec5"
|
checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libloading"
|
name = "libloading"
|
||||||
|
@ -700,9 +700,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint"
|
name = "num-bigint"
|
||||||
version = "0.4.0"
|
version = "0.4.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512"
|
checksum = "74e768dff5fb39a41b3bcd30bb25cf989706c90d028d1ad71971987aa309d535"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"num-integer",
|
"num-integer",
|
||||||
|
@ -830,9 +830,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.19"
|
version = "0.3.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c"
|
checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
|
@ -851,9 +851,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.28"
|
version = "1.0.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612"
|
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
@ -1026,18 +1026,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.128"
|
version = "1.0.130"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1056a0db1978e9dbf0f6e4fca677f6f9143dc1c19de346f22cac23e422196834"
|
checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.128"
|
version = "1.0.130"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "13af2fbb8b60a8950d6c72a56d2095c28870367cc8e10c55e9745bac4995a2c4"
|
checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -1088,9 +1088,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.75"
|
version = "1.0.77"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b7f58f7e8eaa0009c5fec437aabf511bd9933e4b2d7407bd05273c01a8906ea7"
|
checksum = "5239bc68e0fef57495900cfea4e8dc75596d9a319d7e16b1e0a440d24e6fe0a0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -1108,18 +1108,18 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.26"
|
version = "1.0.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2"
|
checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "1.0.26"
|
version = "1.0.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745"
|
checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -1167,9 +1167,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.13.0"
|
version = "1.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06"
|
checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ucd-trie"
|
name = "ucd-trie"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "bliss-audio"
|
name = "bliss-audio"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
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"
|
||||||
|
|
143
src/library.rs
143
src/library.rs
|
@ -8,6 +8,9 @@ use crate::distance;
|
||||||
use crate::distance::{closest_to_first_song, euclidean_distance, DistanceMetric};
|
use crate::distance::{closest_to_first_song, euclidean_distance, DistanceMetric};
|
||||||
use crate::{BlissError, BlissResult, Song};
|
use crate::{BlissError, BlissResult, Song};
|
||||||
use log::{debug, error, info};
|
use log::{debug, error, info};
|
||||||
|
use ndarray::{Array, Array2, Axis};
|
||||||
|
use noisy_float::prelude::n32;
|
||||||
|
use std::collections::HashMap;
|
||||||
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;
|
||||||
|
@ -29,6 +32,92 @@ 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 `number_albums` albums that are similar
|
||||||
|
/// to `album`, discarding songs that don't belong to an album.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `album` - The album the playlist will be built from.
|
||||||
|
/// * `number_albums` - The number of albums to queue.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// A vector of 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_songs_album(
|
||||||
|
&self,
|
||||||
|
first_album: &str,
|
||||||
|
playlist_length: usize,
|
||||||
|
) -> BlissResult<Vec<Song>> {
|
||||||
|
let songs = self.get_stored_songs()?;
|
||||||
|
let mut albums_analysis: HashMap<&str, Array2<f32>> = HashMap::new();
|
||||||
|
let mut albums = Vec::new();
|
||||||
|
|
||||||
|
for song in &songs {
|
||||||
|
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 first_analysis = None;
|
||||||
|
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()));
|
||||||
|
if album == first_album {
|
||||||
|
first_analysis = Some(mean_analysis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if first_analysis.is_none() {
|
||||||
|
return Err(BlissError::ProviderError(format!(
|
||||||
|
"Could not find album \"{}\".",
|
||||||
|
first_album
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
albums.sort_by_key(|(_, analysis)| {
|
||||||
|
n32(euclidean_distance(
|
||||||
|
first_analysis.as_ref().unwrap(),
|
||||||
|
&analysis,
|
||||||
|
))
|
||||||
|
});
|
||||||
|
let albums = albums.get(..playlist_length).unwrap_or(&albums);
|
||||||
|
let mut playlist = Vec::new();
|
||||||
|
for (album, _) in albums {
|
||||||
|
let mut al = songs
|
||||||
|
.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 `playlist_length` songs that are similar
|
/// Return a list of `playlist_length` songs that are similar
|
||||||
/// to ``first_song``, deduplicating identical songs.
|
/// to ``first_song``, deduplicating identical songs.
|
||||||
///
|
///
|
||||||
|
@ -510,6 +599,60 @@ mod test {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_playlist_from_album() {
|
||||||
|
let mut test_library = TestLibrary::default();
|
||||||
|
let first_song = Song {
|
||||||
|
path: Path::new("path-to-first").to_path_buf(),
|
||||||
|
analysis: Analysis::new([0.; 20]),
|
||||||
|
album: Some(String::from("Album")),
|
||||||
|
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")),
|
||||||
|
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")),
|
||||||
|
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")),
|
||||||
|
track_number: Some(String::from("01")),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let fifth_song = Song {
|
||||||
|
path: Path::new("path-to-fifth").to_path_buf(),
|
||||||
|
analysis: Analysis::new([20.; 20]),
|
||||||
|
album: None,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
test_library.internal_storage = vec![
|
||||||
|
first_song.to_owned(),
|
||||||
|
fourth_song.to_owned(),
|
||||||
|
third_song.to_owned(),
|
||||||
|
second_song.to_owned(),
|
||||||
|
fifth_song.to_owned(),
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
vec![first_song, third_song, fourth_song, second_song],
|
||||||
|
test_library.playlist_from_songs_album("Album", 3).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_playlist_from_song() {
|
fn test_playlist_from_song() {
|
||||||
let mut test_library = TestLibrary::default();
|
let mut test_library = TestLibrary::default();
|
||||||
|
|
Loading…
Reference in a new issue