Skip to content

Commit 080bc25

Browse files
committed
Generate playlists from multiple songs
1 parent 8b63ebf commit 080bc25

File tree

3 files changed

+125
-78
lines changed

3 files changed

+125
-78
lines changed

Cargo.lock

Lines changed: 31 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ default = ["bliss-audio/library"]
1717
rpi = ["bliss-audio/rpi"]
1818

1919
[dependencies]
20-
bliss-audio = "0.6.11"
20+
bliss-audio = { "path"="../bliss-rs" }
2121
mpd = "0.0.12"
2222
dirs = "3.0.1"
2323
tempdir = "0.3.7"
@@ -30,3 +30,4 @@ noisy_float = "0.2.0"
3030
termion = "1.5.6"
3131
serde = "1.0"
3232
pretty_assertions = "1.2.1"
33+
extended-isolation-forest = "0.2.3"

src/main.rs

Lines changed: 92 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
use anyhow::{bail, Context, Result};
1010
use bliss_audio::library::{AppConfigTrait, BaseConfig, Library, LibrarySong};
1111
use bliss_audio::playlist::{
12-
closest_to_first_song_by_key, cosine_distance, euclidean_distance, song_to_song_by_key,
13-
DistanceMetric,
12+
closest_to_songs, cosine_distance, euclidean_distance, song_to_song, DistanceMetricBuilder,
1413
};
15-
use bliss_audio::{BlissError, BlissResult, Song};
14+
use bliss_audio::{BlissError, BlissResult};
1615
use clap::{App, Arg, ArgMatches, SubCommand};
16+
use extended_isolation_forest::ForestOptions;
1717
#[cfg(not(test))]
1818
use log::warn;
1919
use mpd::search::{Query, Term};
@@ -334,42 +334,57 @@ impl MPDLibrary {
334334
Ok(())
335335
}
336336

