@@ -65,7 +65,7 @@ fn get_metadata(playable: Option<Playable>) -> Metadata {
|
|||||||
Variant(Box::new(
|
Variant(Box::new(
|
||||||
playable
|
playable
|
||||||
.and_then(|p| p.track())
|
.and_then(|p| p.track())
|
||||||
.map(|t| t.album)
|
.map(|t| t.album.unwrap_or_default())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ use rspotify::model::track::{FullTrack, SavedTrack, SimplifiedTrack};
|
|||||||
use rspotify::model::user::PrivateUser;
|
use rspotify::model::user::PrivateUser;
|
||||||
use rspotify::senum::SearchType;
|
use rspotify::senum::SearchType;
|
||||||
|
|
||||||
use serde_json::json;
|
use serde_json::{json, Map};
|
||||||
|
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
|
|
||||||
@@ -59,6 +59,7 @@ use crate::events::{Event, EventManager};
|
|||||||
use crate::playable::Playable;
|
use crate::playable::Playable;
|
||||||
use crate::queue;
|
use crate::queue;
|
||||||
use crate::track::Track;
|
use crate::track::Track;
|
||||||
|
use rspotify::model::recommend::Recommendations;
|
||||||
use rspotify::model::show::{FullEpisode, FullShow, Show, SimplifiedEpisode};
|
use rspotify::model::show::{FullEpisode, FullShow, Show, SimplifiedEpisode};
|
||||||
|
|
||||||
pub const VOLUME_PERCENT: u16 = ((u16::max_value() as f64) * 1.0 / 100.0) as u16;
|
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))
|
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(
|
pub fn search(
|
||||||
&self,
|
&self,
|
||||||
searchtype: SearchType,
|
searchtype: SearchType,
|
||||||
|
|||||||
72
src/track.rs
72
src/track.rs
@@ -1,5 +1,5 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use rspotify::model::album::FullAlbum;
|
use rspotify::model::album::FullAlbum;
|
||||||
@@ -10,7 +10,8 @@ use crate::artist::Artist;
|
|||||||
use crate::library::Library;
|
use crate::library::Library;
|
||||||
use crate::playable::Playable;
|
use crate::playable::Playable;
|
||||||
use crate::queue::Queue;
|
use crate::queue::Queue;
|
||||||
use crate::traits::{ListItem, ViewExt};
|
use crate::traits::{IntoBoxedViewExt, ListItem, ViewExt};
|
||||||
|
use crate::ui::listview::ListView;
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Serialize)]
|
#[derive(Clone, Deserialize, Serialize)]
|
||||||
pub struct Track {
|
pub struct Track {
|
||||||
@@ -22,7 +23,7 @@ pub struct Track {
|
|||||||
pub duration: u32,
|
pub duration: u32,
|
||||||
pub artists: Vec<String>,
|
pub artists: Vec<String>,
|
||||||
pub artist_ids: Vec<String>,
|
pub artist_ids: Vec<String>,
|
||||||
pub album: String,
|
pub album: Option<String>,
|
||||||
pub album_id: Option<String>,
|
pub album_id: Option<String>,
|
||||||
pub album_artists: Vec<String>,
|
pub album_artists: Vec<String>,
|
||||||
pub cover_url: Option<String>,
|
pub cover_url: Option<String>,
|
||||||
@@ -58,7 +59,7 @@ impl Track {
|
|||||||
duration: track.duration_ms,
|
duration: track.duration_ms,
|
||||||
artists,
|
artists,
|
||||||
artist_ids,
|
artist_ids,
|
||||||
album: album.name.clone(),
|
album: Some(album.name.clone()),
|
||||||
album_id: Some(album.id.clone()),
|
album_id: Some(album.id.clone()),
|
||||||
album_artists,
|
album_artists,
|
||||||
cover_url: album.images.get(0).map(|img| img.url.clone()),
|
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 {
|
impl From<&FullTrack> for Track {
|
||||||
fn from(track: &FullTrack) -> Self {
|
fn from(track: &FullTrack) -> Self {
|
||||||
let artists = track
|
let artists = track
|
||||||
@@ -103,7 +137,7 @@ impl From<&FullTrack> for Track {
|
|||||||
duration: track.duration_ms,
|
duration: track.duration_ms,
|
||||||
artists,
|
artists,
|
||||||
artist_ids,
|
artist_ids,
|
||||||
album: track.album.name.clone(),
|
album: Some(track.album.name.clone()),
|
||||||
album_id: track.album.id.clone(),
|
album_id: track.album.id.clone(),
|
||||||
album_artists,
|
album_artists,
|
||||||
cover_url: track.album.images.get(0).map(|img| img.url.clone()),
|
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 {
|
fn display_center(&self, library: Arc<Library>) -> String {
|
||||||
if library.cfg.values().album_column.unwrap_or(true) {
|
if library.cfg.values().album_column.unwrap_or(true) {
|
||||||
self.album.to_string()
|
self.album.clone().unwrap_or_default()
|
||||||
} else {
|
} else {
|
||||||
"".to_string()
|
"".to_string()
|
||||||
}
|
}
|
||||||
@@ -207,6 +241,32 @@ impl ListItem for Track {
|
|||||||
None
|
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> {
|
fn share_url(&self) -> Option<String> {
|
||||||
self.id
|
self.id
|
||||||
.clone()
|
.clone()
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ pub trait ListItem: Sync + Send + 'static {
|
|||||||
fn save(&mut self, library: Arc<Library>);
|
fn save(&mut self, library: Arc<Library>);
|
||||||
fn unsave(&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(&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 share_url(&self) -> Option<String>;
|
||||||
|
|
||||||
fn album(&self, _queue: Arc<Queue>) -> Option<Album> {
|
fn album(&self, _queue: Arc<Queue>) -> Option<Album> {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ enum ContextMenuAction {
|
|||||||
ShowItem(Box<dyn ListItem>),
|
ShowItem(Box<dyn ListItem>),
|
||||||
ShareUrl(String),
|
ShareUrl(String),
|
||||||
AddToPlaylist(Box<Track>),
|
AddToPlaylist(Box<Track>),
|
||||||
|
ShowRecommentations(Box<dyn ListItem>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContextMenu {
|
impl ContextMenu {
|
||||||
@@ -62,8 +63,12 @@ impl ContextMenu {
|
|||||||
if let Some(t) = item.track() {
|
if let Some(t) = item.track() {
|
||||||
content.add_item(
|
content.add_item(
|
||||||
"Add to playlist",
|
"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
|
// open detail view of artist/album
|
||||||
@@ -88,6 +93,11 @@ impl ContextMenu {
|
|||||||
let dialog = Self::add_track_dialog(library, *track.clone());
|
let dialog = Self::add_track_dialog(library, *track.clone());
|
||||||
s.add_layer(dialog);
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user