Add album and artist searching
This commit is contained in:
134
src/album.rs
Normal file
134
src/album.rs
Normal file
@@ -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<String>,
|
||||
pub year: String,
|
||||
pub cover_url: Option<String>,
|
||||
pub url: String,
|
||||
pub tracks: Option<Vec<Track>>
|
||||
}
|
||||
|
||||
impl Album {
|
||||
fn load_tracks(&mut self, spotify: Arc<Spotify>) {
|
||||
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<Queue>) -> bool {
|
||||
if let Some(tracks) = self.tracks.as_ref() {
|
||||
let playing: Vec<String> = queue
|
||||
.queue
|
||||
.read()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|t| t.id.clone())
|
||||
.collect();
|
||||
let ids: Vec<String> = 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<Queue>) {
|
||||
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<Queue>) {
|
||||
self.load_tracks(queue.get_spotify());
|
||||
|
||||
if let Some(tracks) = self.tracks.as_ref() {
|
||||
for t in tracks {
|
||||
queue.append(&t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
123
src/artist.rs
Normal file
123
src/artist.rs
Normal file
@@ -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<Vec<Album>>
|
||||
}
|
||||
|
||||
impl Artist {
|
||||
fn load_albums(&mut self, spotify: Arc<Spotify>) {
|
||||
if self.albums.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(sas) = spotify.artist_albums(&self.id, 50, 0) {
|
||||
let mut albums: Vec<Album> = 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<Vec<&Track>> {
|
||||
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<Queue>) -> bool {
|
||||
if let Some(tracks) = self.tracks() {
|
||||
let playing: Vec<String> = queue
|
||||
.queue
|
||||
.read()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|t| t.id.clone())
|
||||
.collect();
|
||||
let ids: Vec<String> = 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<Queue>) {
|
||||
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<Queue>) {
|
||||
self.load_albums(queue.get_spotify());
|
||||
|
||||
if let Some(tracks) = self.tracks() {
|
||||
for t in tracks {
|
||||
queue.append(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,8 @@ use cursive::Cursive;
|
||||
|
||||
use librespot::core::authentication::Credentials;
|
||||
|
||||
mod album;
|
||||
mod artist;
|
||||
mod authentication;
|
||||
mod commands;
|
||||
mod config;
|
||||
|
||||
@@ -49,12 +49,12 @@ impl ListItem for Playlist {
|
||||
format!("{} tracks", self.tracks.len())
|
||||
}
|
||||
|
||||
fn play(&self, queue: Arc<Queue>) {
|
||||
fn play(&mut self, queue: Arc<Queue>) {
|
||||
let index = queue.append_next(self.tracks.iter().collect());
|
||||
queue.play(index, true);
|
||||
}
|
||||
|
||||
fn queue(&self, queue: Arc<Queue>) {
|
||||
fn queue(&mut self, queue: Arc<Queue>) {
|
||||
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());
|
||||
|
||||
|
||||
@@ -255,4 +255,8 @@ impl Queue {
|
||||
*random_order = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_spotify(&self) -> Arc<Spotify> {
|
||||
self.spotify.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SearchAlbums> {
|
||||
self.api_with_retry(|api| api.search_album(query, limit, offset, None))
|
||||
}
|
||||
|
||||
pub fn search_artist(&self, query: &str, limit: u32, offset: u32) -> Option<SearchArtists> {
|
||||
self.api_with_retry(|api| api.search_artist(query, limit, offset, None))
|
||||
}
|
||||
|
||||
pub fn search_playlist(&self, query: &str, limit: u32, offset: u32) -> Option<SearchPlaylists> {
|
||||
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<FullAlbum> {
|
||||
self.api_with_retry(|api| api.album(album_id))
|
||||
}
|
||||
|
||||
pub fn artist_albums(
|
||||
&self,
|
||||
artist_id: &str,
|
||||
limit: u32,
|
||||
offset: u32
|
||||
) -> Option<Page<SimplifiedAlbum>> {
|
||||
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
|
||||
|
||||
56
src/track.rs
56
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::<Vec<String>>();
|
||||
let album_artists = album
|
||||
.artists
|
||||
.iter()
|
||||
.map(|ref artist| artist.name.clone())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
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<Queue>) {
|
||||
fn play(&mut self, queue: Arc<Queue>) {
|
||||
let index = queue.append_next(vec![self]);
|
||||
queue.play(index, true);
|
||||
}
|
||||
|
||||
fn queue(&self, queue: Arc<Queue>) {
|
||||
fn queue(&mut self, queue: Arc<Queue>) {
|
||||
queue.append(self);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ pub trait ListItem {
|
||||
fn is_playing(&self, queue: Arc<Queue>) -> bool;
|
||||
fn display_left(&self) -> String;
|
||||
fn display_right(&self) -> String;
|
||||
fn play(&self, queue: Arc<Queue>);
|
||||
fn queue(&self, queue: Arc<Queue>);
|
||||
fn play(&mut self, queue: Arc<Queue>);
|
||||
fn queue(&mut self, queue: Arc<Queue>);
|
||||
}
|
||||
|
||||
pub trait ViewExt: View {
|
||||
|
||||
@@ -181,16 +181,16 @@ impl<I: ListItem> ViewExt for ListView<I> {
|
||||
args: &[String],
|
||||
) -> Result<CommandResult, String> {
|
||||
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));
|
||||
|
||||
@@ -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<RwLock<Vec<Track>>>,
|
||||
results_albums: Arc<RwLock<Vec<Album>>>,
|
||||
results_artists: Arc<RwLock<Vec<Artist>>>,
|
||||
results_playlists: Arc<RwLock<Vec<Playlist>>>,
|
||||
edit: IdView<EditView>,
|
||||
list: IdView<TabView>,
|
||||
@@ -32,6 +36,8 @@ pub const EDIT_ID: &str = "search_edit";
|
||||
impl SearchView {
|
||||
pub fn new(spotify: Arc<Spotify>, queue: Arc<Queue>) -> 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<Spotify>,
|
||||
albums: Arc<RwLock<Vec<Album>>>,
|
||||
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<Spotify>,
|
||||
artists: Arc<RwLock<Vec<Artist>>>,
|
||||
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<Spotify>,
|
||||
playlists: Arc<RwLock<Vec<Playlist>>>,
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user