99use anyhow:: { bail, Context , Result } ;
1010use bliss_audio:: library:: { AppConfigTrait , BaseConfig , Library , LibrarySong } ;
1111use 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 } ;
1615use clap:: { App , Arg , ArgMatches , SubCommand } ;
16+ use extended_isolation_forest:: ForestOptions ;
1717#[ cfg( not( test) ) ]
1818use log:: warn;
1919use 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) ]
819840mod 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