From be0a3e5290f6cb33cf55f22702fd1e2c5b7e33af Mon Sep 17 00:00:00 2001 From: Polochon-street Date: Sat, 2 Oct 2021 19:53:24 +0200 Subject: [PATCH] Add an option to make a playlist of albums --- CHANGELOG.md | 3 ++ Cargo.lock | 54 +++++++++---------- Cargo.toml | 2 +- src/library.rs | 143 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 174 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0140ed7..70001eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## bliss 0.4.1 +* Add a function to make album playlists. + ## bliss 0.4.0 * Make the song-to-song custom sorting method faster. * Rename `to_vec` and `to_arr1` to `as_vec` and `as_arr1` . diff --git a/Cargo.lock b/Cargo.lock index 66bd73d..e924850 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf" +checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" [[package]] name = "atty" @@ -77,7 +77,7 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bliss-audio" -version = "0.4.0" +version = "0.4.1" dependencies = [ "bliss-audio-aubio-rs", "crossbeam", @@ -183,9 +183,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" +checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" dependencies = [ "jobserver", ] @@ -403,9 +403,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3aec53de10fe96d7d8c565eb17f2c687bb5518a2ec453b5b1252964526abe0" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" dependencies = [ "cfg-if 1.0.0", "crc32fast", @@ -554,9 +554,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1fa8cddc8fbbee11227ef194b5317ed014b8acbf15139bd716a18ad3fe99ec5" +checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" [[package]] name = "libloading" @@ -700,9 +700,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512" +checksum = "74e768dff5fb39a41b3bcd30bb25cf989706c90d028d1ad71971987aa309d535" dependencies = [ "autocfg", "num-integer", @@ -830,9 +830,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb" [[package]] name = "ppv-lite86" @@ -851,9 +851,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.28" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" +checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" dependencies = [ "unicode-xid", ] @@ -1026,18 +1026,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.128" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1056a0db1978e9dbf0f6e4fca677f6f9143dc1c19de346f22cac23e422196834" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.128" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13af2fbb8b60a8950d6c72a56d2095c28870367cc8e10c55e9745bac4995a2c4" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" dependencies = [ "proc-macro2", "quote", @@ -1088,9 +1088,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.75" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f58f7e8eaa0009c5fec437aabf511bd9933e4b2d7407bd05273c01a8906ea7" +checksum = "5239bc68e0fef57495900cfea4e8dc75596d9a319d7e16b1e0a440d24e6fe0a0" dependencies = [ "proc-macro2", "quote", @@ -1108,18 +1108,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.26" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" +checksum = "602eca064b2d83369e2b2f34b09c70b605402801927c65c11071ac911d299b88" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.26" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" +checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c" dependencies = [ "proc-macro2", "quote", @@ -1167,9 +1167,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.13.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" +checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" [[package]] name = "ucd-trie" diff --git a/Cargo.toml b/Cargo.toml index 30db7d9..d453f1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bliss-audio" -version = "0.4.0" +version = "0.4.1" authors = ["Polochon-street "] edition = "2018" license = "GPL-3.0-only" diff --git a/src/library.rs b/src/library.rs index 9ad5a25..4631fed 100644 --- a/src/library.rs +++ b/src/library.rs @@ -8,6 +8,9 @@ use crate::distance; use crate::distance::{closest_to_first_song, euclidean_distance, DistanceMetric}; use crate::{BlissError, BlissResult, Song}; 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::{Receiver, Sender}; use std::thread; @@ -29,6 +32,92 @@ pub trait Library { /// once. fn get_stored_songs(&self) -> BlissResult>; + /// 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::>()`. + fn playlist_from_songs_album( + &self, + first_album: &str, + playlist_length: usize, + ) -> BlissResult> { + let songs = self.get_stored_songs()?; + let mut albums_analysis: HashMap<&str, Array2> = 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::>(); + 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::() { + if let Ok(y) = track_number2.parse::() { + 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 /// 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] fn test_playlist_from_song() { let mut test_library = TestLibrary::default();