Merge pull request #24 from Polochon-street/add-album-playlist

Add an option to make a playlist of albums
This commit is contained in:
Polochon-street 2021-10-02 21:35:26 +02:00 committed by GitHub
commit d2442260da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 174 additions and 28 deletions

View file

@ -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
View file

@ -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"

View file

@ -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"

View file

@ -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();