Skip to content

Commit 28e01ae

Browse files
authored
Merge pull request #200 from neriglissar/play-recommendations-for-selected-seed
Add play recommendations for song/artist on pressing 'r'
2 parents 7d3dc07 + c94d7e9 commit 28e01ae

11 files changed

Lines changed: 387 additions & 8 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ rspotify = "0.7.0"
1818
tui = { version = "0.8.0", features = ["crossterm"], default-features = false }
1919
failure = "0.1.6"
2020
serde = { version = "1.0", features = ["derive"] }
21+
serde_json = "1.0"
2122
serde_yaml = "0.8"
2223
dirs = "2.0.2"
2324
clap = "2.33.0"

src/app.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ use rspotify::spotify::{
1111
page::{CursorBasedPage, Page},
1212
playing::PlayHistory,
1313
playlist::{PlaylistTrack, SimplifiedPlaylist},
14+
recommend::Recommendations,
1415
search::{SearchAlbums, SearchArtists, SearchPlaylists, SearchTracks},
1516
track::{FullTrack, SavedTrack, SimplifiedTrack},
1617
user::PrivateUser,
1718
},
1819
senum::{Country, RepeatState},
1920
};
21+
use serde_json::{map::Map, Value};
2022
use std::{
2123
cmp::{max, min},
2224
collections::HashSet,
@@ -143,6 +145,7 @@ pub enum RouteId {
143145
MadeForYou,
144146
Artists,
145147
Podcasts,
148+
Recommendations,
146149
}
147150

148151
pub struct Route {
@@ -158,6 +161,7 @@ pub enum TrackTableContext {
158161
AlbumSearch,
159162
PlaylistSearch,
160163
SavedTracks,
164+
RecommendedTracks,
161165
}
162166

163167
#[derive(Clone, PartialEq, Debug)]
@@ -166,6 +170,12 @@ pub enum AlbumTableContext {
166170
Full,
167171
}
168172

173+
#[derive(Clone, PartialEq, Debug)]
174+
pub enum RecommendationsContext {
175+
Artist,
176+
Song,
177+
}
178+
169179
pub struct SearchResult {
170180
pub albums: Option<SearchAlbums>,
171181
pub artists: Option<SearchArtists>,
@@ -242,6 +252,9 @@ pub struct App {
242252
pub playlist_tracks: Option<Page<PlaylistTrack>>,
243253
pub playlists: Option<Page<SimplifiedPlaylist>>,
244254
pub recently_played: SpotifyResultAndSelectedIndex<Option<CursorBasedPage<PlayHistory>>>,
255+
pub recommended_tracks: Vec<FullTrack>,
256+
pub recommendations_seed: String,
257+
pub recommendations_context: Option<RecommendationsContext>,
245258
pub search_results: SearchResult,
246259
pub selected_album: Option<SelectedAlbum>,
247260
pub selected_album_full: Option<SelectedFullAlbum>,
@@ -293,6 +306,9 @@ impl App {
293306
playlist_offset: 0,
294307
playlist_tracks: None,
295308
playlists: None,
309+
recommended_tracks: vec![],
310+
recommendations_context: None,
311+
recommendations_seed: "".to_string(),
296312
search_results: SearchResult {
297313
hovered_block: SearchResultBlock::SongSearch,
298314
selected_block: SearchResultBlock::Empty,
@@ -479,6 +495,59 @@ impl App {
479495
}
480496
}
481497

498+
pub fn get_recommendations_for_seed(
499+
&mut self,
500+
seed_artists: Option<Vec<String>>,
501+
seed_tracks: Option<Vec<String>>,
502+
first_track: Option<&FullTrack>,
503+
) {
504+
if let (Some(spotify), Some(user)) = (&self.spotify, &self.user.to_owned()) {
505+
let user_country =
506+
Country::from_str(&user.country.to_owned().unwrap_or_else(|| "".to_string()));
507+
let empty_payload: Map<String, Value> = Map::new();
508+
509+
match spotify.recommendations(
510+
seed_artists, // artists
511+
None, // genres
512+
seed_tracks, // tracks
513+
self.large_search_limit, // adjust playlist to screen size
514+
user_country, // country
515+
&empty_payload, // payload
516+
) {
517+
Ok(result) => {
518+
if let Some(mut recommended_tracks) = self.extract_recommended_tracks(&result) {
519+
//custom first track
520+
if let Some(track) = first_track {
521+
recommended_tracks.insert(0, track.clone());
522+
}
523+
self.recommended_tracks = recommended_tracks.clone();
524+
self.set_tracks_to_table(recommended_tracks);
525+
self.track_table.context = Some(TrackTableContext::RecommendedTracks);
526+
527+
if self.get_current_route().id != RouteId::Recommendations {
528+
self.push_navigation_stack(
529+
RouteId::Recommendations,
530+
ActiveBlock::TrackTable,
531+
);
532+
};
533+
}
534+
self.start_recommendations_playback(Some(0));
535+
}
536+
Err(e) => println!("error: {:?}", e),
537+
}
538+
}
539+
}
540+
541+
pub fn get_recommendations_for_trackid(&mut self, id: &str) {
542+
if let Some(track) = self.get_fulltrack_from_id(id) {
543+
let track_id_list: Option<Vec<String>> = match &track.id {
544+
Some(id) => Some(vec![id.to_string()]),
545+
None => None,
546+
};
547+
self.get_recommendations_for_seed(None, track_id_list, Some(&track));
548+
}
549+
}
550+
482551
fn change_volume(&mut self, volume_percent: u8) {
483552
if let (Some(spotify), Some(device_id), Some(context)) = (
484553
&self.spotify,
@@ -564,6 +633,19 @@ impl App {
564633
}
565634
}
566635

636+
pub fn start_recommendations_playback(&mut self, offset: Option<usize>) {
637+
self.start_playback(
638+
None,
639+
Some(
640+
self.recommended_tracks
641+
.iter()
642+
.map(|x| x.uri.clone())
643+
.collect::<Vec<String>>(),
644+
),
645+
offset,
646+
);
647+
}
648+
567649
pub fn start_playback(
568650
&mut self,
569651
context_uri: Option<String>,
@@ -720,6 +802,42 @@ impl App {
720802
);
721803
}
722804

805+
fn extract_recommended_tracks(
806+
&self,
807+
recommendations: &Recommendations,
808+
) -> Option<Vec<FullTrack>> {
809+
if let Some(spotify) = &self.spotify {
810+
let tracks = recommendations
811+
.clone()
812+
.tracks
813+
.into_iter()
814+
.map(|item| item.uri)
815+
.collect::<Vec<String>>();
816+
if let Ok(result) =
817+
spotify.tracks(tracks.iter().map(|x| &x[..]).collect::<Vec<&str>>(), None)
818+
{
819+
return Some(result.tracks);
820+
}
821+
}
822+
823+
None
824+
}
825+
826+
fn get_fulltrack_from_id(&self, id: &str) -> Option<FullTrack> {
827+
if let Some(spotify) = &self.spotify {
828+
match spotify.track(id) {
829+
Ok(track) => {
830+
return Some(track);
831+
}
832+
Err(_e) => {
833+
return None;
834+
}
835+
};
836+
}
837+
838+
None
839+
}
840+
723841
pub fn set_tracks_to_table(&mut self, tracks: Vec<FullTrack>) {
724842
self.track_table.tracks = tracks.clone();
725843

src/handlers/album_tracks.rs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use super::common_key_events;
22
use crate::{
3-
app::{AlbumTableContext, App},
3+
app::{AlbumTableContext, App, RecommendationsContext},
44
event::Key,
55
};
66

@@ -106,10 +106,52 @@ pub fn handler(key: Key, app: &mut App) {
106106
};
107107
}
108108
},
109+
//recommended playlist based on selected track
110+
Key::Char('r') => {
111+
handle_recommended_tracks(app);
112+
}
109113
_ => {}
110114
};
111115
}
112116

117+
fn handle_recommended_tracks(app: &mut App) {
118+
match app.album_table_context {
119+
AlbumTableContext::Full => {
120+
if let Some(albums) = &app.library.clone().saved_albums.get_results(None) {
121+
if let Some(selected_album) = albums.items.get(app.album_list_index) {
122+
if let Some(track) = &selected_album
123+
.album
124+
.tracks
125+
.items
126+
.get(app.saved_album_tracks_index)
127+
{
128+
if let Some(id) = &track.id {
129+
app.recommendations_context = Some(RecommendationsContext::Song);
130+
app.recommendations_seed = track.name.clone();
131+
app.get_recommendations_for_trackid(&id);
132+
}
133+
}
134+
}
135+
}
136+
}
137+
AlbumTableContext::Simplified => {
138+
if let Some(selected_album) = &app.selected_album.clone() {
139+
if let Some(track) = &selected_album
140+
.tracks
141+
.items
142+
.get(selected_album.selected_index)
143+
{
144+
if let Some(id) = &track.id {
145+
app.recommendations_context = Some(RecommendationsContext::Song);
146+
app.recommendations_seed = track.name.clone();
147+
app.get_recommendations_for_trackid(&id);
148+
}
149+
}
150+
};
151+
}
152+
}
153+
}
154+
113155
#[cfg(test)]
114156
mod tests {
115157
use super::*;

src/handlers/artist.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::common_key_events;
2-
use crate::app::{App, ArtistBlock, TrackTableContext};
2+
use crate::app::{App, ArtistBlock, RecommendationsContext, TrackTableContext};
33
use crate::event::Key;
44

55
fn handle_down_press_on_selected_block(app: &mut App) {
@@ -94,6 +94,37 @@ fn handle_up_press_on_hovered_block(app: &mut App) {
9494
}
9595
}
9696

97+
fn handle_recommend_event_on_selected_block(app: &mut App) {
98+
//recommendations.
99+
if let Some(artist) = &mut app.artist.clone() {
100+
match artist.artist_selected_block {
101+
ArtistBlock::TopTracks => {
102+
let selected_index = artist.selected_top_track_index;
103+
if let Some(track) = artist.top_tracks.get(selected_index) {
104+
let track_id_list: Option<Vec<String>> = match &track.id {
105+
Some(id) => Some(vec![id.to_string()]),
106+
None => None,
107+
};
108+
app.recommendations_context = Some(RecommendationsContext::Song);
109+
app.recommendations_seed = track.name.clone();
110+
app.get_recommendations_for_seed(None, track_id_list, Some(track));
111+
}
112+
}
113+
ArtistBlock::RelatedArtists => {
114+
let selected_index = artist.selected_related_artist_index;
115+
let artist_id = &artist.related_artists[selected_index].id;
116+
let artist_name = &artist.related_artists[selected_index].name;
117+
let artist_id_list: Option<Vec<String>> = Some(vec![artist_id.clone()]);
118+
119+
app.recommendations_context = Some(RecommendationsContext::Artist);
120+
app.recommendations_seed = artist_name.clone();
121+
app.get_recommendations_for_seed(artist_id_list, None, None);
122+
}
123+
_ => {}
124+
}
125+
}
126+
}
127+
97128
fn handle_enter_event_on_selected_block(app: &mut App) {
98129
if let Some(artist) = &mut app.artist.clone() {
99130
match artist.artist_selected_block {
@@ -185,6 +216,11 @@ pub fn handler(key: Key, app: &mut App) {
185216
handle_enter_event_on_hovered_block(app);
186217
}
187218
}
219+
Key::Char('r') => {
220+
if artist.artist_selected_block != ArtistBlock::Empty {
221+
handle_recommend_event_on_selected_block(app);
222+
}
223+
}
188224
_ => {}
189225
};
190226
}

src/handlers/artists.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use super::common_key_events;
22
use crate::{
3-
app::{ActiveBlock, App, RouteId},
3+
app::{ActiveBlock, App, RecommendationsContext, RouteId},
44
event::Key,
55
};
66

@@ -32,6 +32,18 @@ pub fn handler(key: Key, app: &mut App) {
3232
app.push_navigation_stack(RouteId::Artist, ActiveBlock::ArtistBlock);
3333
}
3434
Key::Char('D') => app.user_unfollow_artists(),
35+
Key::Char('r') => {
36+
let artists = app.artists.to_owned();
37+
let artist = artists.get(app.artists_list_index);
38+
if let Some(artist) = artist {
39+
let artist_name = artist.name.clone();
40+
let artist_id_list: Option<Vec<String>> = Some(vec![artist.id.clone()]);
41+
42+
app.recommendations_context = Some(RecommendationsContext::Artist);
43+
app.recommendations_seed = artist_name;
44+
app.get_recommendations_for_seed(artist_id_list, None, None);
45+
}
46+
}
3547
_ => {}
3648
}
3749
}

src/handlers/common_key_events.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ pub fn handle_right_event(app: &mut App) {
8383
Some(ActiveBlock::Podcasts),
8484
);
8585
}
86+
RouteId::Recommendations => {
87+
app.set_current_route_state(
88+
Some(ActiveBlock::TrackTable),
89+
Some(ActiveBlock::TrackTable),
90+
);
91+
}
8692
RouteId::AlbumList => {
8793
app.set_current_route_state(
8894
Some(ActiveBlock::AlbumList),

src/handlers/recently_played.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::{super::app::App, common_key_events};
2-
use crate::event::Key;
2+
use crate::{app::RecommendationsContext, event::Key};
33

44
pub fn handler(key: Key, app: &mut App) {
55
match key {
@@ -44,6 +44,20 @@ pub fn handler(key: Key, app: &mut App) {
4444
app.start_playback(None, Some(track_uris), Some(app.recently_played.index));
4545
};
4646
}
47+
Key::Char('r') => {
48+
if let Some(recently_played_result) = &app.recently_played.result.clone() {
49+
let selected_track_history_item =
50+
recently_played_result.items.get(app.recently_played.index);
51+
52+
if let Some(item) = selected_track_history_item {
53+
if let Some(id) = &item.track.id {
54+
app.recommendations_context = Some(RecommendationsContext::Song);
55+
app.recommendations_seed = item.track.name.clone();
56+
app.get_recommendations_for_trackid(&id);
57+
}
58+
}
59+
}
60+
}
4761
_ => {}
4862
};
4963
}

0 commit comments

Comments
 (0)