diff --git a/src/mpris.rs b/src/mpris.rs index b01cde2..5b07d60 100644 --- a/src/mpris.rs +++ b/src/mpris.rs @@ -65,7 +65,7 @@ fn get_metadata(playable: Option) -> Metadata { Variant(Box::new( playable .and_then(|p| p.track()) - .map(|t| t.album) + .map(|t| t.album.unwrap_or_default()) .unwrap_or_default(), )), ); diff --git a/src/spotify.rs b/src/spotify.rs index 16611e6..3f89da3 100644 --- a/src/spotify.rs +++ b/src/spotify.rs @@ -23,7 +23,7 @@ use rspotify::model::track::{FullTrack, SavedTrack, SimplifiedTrack}; use rspotify::model::user::PrivateUser; use rspotify::senum::SearchType; -use serde_json::json; +use serde_json::{json, Map}; use failure::Error; @@ -59,6 +59,7 @@ use crate::events::{Event, EventManager}; use crate::playable::Playable; use crate::queue; use crate::track::Track; +use rspotify::model::recommend::Recommendations; use rspotify::model::show::{FullEpisode, FullShow, Show, SimplifiedEpisode}; pub const VOLUME_PERCENT: u16 = ((u16::max_value() as f64) * 1.0 / 100.0) as u16; @@ -686,6 +687,24 @@ impl Spotify { self.api_with_retry(|api| api.get_an_episode(episode_id.to_string(), None)) } + pub fn recommentations( + &self, + seed_artists: Option>, + seed_genres: Option>, + seed_tracks: Option>, + ) -> Option { + self.api_with_retry(|api| { + api.recommendations( + seed_artists.clone(), + seed_genres.clone(), + seed_tracks.clone(), + 100, + None, + &Map::new(), + ) + }) + } + pub fn search( &self, searchtype: SearchType, diff --git a/src/track.rs b/src/track.rs index 266b914..2ce1a08 100644 --- a/src/track.rs +++ b/src/track.rs @@ -1,5 +1,5 @@ use std::fmt; -use std::sync::Arc; +use std::sync::{Arc, RwLock}; use chrono::{DateTime, Utc}; use rspotify::model::album::FullAlbum; @@ -10,7 +10,8 @@ use crate::artist::Artist; use crate::library::Library; use crate::playable::Playable; use crate::queue::Queue; -use crate::traits::{ListItem, ViewExt}; +use crate::traits::{IntoBoxedViewExt, ListItem, ViewExt}; +use crate::ui::listview::ListView; #[derive(Clone, Deserialize, Serialize)] pub struct Track { @@ -22,7 +23,7 @@ pub struct Track { pub duration: u32, pub artists: Vec, pub artist_ids: Vec, - pub album: String, + pub album: Option, pub album_id: Option, pub album_artists: Vec, pub cover_url: Option, @@ -58,7 +59,7 @@ impl Track { duration: track.duration_ms, artists, artist_ids, - album: album.name.clone(), + album: Some(album.name.clone()), album_id: Some(album.id.clone()), album_artists, cover_url: album.images.get(0).map(|img| img.url.clone()), @@ -74,6 +75,39 @@ impl Track { } } +impl From<&SimplifiedTrack> for Track { + fn from(track: &SimplifiedTrack) -> Self { + let artists = track + .artists + .iter() + .map(|ref artist| artist.name.clone()) + .collect::>(); + let artist_ids = track + .artists + .iter() + .filter(|a| a.id.is_some()) + .map(|ref artist| artist.id.clone().unwrap()) + .collect::>(); + + Self { + id: track.id.clone(), + uri: track.uri.clone(), + title: track.name.clone(), + track_number: track.track_number, + disc_number: track.disc_number, + duration: track.duration_ms, + artists, + artist_ids, + album: None, + album_id: None, + album_artists: Vec::new(), + cover_url: None, + url: track.uri.clone(), + added_at: None, + } + } +} + impl From<&FullTrack> for Track { fn from(track: &FullTrack) -> Self { let artists = track @@ -103,7 +137,7 @@ impl From<&FullTrack> for Track { duration: track.duration_ms, artists, artist_ids, - album: track.album.name.clone(), + album: Some(track.album.name.clone()), album_id: track.album.id.clone(), album_artists, cover_url: track.album.images.get(0).map(|img| img.url.clone()), @@ -155,7 +189,7 @@ impl ListItem for Track { fn display_center(&self, library: Arc) -> String { if library.cfg.values().album_column.unwrap_or(true) { - self.album.to_string() + self.album.clone().unwrap_or_default() } else { "".to_string() } @@ -207,6 +241,32 @@ impl ListItem for Track { None } + fn open_recommentations( + &self, + queue: Arc, + library: Arc, + ) -> Option> { + let spotify = queue.get_spotify(); + + let recommendations: Option> = if let Some(id) = &self.id { + spotify + .recommentations(None, None, Some(vec![id.clone()])) + .map(|r| r.tracks) + .map(|tracks| tracks.iter().map(Track::from).collect()) + } else { + None + }; + + recommendations.map(|tracks| { + ListView::new( + Arc::new(RwLock::new(tracks)), + queue.clone(), + library.clone(), + ) + .as_boxed_view_ext() + }) + } + fn share_url(&self) -> Option { self.id .clone() diff --git a/src/traits.rs b/src/traits.rs index a32a67a..e6d9794 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -26,6 +26,13 @@ pub trait ListItem: Sync + Send + 'static { fn save(&mut self, library: Arc); fn unsave(&mut self, library: Arc); fn open(&self, queue: Arc, library: Arc) -> Option>; + fn open_recommentations( + &self, + _queue: Arc, + _library: Arc, + ) -> Option> { + None + } fn share_url(&self) -> Option; fn album(&self, _queue: Arc) -> Option { diff --git a/src/ui/contextmenu.rs b/src/ui/contextmenu.rs index abdbb7b..23855ee 100644 --- a/src/ui/contextmenu.rs +++ b/src/ui/contextmenu.rs @@ -24,6 +24,7 @@ enum ContextMenuAction { ShowItem(Box), ShareUrl(String), AddToPlaylist(Box), + ShowRecommentations(Box), } impl ContextMenu { @@ -62,8 +63,12 @@ impl ContextMenu { if let Some(t) = item.track() { content.add_item( "Add to playlist", - ContextMenuAction::AddToPlaylist(Box::new(t)), - ) + ContextMenuAction::AddToPlaylist(Box::new(t.clone())), + ); + content.add_item( + "Similar tracks", + ContextMenuAction::ShowRecommentations(Box::new(t)), + ); } // open detail view of artist/album @@ -88,6 +93,11 @@ impl ContextMenu { let dialog = Self::add_track_dialog(library, *track.clone()); s.add_layer(dialog); } + ContextMenuAction::ShowRecommentations(item) => { + if let Some(view) = item.open_recommentations(queue, library) { + s.call_on_name("main", move |v: &mut Layout| v.push_view(view)); + } + } } });