337-
fn queue_from_current_song_custom<F, G>(
337+
// Make a playlist from the current playlist
338+
fn queue_from_current_playlist<F>(
338339
&self,
339340
number_songs: usize,
340-
distance: G,
341-
sort_by: F,
341+
distance: &dyn DistanceMetricBuilder,
342+
sort_by: &mut F,
342343
dedup: bool,
344+
current_song_only: bool,
343345
) -> Result<()>
344346
where
345-
F: FnMut(&LibrarySong<()>, &mut Vec<LibrarySong<()>>, G, fn(&LibrarySong<()>) -> Song),
346-
G: DistanceMetric + Copy,
347+
F: FnMut(&[LibrarySong<()>], &mut [LibrarySong<()>], &dyn DistanceMetricBuilder),
347348
{
348349
let mut mpd_conn = self.mpd_conn.lock().unwrap();
349350
mpd_conn.random(false)?;
350-
let mpd_song = match mpd_conn.currentsong()? {
351-
Some(s) => s,
352-
None => bail!("No song is currently playing. Add a song to start the playlist from, and try again."),
351+
let songs = if current_song_only {
352+
match mpd_conn.currentsong()? {
353+
Some(s) => {
354+
let current_pos = s.place.unwrap().pos;
355+
mpd_conn.delete(0..current_pos)?;
356+
if mpd_conn.queue()?.len() > 1 {
357+
mpd_conn.delete(1..)?;
358+
}
359+
vec![s]
360+
}
361+
None => bail!("No song is currently playing. Add a song to start the playlist from, and try again."),
362+
}
363+
} else {
364+
mpd_conn.queue()?
353365
};
354-
let path = self.mpd_to_bliss_path(&mpd_song)?;
355-
356-
let playlist = self.library.playlist_from_custom(
357-
&path.to_string_lossy().clone(),
358-
number_songs,
359-
distance,
360-
sort_by,
361-
dedup,
362-
)?;
363-
let current_pos = mpd_song.place.unwrap().pos;
364-
mpd_conn.delete(0..current_pos)?;
365-
if mpd_conn.queue()?.len() > 1 {
366-
mpd_conn.delete(1..)?;
367-
}
368-
369-
for song in &playlist[1..] {
366+
let paths = songs
367+
.iter()
368+
.map(|s| self.mpd_to_bliss_path(&s))
369+
.collect::<Result<Vec<_>, _>>()?;
370+
let paths = paths
371+
.iter()
372+
.map(|s| s.to_string_lossy())
373+
.collect::<Vec<_>>();
374+
let paths = paths.iter().map(|s| &**s).collect::<Vec<&str>>();
375+
let playlist =
376+
self.library
377+
.playlist_from_custom(&paths, number_songs, distance, sort_by, dedup)?;
378+
379+
// If this playlist is generated from a single song, the closest song will be itself.
380+
// Remove it so that it won't show up twice in a row if deduplication is not used.
381+
let playlist_from_idx =if current_song_only { 1 } else { 0 };
382+
383+
for song in &playlist[playlist_from_idx..] {
370384
let mpd_song = self.bliss_song_to_mpd(song)?;
371385
mpd_conn.push(mpd_song)?;
372386
}
387+
373388
Ok(())
374389
}
375390

@@ -500,8 +515,11 @@ impl MPDLibrary {
500515
.join("\n")
501516
);
502517
}
503-
songs
504-
.sort_by_cached_key(|song| n32(current_song.bliss_song.distance(&song.bliss_song)));
518+
let distance =
519+
(&euclidean_distance).build(&[current_song.bliss_song.analysis.as_arr1()]);
520+
songs.sort_by_cached_key(|song| {
521+
n32(distance.distance(&song.bliss_song.analysis.as_arr1()))
522+
});
505523
// TODO put a proper dedup here
506524
//dedup_playlist(&mut songs, None);
507525
for (i, song) in songs[1..number_choices + 1].iter().enumerate() {
@@ -657,19 +675,23 @@ fn main() -> Result<()> {
657675
.long("distance")
658676
.value_name("distance metric")
659677
.help(
660-
"Choose the distance metric used to make the playlist. Default is 'euclidean',\
661-
other option is 'cosine'"
678+
"Choose the distance metric used to make the playlist. Default is 'extended_isolation_forest',\
679+
other options are 'cosine', and 'euclidean'"
662680
)
663-
.default_value("euclidean")
681+
.default_value("extended_isolation_forest")
664682
)
665-
.arg(Arg::with_name("seed")
666-
.long("seed-song")
683+
.arg(Arg::with_name("sort")
684+
.long("sort")
685+
.value_name("sort function")
667686
.help(
668-
"Instead of making a playlist of only the closest song to the current song,\
669-
make a playlist that queues the closest song to the first song, then
670-
the closest to the second song, etc. Can take some time to build."
687+
"Choose the way the playlist will be sorted. Default is 'closest_to_songs',\
688+
which will sort songs by their distance to the current queue/or current song,\
689+
in descending order. The alternative is song_to_song, which will first select\
690+
the closest match. The second song will be the closest song to the first\
691+
selection, etc., so that each song is as close as possible to the previous\
692+
song. Can take some time to build."
671693
)
672-
.takes_value(false)
694+
.takes_value(true)
673695
)
674696
.arg(Arg::with_name("dedup")
675697
.long("deduplicate-songs")
@@ -684,6 +706,11 @@ fn main() -> Result<()> {
684706
.help("Make a playlist of similar albums from the current album.")
685707
.takes_value(false)
686708
)
709+
.arg(Arg::with_name("from-queue")
710+
.long("from-queue")
711+
.help("Base the playlist on the entire queue, not just the currently playing song.")
712+
.takes_value(false)
713+
)
687714
)
688715
.subcommand(
689716
SubCommand::with_name("interactive-playlist")
@@ -772,35 +799,29 @@ fn main() -> Result<()> {
772799
if sub_m.is_present("album") {
773800
library.queue_from_current_album(number_songs)?;
774801
} else {
775-
let distance_metric = if let Some(m) = sub_m.value_of("distance") {
776-
match m {
777-
"euclidean" => euclidean_distance,
778-
"cosine" => cosine_distance,
779-
_ => bail!("Please choose a distance name, between 'euclidean' and 'cosine'."),
780-
}
781-
} else {
782-
euclidean_distance
802+
let mut sort = match sub_m.value_of("sort") {
803+
Some("song_to_song") => song_to_song,
804+
Some("closest_to_songs") => closest_to_songs,
805+
Some(_) => bail!(
806+
"Please choose a sort function from 'song_to_song' and 'closest_to_songs'"
807+
),
808+
None => closest_to_songs,
783809
};
784810

785-
let sort = match sub_m.is_present("seed") {
786-
false => closest_to_first_song_by_key,
787-
true => song_to_song_by_key,
811+
let default_forest_options = ForestOptions::default();
812+
let distance: &dyn DistanceMetricBuilder = match sub_m.value_of("distance") {
813+
Some("extended_isolation_forest") | None => &default_forest_options,
814+
Some("euclidean_distance") => &euclidean_distance,
815+
Some("cosine_distance") => &cosine_distance,
816+
Some(_) => bail!("Please choose a distance name, between 'extended_isolation_forest', 'euclidean' and 'cosine'.")
788817
};
789-
if sub_m.is_present("dedup") {
790-
library.queue_from_current_song_custom(
791-
number_songs,
792-
distance_metric,
793-
sort,
794-
true,
795-
)?;
796-
} else {
797-
library.queue_from_current_song_custom(
798-
number_songs,
799-
distance_metric,
800-
sort,
801-
false,
802-
)?;
803-
}
818+
library.queue_from_current_playlist(
819+
number_songs,
820+
distance,
821+
&mut sort,
822+
sub_m.is_present("dedup"),
823+
!sub_m.is_present("from-queue"),
824+
)?;
804825
}
805826
} else if let Some(sub_m) = matches.subcommand_matches("interactive-playlist") {
806827
let number_choices: usize = sub_m.value_of("choices").unwrap_or("3").parse()?;
@@ -818,7 +839,7 @@ fn main() -> Result<()> {
818839
#[cfg(test)]
819840
mod test {
820841
use super::*;
821-
use bliss_audio::Analysis;
842+
use bliss_audio::{Analysis, Song};
822843
use mpd::error::Result;
823844
use mpd::song::{Id, QueuePlace, Song as MPDSong};
824845
use pretty_assertions::assert_eq;
@@ -992,7 +1013,7 @@ mod test {
9921013
.unwrap();
9931014
}
9941015
assert_eq!(
995-
library.queue_from_current_song_custom(20, euclidean_distance, closest_to_first_song_by_key, true).unwrap_err().to_string(),
1016+
library.queue_from_current_playlist(20, &euclidean_distance, &mut closest_to_songs, true, true).unwrap_err().to_string(),
9961017
String::from("No song is currently playing. Add a song to start the playlist from, and try again."),
9971018
);
9981019
}
@@ -1029,10 +1050,11 @@ mod test {
10291050

10301051
assert_eq!(
10311052
library
1032-
.queue_from_current_song_custom(
1053+
.queue_from_current_playlist(
10331054
20,
1034-
euclidean_distance,
1035-
closest_to_first_song_by_key,
1055+
&euclidean_distance,
1056+
&mut closest_to_songs,
1057+
true,
10361058
true
10371059
)
10381060
.unwrap_err()
@@ -1155,12 +1177,7 @@ mod test {
11551177
.unwrap();
11561178
}
11571179
library
1158-
.queue_from_current_song_custom(
1159-
20,
1160-
euclidean_distance,
1161-
closest_to_first_song_by_key,
1162-
false,
1163-
)
1180+
.queue_from_current_playlist(20, &euclidean_distance, &mut closest_to_songs, false, true)
11641181
.unwrap();
11651182

11661183
let playlist = library

0 commit comments

Comments
 (0)