diff --git a/CHANGELOG.md b/CHANGELOG.md index da59198..054d50c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## blissify 0.3.12 +* Make deduplicating songs the default, and add a --no-deduplication option +* Add a "playlist from playlist" feature (Thanks @SimonTeixidor!) * Use window / offset to read the list of MPD files to avoid timeout errors. ## blissify 0.3.11 diff --git a/Cargo.lock b/Cargo.lock index a501bda..4787096 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,6 +156,7 @@ dependencies = [ "clap", "dirs 3.0.2", "env_logger", + "extended-isolation-forest", "indicatif 0.16.2", "log", "mpd", diff --git a/Cargo.toml b/Cargo.toml index e8b963c..7a757e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,3 +30,4 @@ noisy_float = "0.2.0" termion = "1.5.6" serde = "1.0" pretty_assertions = "1.2.1" +extended-isolation-forest = { version = "0.2.3", default-features = false } diff --git a/src/main.rs b/src/main.rs index e3f39e0..bae545b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,6 +30,8 @@ use std::num::NonZeroUsize; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; +use extended_isolation_forest::ForestOptions; + use std::io; use std::io::Write; #[cfg(not(test))] @@ -342,6 +344,43 @@ impl MPDLibrary { Ok(()) } + fn queue_from_current_playlist( + &self, + number_songs: usize, + distance: &dyn DistanceMetricBuilder, + mut sort_by: F, + dedup: bool, + ) -> Result<()> + where + F: FnMut(&[LibrarySong<()>], &mut [LibrarySong<()>], &dyn DistanceMetricBuilder), + { + let mut mpd_conn = self.mpd_conn.lock().unwrap(); + mpd_conn.random(false)?; + let mpd_songs = mpd_conn.queue()?; + let paths = mpd_songs + .iter() + .map(|s| { + self.mpd_to_bliss_path(s) + .map(|s| s.to_string_lossy().to_string()) + }) + .collect::, _>>()?; + let paths = paths.iter().map(|s| &**s).collect::>(); + + let playlist = self.library.playlist_from_custom( + &paths, + number_songs, + distance, + &mut sort_by, + dedup, + )?; + + for song in &playlist[1..] { + let mpd_song = self.bliss_song_to_mpd(song)?; + mpd_conn.push(mpd_song)?; + } + Ok(()) + } + fn queue_from_current_song_custom( &self, number_songs: usize, @@ -690,10 +729,11 @@ Useful to avoid a too heavy load on a machine.") ) .takes_value(false) ) - .arg(Arg::with_name("dedup") - .long("deduplicate-songs") + .arg(Arg::with_name("no-dedup") + .long("no-deduplication") .help( - "Deduplicate songs based both on the title / artist and their sheer proximity." + "Do not deduplicate songs based both on the title / artist and their\ + sheer proximity." ) .takes_value(false) ) @@ -702,6 +742,13 @@ Useful to avoid a too heavy load on a machine.") .help("Make a playlist of similar albums from the current album.") .takes_value(false) ) + .arg(Arg::with_name("entire") + .long("from-entire-playlist") + .help("Make a playlist of songs similar to all the playlist's songs, \ + instead of just the first one. Defaults to using the distance metric \ + extended_isolation_forest, which give the best results.") + .takes_value(false) + ) ) .subcommand( SubCommand::with_name("interactive-playlist") @@ -787,33 +834,45 @@ Defaults to 3, cannot be more than 9." if sub_m.is_present("album") { library.queue_from_current_album(number_songs)?; } else { - let distance_metric = if let Some(m) = sub_m.value_of("distance") { + // TODO let users customize options? + let forest_distance: &dyn DistanceMetricBuilder = &ForestOptions { + n_trees: 1000, + sample_size: 200, + max_tree_depth: None, + extension_level: 10, + }; + + let distance_metric: &dyn DistanceMetricBuilder = if let Some(m) = + sub_m.value_of("distance") + { match m { - "euclidean" => euclidean_distance, - "cosine" => cosine_distance, - _ => bail!("Please choose a distance name, between 'euclidean' and 'cosine'."), + "euclidean" => &euclidean_distance, + "cosine" => &cosine_distance, + "extended_isolation_forest" => forest_distance, + _ => bail!("Please choose a distance name, between 'euclidean', 'cosine' and 'extended_isolation_forest'."), } } else { - euclidean_distance + &euclidean_distance }; - let sort = match sub_m.is_present("seed") { + let mut sort = match sub_m.is_present("seed") { false => closest_to_songs, true => song_to_song, }; - if sub_m.is_present("dedup") { - library.queue_from_current_song_custom( + let no_dedup = sub_m.is_present("no-dedup"); + if sub_m.is_present("entire") { + library.queue_from_current_playlist( number_songs, - &distance_metric, - sort, - true, + forest_distance, + &mut sort, + !no_dedup, )?; } else { library.queue_from_current_song_custom( number_songs, - &distance_metric, - sort, - false, + distance_metric, + &mut sort, + !no_dedup, )?; } }