add recommendations/similar tracks feature

fixes #186
This commit is contained in:
Henrik Friedrichsen
2020-10-18 17:41:18 +02:00
parent e5be021699
commit 79a3d0ca8a
5 changed files with 106 additions and 10 deletions

View File

@@ -65,7 +65,7 @@ fn get_metadata(playable: Option<Playable>) -> Metadata {
Variant(Box::new(
playable
.and_then(|p| p.track())
.map(|t| t.album)
.map(|t| t.album.unwrap_or_default())
.unwrap_or_default(),
)),
);

View File

@@ -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<Vec<String>>,
seed_genres: Option<Vec<String>>,
seed_tracks: Option<Vec<String>>,
) -> Option<Recommendations> {
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,

View File

@@ -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<String>,
pub artist_ids: Vec<String>,
pub album: String,
pub album: Option<String>,
pub album_id: Option<String>,
pub album_artists: Vec<String>,
pub cover_url: Option<String>,
@@ -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::<Vec<String>>();
let artist_ids = track
.artists
.iter()
.filter(|a| a.id.is_some())
.map(|ref artist| artist.id.clone().unwrap())
.collect::<Vec<String>>();
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<Library>) -> 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<Queue>,
library: Arc<Library>,
) -> Option<Box<dyn ViewExt>> {
let spotify = queue.get_spotify();
let recommendations: Option<Vec<Track>> = 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<String> {
self.id
.clone()

View File

@@ -26,6 +26,13 @@ pub trait ListItem: Sync + Send + 'static {
fn save(&mut self, library: Arc<Library>);
fn unsave(&mut self, library: Arc<Library>);
fn open(&self, queue: Arc<Queue>, library: Arc<Library>) -> Option<Box<dyn ViewExt>>;
fn open_recommentations(
&self,
_queue: Arc<Queue>,
_library: Arc<Library>,
) -> Option<Box<dyn ViewExt>> {
None
}
fn share_url(&self) -> Option<String>;
fn album(&self, _queue: Arc<Queue>) -> Option<Album> {

View File

@@ -24,6 +24,7 @@ enum ContextMenuAction {
ShowItem(Box<dyn ListItem>),
ShareUrl(String),
AddToPlaylist(Box<Track>),
ShowRecommentations(Box<dyn ListItem>),
}
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));
}
}
}
});