Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ rspotify = "0.7.0"
tui = { version = "0.8.0", features = ["crossterm"], default-features = false }
failure = "0.1.6"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.8"
dirs = "2.0.2"
clap = "2.33.0"
Expand Down
118 changes: 118 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ use rspotify::spotify::{
page::{CursorBasedPage, Page},
playing::PlayHistory,
playlist::{PlaylistTrack, SimplifiedPlaylist},
recommend::Recommendations,
search::{SearchAlbums, SearchArtists, SearchPlaylists, SearchTracks},
track::{FullTrack, SavedTrack, SimplifiedTrack},
user::PrivateUser,
},
senum::{Country, RepeatState},
};
use serde_json::{map::Map, Value};
use std::{
cmp::{max, min},
collections::HashSet,
Expand Down Expand Up @@ -143,6 +145,7 @@ pub enum RouteId {
MadeForYou,
Artists,
Podcasts,
Recommendations,
}

pub struct Route {
Expand All @@ -158,6 +161,7 @@ pub enum TrackTableContext {
AlbumSearch,
PlaylistSearch,
SavedTracks,
RecommendedTracks,
}

#[derive(Clone, PartialEq, Debug)]
Expand All @@ -166,6 +170,12 @@ pub enum AlbumTableContext {
Full,
}

#[derive(Clone, PartialEq, Debug)]
pub enum RecommendationsContext {
Artist,
Song,
}

pub struct SearchResult {
pub albums: Option<SearchAlbums>,
pub artists: Option<SearchArtists>,
Expand Down Expand Up @@ -242,6 +252,9 @@ pub struct App {
pub playlist_tracks: Vec<PlaylistTrack>,
pub playlists: Option<Page<SimplifiedPlaylist>>,
pub recently_played: SpotifyResultAndSelectedIndex<Option<CursorBasedPage<PlayHistory>>>,
pub recommended_tracks: Vec<FullTrack>,
pub recommendations_seed: String,
pub recommendations_context: Option<RecommendationsContext>,
pub search_results: SearchResult,
pub selected_album: Option<SelectedAlbum>,
pub selected_album_full: Option<SelectedFullAlbum>,
Expand Down Expand Up @@ -293,6 +306,9 @@ impl App {
playlist_offset: 0,
playlist_tracks: vec![],
playlists: None,
recommended_tracks: vec![],
recommendations_context: None,
recommendations_seed: "".to_string(),
search_results: SearchResult {
hovered_block: SearchResultBlock::SongSearch,
selected_block: SearchResultBlock::Empty,
Expand Down Expand Up @@ -479,6 +495,59 @@ impl App {
}
}

pub fn get_recommendations_for_seed(
&mut self,
seed_artists: Option<Vec<String>>,
seed_tracks: Option<Vec<String>>,
first_track: Option<&FullTrack>,
) {
if let (Some(spotify), Some(user)) = (&self.spotify, &self.user.to_owned()) {
let user_country =
Country::from_str(&user.country.to_owned().unwrap_or_else(|| "".to_string()));
let empty_payload: Map<String, Value> = Map::new();

match spotify.recommendations(
seed_artists, // artists
None, // genres
seed_tracks, // tracks
self.large_search_limit, // adjust playlist to screen size
user_country, // country
&empty_payload, // payload
) {
Ok(result) => {
if let Some(mut recommended_tracks) = self.extract_recommended_tracks(&result) {
//custom first track
if let Some(track) = first_track {
recommended_tracks.insert(0, track.clone());
}
self.recommended_tracks = recommended_tracks.clone();
self.set_tracks_to_table(recommended_tracks);
self.track_table.context = Some(TrackTableContext::RecommendedTracks);

if self.get_current_route().id != RouteId::Recommendations {
self.push_navigation_stack(
RouteId::Recommendations,
ActiveBlock::TrackTable,
);
};
}
self.start_recommendations_playback(Some(0));
}
Err(e) => println!("error: {:?}", e),
}
}
}

pub fn get_recommendations_for_trackid(&mut self, id: &str) {
if let Some(track) = self.get_fulltrack_from_id(id) {
let track_id_list: Option<Vec<String>> = match &track.id {
Some(id) => Some(vec![id.to_string()]),
None => None,
};
self.get_recommendations_for_seed(None, track_id_list, Some(&track));
}
}

fn change_volume(&mut self, volume_percent: u8) {
if let (Some(spotify), Some(device_id), Some(context)) = (
&self.spotify,
Expand Down Expand Up @@ -560,6 +629,19 @@ impl App {
}
}

pub fn start_recommendations_playback(&mut self, offset: Option<usize>) {
self.start_playback(
None,
Some(
self.recommended_tracks
.iter()
.map(|x| x.uri.clone())
.collect::<Vec<String>>(),
),
offset,
);
}

pub fn start_playback(
&mut self,
context_uri: Option<String>,
Expand Down Expand Up @@ -716,6 +798,42 @@ impl App {
);
}

fn extract_recommended_tracks(
&self,
recommendations: &Recommendations,
) -> Option<Vec<FullTrack>> {
if let Some(spotify) = &self.spotify {
let tracks = recommendations
.clone()
.tracks
.into_iter()
.map(|item| item.uri)
.collect::<Vec<String>>();
if let Ok(result) =
spotify.tracks(tracks.iter().map(|x| &x[..]).collect::<Vec<&str>>(), None)
{
return Some(result.tracks);
}
}

None
}

fn get_fulltrack_from_id(&self, id: &str) -> Option<FullTrack> {
if let Some(spotify) = &self.spotify {
match spotify.track(id) {
Ok(track) => {
return Some(track);
}
Err(_e) => {
return None;
}
};
}

None
}

pub fn set_tracks_to_table(&mut self, tracks: Vec<FullTrack>) {
self.track_table.tracks = tracks.clone();

Expand Down
44 changes: 43 additions & 1 deletion src/handlers/album_tracks.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::common_key_events;
use crate::{
app::{AlbumTableContext, App},
app::{AlbumTableContext, App, RecommendationsContext},
event::Key,
};

Expand Down Expand Up @@ -106,10 +106,52 @@ pub fn handler(key: Key, app: &mut App) {
};
}
},
//recommended playlist based on selected track
Key::Char('r') => {
handle_recommended_tracks(app);
}
_ => {}
};
}

fn handle_recommended_tracks(app: &mut App) {
match app.album_table_context {
AlbumTableContext::Full => {
if let Some(albums) = &app.library.clone().saved_albums.get_results(None) {
if let Some(selected_album) = albums.items.get(app.album_list_index) {
if let Some(track) = &selected_album
.album
.tracks
.items
.get(app.saved_album_tracks_index)
{
if let Some(id) = &track.id {
app.recommendations_context = Some(RecommendationsContext::Song);
app.recommendations_seed = track.name.clone();
app.get_recommendations_for_trackid(&id);
}
}
}
}
}
AlbumTableContext::Simplified => {
if let Some(selected_album) = &app.selected_album.clone() {
if let Some(track) = &selected_album
.tracks
.items
.get(selected_album.selected_index)
{
if let Some(id) = &track.id {
app.recommendations_context = Some(RecommendationsContext::Song);
app.recommendations_seed = track.name.clone();
app.get_recommendations_for_trackid(&id);
}
}
};
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
38 changes: 37 additions & 1 deletion src/handlers/artist.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::common_key_events;
use crate::app::{App, ArtistBlock, TrackTableContext};
use crate::app::{App, ArtistBlock, RecommendationsContext, TrackTableContext};
use crate::event::Key;

fn handle_down_press_on_selected_block(app: &mut App) {
Expand Down Expand Up @@ -94,6 +94,37 @@ fn handle_up_press_on_hovered_block(app: &mut App) {
}
}

fn handle_recommend_event_on_selected_block(app: &mut App) {
//recommendations.
if let Some(artist) = &mut app.artist.clone() {
match artist.artist_selected_block {
ArtistBlock::TopTracks => {
let selected_index = artist.selected_top_track_index;
if let Some(track) = artist.top_tracks.get(selected_index) {
let track_id_list: Option<Vec<String>> = match &track.id {
Some(id) => Some(vec![id.to_string()]),
None => None,
};
app.recommendations_context = Some(RecommendationsContext::Song);
app.recommendations_seed = track.name.clone();
app.get_recommendations_for_seed(None, track_id_list, Some(track));
}
}
ArtistBlock::RelatedArtists => {
let selected_index = artist.selected_related_artist_index;
let artist_id = &artist.related_artists[selected_index].id;
let artist_name = &artist.related_artists[selected_index].name;
let artist_id_list: Option<Vec<String>> = Some(vec![artist_id.clone()]);

app.recommendations_context = Some(RecommendationsContext::Artist);
app.recommendations_seed = artist_name.clone();
app.get_recommendations_for_seed(artist_id_list, None, None);
}
_ => {}
}
}
}

fn handle_enter_event_on_selected_block(app: &mut App) {
if let Some(artist) = &mut app.artist.clone() {
match artist.artist_selected_block {
Expand Down Expand Up @@ -185,6 +216,11 @@ pub fn handler(key: Key, app: &mut App) {
handle_enter_event_on_hovered_block(app);
}
}
Key::Char('r') => {
if artist.artist_selected_block != ArtistBlock::Empty {
handle_recommend_event_on_selected_block(app);
}
}
_ => {}
};
}
Expand Down
14 changes: 13 additions & 1 deletion src/handlers/artists.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::common_key_events;
use crate::{
app::{ActiveBlock, App, RouteId},
app::{ActiveBlock, App, RecommendationsContext, RouteId},
event::Key,
};

Expand Down Expand Up @@ -32,6 +32,18 @@ pub fn handler(key: Key, app: &mut App) {
app.push_navigation_stack(RouteId::Artist, ActiveBlock::ArtistBlock);
}
Key::Char('D') => app.user_unfollow_artists(),
Key::Char('r') => {
let artists = app.artists.to_owned();
let artist = artists.get(app.artists_list_index);
if let Some(artist) = artist {
let artist_name = artist.name.clone();
let artist_id_list: Option<Vec<String>> = Some(vec![artist.id.clone()]);

app.recommendations_context = Some(RecommendationsContext::Artist);
app.recommendations_seed = artist_name;
app.get_recommendations_for_seed(artist_id_list, None, None);
}
}
_ => {}
}
}
6 changes: 6 additions & 0 deletions src/handlers/common_key_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ pub fn handle_right_event(app: &mut App) {
Some(ActiveBlock::Podcasts),
);
}
RouteId::Recommendations => {
app.set_current_route_state(
Some(ActiveBlock::TrackTable),
Some(ActiveBlock::TrackTable),
);
}
RouteId::AlbumList => {
app.set_current_route_state(
Some(ActiveBlock::AlbumList),
Expand Down
16 changes: 15 additions & 1 deletion src/handlers/recently_played.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::{super::app::App, common_key_events};
use crate::event::Key;
use crate::{app::RecommendationsContext, event::Key};

pub fn handler(key: Key, app: &mut App) {
match key {
Expand Down Expand Up @@ -44,6 +44,20 @@ pub fn handler(key: Key, app: &mut App) {
app.start_playback(None, Some(track_uris), Some(app.recently_played.index));
};
}
Key::Char('r') => {
if let Some(recently_played_result) = &app.recently_played.result.clone() {
let selected_track_history_item =
recently_played_result.items.get(app.recently_played.index);

if let Some(item) = selected_track_history_item {
if let Some(id) = &item.track.id {
app.recommendations_context = Some(RecommendationsContext::Song);
app.recommendations_seed = item.track.name.clone();
app.get_recommendations_for_trackid(&id);
}
}
}
}
_ => {}
};
}
Expand Down
Loading