diff --git a/src/album.rs b/src/album.rs new file mode 100644 index 0000000..347ec54 --- /dev/null +++ b/src/album.rs @@ -0,0 +1,134 @@ +use std::fmt; +use std::sync::Arc; + +use rspotify::spotify::model::album::{FullAlbum, SimplifiedAlbum}; + +use queue::Queue; +use spotify::Spotify; +use track::Track; +use traits::ListItem; + +#[derive(Clone, Deserialize, Serialize)] +pub struct Album { + pub id: String, + pub title: String, + pub artists: Vec, + pub year: String, + pub cover_url: Option, + pub url: String, + pub tracks: Option> +} + +impl Album { + fn load_tracks(&mut self, spotify: Arc) { + if self.tracks.is_some() { + return; + } + + if let Some(fa) = spotify.full_album(&self.id) { + self.tracks = Some(fa.tracks.items + .iter() + .map(|st| Track::from_simplified_track(&st, &fa)) + .collect() + ); + } + } +} + +impl From<&SimplifiedAlbum> for Album { + fn from(sa: &SimplifiedAlbum) -> Self { + Self { + id: sa.id.clone(), + title: sa.name.clone(), + artists: sa.artists.iter().map(|sa| sa.name.clone()).collect(), + year: sa.release_date.split("-").next().unwrap().into(), + cover_url: sa.images.get(0).map(|i| i.url.clone()), + url: sa.uri.clone(), + tracks: None + } + } +} + +impl From<&FullAlbum> for Album { + fn from(fa: &FullAlbum) -> Self { + let tracks = Some(fa.tracks.items + .iter() + .map(|st| Track::from_simplified_track(&st, &fa)) + .collect() + ); + + Self { + id: fa.id.clone(), + title: fa.name.clone(), + artists: fa.artists.iter().map(|sa| sa.name.clone()).collect(), + year: fa.release_date.split("-").next().unwrap().into(), + cover_url: fa.images.get(0).map(|i| i.url.clone()), + url: fa.uri.clone(), + tracks: tracks + } + } +} + +impl fmt::Display for Album { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} - {}", self.artists.join(", "), self.title) + } +} + +impl fmt::Debug for Album { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "({} - {} ({}))", + self.artists.join(", "), + self.title, + self.id + ) + } +} + +impl ListItem for Album { + fn is_playing(&self, queue: Arc) -> bool { + if let Some(tracks) = self.tracks.as_ref() { + let playing: Vec = queue + .queue + .read() + .unwrap() + .iter() + .map(|t| t.id.clone()) + .collect(); + let ids: Vec = tracks.iter().map(|t| t.id.clone()).collect(); + !ids.is_empty() && playing == ids + } else { + false + } + } + + fn display_left(&self) -> String { + format!("{}", self) + } + + fn display_right(&self) -> String { + self.year.clone() + } + + fn play(&mut self, queue: Arc) { + self.load_tracks(queue.get_spotify()); + + if let Some(tracks) = self.tracks.as_ref() { + let tracks: Vec<&Track> = tracks.iter().collect(); + let index = queue.append_next(tracks); + queue.play(index, true); + } + } + + fn queue(&mut self, queue: Arc) { + self.load_tracks(queue.get_spotify()); + + if let Some(tracks) = self.tracks.as_ref() { + for t in tracks { + queue.append(&t); + } + } + } +} diff --git a/src/artist.rs b/src/artist.rs new file mode 100644 index 0000000..b0794b2 --- /dev/null +++ b/src/artist.rs @@ -0,0 +1,123 @@ +use std::fmt; +use std::sync::Arc; + +use rspotify::spotify::model::artist::FullArtist; + +use album::Album; +use queue::Queue; +use spotify::Spotify; +use track::Track; +use traits::ListItem; + +#[derive(Clone, Deserialize, Serialize)] +pub struct Artist { + pub id: String, + pub name: String, + pub url: String, + pub albums: Option> +} + +impl Artist { + fn load_albums(&mut self, spotify: Arc) { + if self.albums.is_some() { + return; + } + + if let Some(sas) = spotify.artist_albums(&self.id, 50, 0) { + let mut albums: Vec = Vec::new(); + + for sa in sas.items { + if Some("appears_on".into()) == sa.album_group { + continue; + } + + if let Some(fa) = spotify.full_album(&sa.id).as_ref() { + albums.push(fa.into()); + } + } + + self.albums = Some(albums); + } + } + + fn tracks(&self) -> Option> { + if let Some(albums) = self.albums.as_ref() { + Some(albums + .iter() + .map(|a| a.tracks.as_ref().unwrap()) + .flatten() + .collect() + ) + } else { + None + } + } +} + +impl From<&FullArtist> for Artist { + fn from(fa: &FullArtist) -> Self { + Self { + id: fa.id.clone(), + name: fa.name.clone(), + url: fa.uri.clone(), + albums: None + } + } +} + +impl fmt::Display for Artist { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.name) + } +} + +impl fmt::Debug for Artist { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} ({})", self.name, self.id) + } +} + +impl ListItem for Artist { + fn is_playing(&self, queue: Arc) -> bool { + if let Some(tracks) = self.tracks() { + let playing: Vec = queue + .queue + .read() + .unwrap() + .iter() + .map(|t| t.id.clone()) + .collect(); + let ids: Vec = tracks.iter().map(|t| t.id.clone()).collect(); + !ids.is_empty() && playing == ids + } else { + false + } + } + + fn display_left(&self) -> String { + format!("{}", self) + } + + fn display_right(&self) -> String { + "".into() + } + + fn play(&mut self, queue: Arc) { + self.load_albums(queue.get_spotify()); + + if let Some(tracks) = self.tracks() { + let index = queue.append_next(tracks); + queue.play(index, true); + } + } + + fn queue(&mut self, queue: Arc) { + self.load_albums(queue.get_spotify()); + + if let Some(tracks) = self.tracks() { + for t in tracks { + queue.append(t); + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 17dd9d5..87a7643 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,6 +38,8 @@ use cursive::Cursive; use librespot::core::authentication::Credentials; +mod album; +mod artist; mod authentication; mod commands; mod config; diff --git a/src/playlists.rs b/src/playlists.rs index 6eb7253..03e6ec2 100644 --- a/src/playlists.rs +++ b/src/playlists.rs @@ -49,12 +49,12 @@ impl ListItem for Playlist { format!("{} tracks", self.tracks.len()) } - fn play(&self, queue: Arc) { + fn play(&mut self, queue: Arc) { let index = queue.append_next(self.tracks.iter().collect()); queue.play(index, true); } - fn queue(&self, queue: Arc) { + fn queue(&mut self, queue: Arc) { for track in self.tracks.iter() { queue.append(track); } @@ -117,7 +117,7 @@ impl Playlists { let mut tracks_result = spotify.user_playlist_tracks(&id, 100, 0); while let Some(ref tracks) = tracks_result.clone() { for listtrack in &tracks.items { - collected_tracks.push(Track::new(&listtrack.track)); + collected_tracks.push((&listtrack.track).into()); } debug!("got {} tracks", tracks.items.len()); diff --git a/src/queue.rs b/src/queue.rs index c781cba..6798ff8 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -255,4 +255,8 @@ impl Queue { *random_order = None; } } + + pub fn get_spotify(&self) -> Arc { + self.spotify.clone() + } } diff --git a/src/spotify.rs b/src/spotify.rs index 032fb5a..b7e0e04 100644 --- a/src/spotify.rs +++ b/src/spotify.rs @@ -13,9 +13,10 @@ use librespot::playback::player::Player; use rspotify::spotify::client::ApiError; use rspotify::spotify::client::Spotify as SpotifyAPI; +use rspotify::spotify::model::album::{FullAlbum, SimplifiedAlbum}; use rspotify::spotify::model::page::Page; use rspotify::spotify::model::playlist::{PlaylistTrack, SimplifiedPlaylist}; -use rspotify::spotify::model::search::{SearchTracks, SearchPlaylists}; +use rspotify::spotify::model::search::{SearchTracks, SearchAlbums, SearchArtists, SearchPlaylists}; use failure::Error; @@ -432,6 +433,14 @@ impl Spotify { self.api_with_retry(|api| api.search_track(query, limit, offset, None)) } + pub fn search_album(&self, query: &str, limit: u32, offset: u32) -> Option { + self.api_with_retry(|api| api.search_album(query, limit, offset, None)) + } + + pub fn search_artist(&self, query: &str, limit: u32, offset: u32) -> Option { + self.api_with_retry(|api| api.search_artist(query, limit, offset, None)) + } + pub fn search_playlist(&self, query: &str, limit: u32, offset: u32) -> Option { self.api_with_retry(|api| api.search_playlist(query, limit, offset, None)) } @@ -456,6 +465,25 @@ impl Spotify { }) } + pub fn full_album(&self, album_id: &str) -> Option { + self.api_with_retry(|api| api.album(album_id)) + } + + pub fn artist_albums( + &self, + artist_id: &str, + limit: u32, + offset: u32 + ) -> Option> { + self.api_with_retry(|api| api.artist_albums( + artist_id, + None, + None, + Some(limit), + Some(offset) + )) + } + pub fn load(&self, track: &Track) { info!("loading track: {:?}", track); self.channel diff --git a/src/track.rs b/src/track.rs index ea4c4fc..1a04ba1 100644 --- a/src/track.rs +++ b/src/track.rs @@ -1,7 +1,8 @@ use std::fmt; use std::sync::Arc; -use rspotify::spotify::model::track::FullTrack; +use rspotify::spotify::model::album::FullAlbum; +use rspotify::spotify::model::track::{FullTrack, SimplifiedTrack}; use queue::Queue; use traits::ListItem; @@ -21,7 +22,46 @@ pub struct Track { } impl Track { - pub fn new(track: &FullTrack) -> Track { + pub fn from_simplified_track(track: &SimplifiedTrack, album: &FullAlbum) -> Track { + let artists = track + .artists + .iter() + .map(|ref artist| artist.name.clone()) + .collect::>(); + let album_artists = album + .artists + .iter() + .map(|ref artist| artist.name.clone()) + .collect::>(); + + let cover_url = match album.images.get(0) { + Some(image) => image.url.clone(), + None => "".to_owned(), + }; + + Self { + id: track.id.clone(), + title: track.name.clone(), + track_number: track.track_number, + disc_number: track.disc_number, + duration: track.duration_ms, + artists, + album: album.name.clone(), + album_artists, + cover_url, + url: track.uri.clone(), + } + } + + pub fn duration_str(&self) -> String { + let minutes = self.duration / 60_000; + let seconds = (self.duration / 1000) % 60; + format!("{:02}:{:02}", minutes, seconds) + } +} + +impl From<&FullTrack> for Track { + fn from(track: &FullTrack) -> Self { let artists = track .artists .iter() @@ -39,7 +79,7 @@ impl Track { None => "".to_owned(), }; - Track { + Self { id: track.id.clone(), title: track.name.clone(), track_number: track.track_number, @@ -52,12 +92,6 @@ impl Track { url: track.uri.clone(), } } - - pub fn duration_str(&self) -> String { - let minutes = self.duration / 60_000; - let seconds = (self.duration / 1000) % 60; - format!("{:02}:{:02}", minutes, seconds) - } } impl fmt::Display for Track { @@ -92,12 +126,12 @@ impl ListItem for Track { self.duration_str() } - fn play(&self, queue: Arc) { + fn play(&mut self, queue: Arc) { let index = queue.append_next(vec![self]); queue.play(index, true); } - fn queue(&self, queue: Arc) { + fn queue(&mut self, queue: Arc) { queue.append(self); } } diff --git a/src/traits.rs b/src/traits.rs index cc48594..f6dfade 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -11,8 +11,8 @@ pub trait ListItem { fn is_playing(&self, queue: Arc) -> bool; fn display_left(&self) -> String; fn display_right(&self) -> String; - fn play(&self, queue: Arc); - fn queue(&self, queue: Arc); + fn play(&mut self, queue: Arc); + fn queue(&mut self, queue: Arc); } pub trait ViewExt: View { diff --git a/src/ui/listview.rs b/src/ui/listview.rs index e7b4375..bd6a3ae 100644 --- a/src/ui/listview.rs +++ b/src/ui/listview.rs @@ -181,16 +181,16 @@ impl ViewExt for ListView { args: &[String], ) -> Result { if cmd == "play" { - let content = self.content.read().unwrap(); - if let Some(item) = content.get(self.selected) { + let mut content = self.content.write().unwrap(); + if let Some(item) = content.get_mut(self.selected) { item.play(self.queue.clone()); } return Ok(CommandResult::Consumed(None)); } if cmd == "queue" { - let content = self.content.read().unwrap(); - if let Some(item) = content.get(self.selected) { + let mut content = self.content.write().unwrap(); + if let Some(item) = content.get_mut(self.selected) { item.queue(self.queue.clone()); } return Ok(CommandResult::Consumed(None)); diff --git a/src/ui/search.rs b/src/ui/search.rs index 2fa524b..3567c59 100644 --- a/src/ui/search.rs +++ b/src/ui/search.rs @@ -9,6 +9,8 @@ use cursive::{Cursive, Printer, Vec2}; use std::cell::RefCell; use std::sync::{Arc, Mutex, RwLock}; +use album::Album; +use artist::Artist; use commands::CommandResult; use playlists::{Playlist, Playlists}; use queue::Queue; @@ -20,6 +22,8 @@ use ui::tabview::TabView; pub struct SearchView { results_tracks: Arc>>, + results_albums: Arc>>, + results_artists: Arc>>, results_playlists: Arc>>, edit: IdView, list: IdView, @@ -32,6 +36,8 @@ pub const EDIT_ID: &str = "search_edit"; impl SearchView { pub fn new(spotify: Arc, queue: Arc) -> SearchView { let results_tracks = Arc::new(RwLock::new(Vec::new())); + let results_albums = Arc::new(RwLock::new(Vec::new())); + let results_artists = Arc::new(RwLock::new(Vec::new())); let results_playlists = Arc::new(RwLock::new(Vec::new())); let searchfield = EditView::new() @@ -47,10 +53,14 @@ impl SearchView { let tabs = TabView::new() .tab("tracks", "Tracks", ListView::new(results_tracks.clone(), queue.clone())) + .tab("albums", "Albums", ListView::new(results_albums.clone(), queue.clone())) + .tab("artists", "Artists", ListView::new(results_artists.clone(), queue.clone())) .tab("playlists", "Playlists", ListView::new(results_playlists.clone(), queue.clone())); SearchView { results_tracks, + results_albums, + results_artists, results_playlists, edit: searchfield, list: tabs.with_id(LIST_ID), @@ -76,13 +86,47 @@ impl SearchView { .tracks .items .iter() - .map(|ft| Track::new(ft)) + .map(|ft| ft.into()) .collect(); let mut r = tracks.write().unwrap(); *r = t; } } + fn search_album( + spotify: Arc, + albums: Arc>>, + query: String, + ) { + if let Some(results) = spotify.search_album(&query, 50, 0) { + let a = results + .albums + .items + .iter() + .map(|sa| sa.into()) + .collect(); + let mut r = albums.write().unwrap(); + *r = a; + } + } + + fn search_artist( + spotify: Arc, + artists: Arc>>, + query: String, + ) { + if let Some(results) = spotify.search_artist(&query, 50, 0) { + let a = results + .artists + .items + .iter() + .map(|fa| fa.into()) + .collect(); + let mut r = artists.write().unwrap(); + *r = a; + } + } + fn search_playlist( spotify: Arc, playlists: Arc>>, @@ -122,6 +166,24 @@ impl SearchView { }); } + { + let spotify = self.spotify.clone(); + let results = self.results_albums.clone(); + let query = query.clone(); + std::thread::spawn(|| { + Self::search_album(spotify, results, query); + }); + } + + { + let spotify = self.spotify.clone(); + let results = self.results_artists.clone(); + let query = query.clone(); + std::thread::spawn(|| { + Self::search_artist(spotify, results, query); + }); + } + { let spotify = self.spotify.clone(); let results = self.results_playlists.clone();