From 02921752e174d8b2a8f45cd5697b6014ae1e20e1 Mon Sep 17 00:00:00 2001 From: Henrik Friedrichsen Date: Tue, 6 Apr 2021 14:56:41 +0200 Subject: [PATCH] Use new pagination interface for artist albums Brings along some other changes: - Split artist albums/singles into separate panel - Paginate artist albums/singles - Play top tracks by artist instead of all tracks by artist Fixes #477 --- src/artist.rs | 69 ++++++---------------------------------------- src/spotify.rs | 55 ++++++++++++++++++++++++------------ src/ui/artist.rs | 59 +++++++++++++++++++++++++++------------ src/ui/listview.rs | 4 +-- 4 files changed, 89 insertions(+), 98 deletions(-) diff --git a/src/artist.rs b/src/artist.rs index a236c2b..42260ed 100644 --- a/src/artist.rs +++ b/src/artist.rs @@ -3,7 +3,6 @@ use std::sync::Arc; use rspotify::model::artist::{FullArtist, SimplifiedArtist}; -use crate::album::Album; use crate::library::Library; use crate::playable::Playable; use crate::queue::Queue; @@ -17,7 +16,6 @@ pub struct Artist { pub id: Option, pub name: String, pub url: Option, - pub albums: Option>, pub tracks: Option>, pub is_followed: bool, } @@ -28,63 +26,16 @@ impl Artist { id: Some(id), name, url: None, - albums: None, tracks: None, is_followed: false, } } - pub fn load_albums(&mut self, spotify: Spotify) { - if let Some(albums) = self.albums.as_mut() { - for album in albums { - album.load_tracks(spotify.clone()); + fn load_top_tracks(&mut self, spotify: Spotify) { + if let Some(artist_id) = &self.id { + if self.tracks.is_none() { + self.tracks = spotify.artist_top_tracks(artist_id); } - } else if let Some(ref artist_id) = self.id { - let mut collected_ids: Vec = Vec::new(); - let mut offset = 0; - while let Some(sas) = spotify.artist_albums(artist_id, 50, offset) { - let items_len = sas.items.len() as u32; - debug!("got {} albums", items_len); - - for sa in sas.items { - if Some("appears_on") == sa.album_group.as_deref() { - continue; - } - if let Some(album_id) = sa.id { - collected_ids.push(album_id); - } - } - - match sas.next { - Some(_) => offset += items_len, - None => break, - } - } - - let albums = match spotify.albums(&collected_ids) { - Some(fas) => fas.iter().map(Album::from).collect(), - None => Vec::new(), - }; - self.albums = Some(albums); - } - if let Some(ref mut albums) = self.albums { - albums.sort_by(|a, b| b.year.cmp(&a.year)); - } - } - - fn tracks(&self) -> Option> { - if let Some(tracks) = self.tracks.as_ref() { - Some(tracks.iter().collect()) - } else if let Some(albums) = self.albums.as_ref() { - Some( - albums - .iter() - .map(|a| a.tracks.as_ref().unwrap()) - .flatten() - .collect(), - ) - } else { - None } } } @@ -95,7 +46,6 @@ impl From<&SimplifiedArtist> for Artist { id: sa.id.clone(), name: sa.name.clone(), url: sa.uri.clone(), - albums: None, tracks: None, is_followed: false, } @@ -108,7 +58,6 @@ impl From<&FullArtist> for Artist { id: Some(fa.id.clone()), name: fa.name.clone(), url: Some(fa.uri.clone()), - albums: None, tracks: None, is_followed: false, } @@ -129,7 +78,7 @@ impl fmt::Debug for Artist { impl ListItem for Artist { fn is_playing(&self, queue: Arc) -> bool { - if let Some(tracks) = self.tracks() { + if let Some(tracks) = &self.tracks { let playing: Vec = queue .queue .read() @@ -173,7 +122,7 @@ impl ListItem for Artist { } fn play(&mut self, queue: Arc) { - self.load_albums(queue.get_spotify()); + self.load_top_tracks(queue.get_spotify()); if let Some(tracks) = self.tracks.as_ref() { let tracks: Vec = tracks @@ -186,7 +135,7 @@ impl ListItem for Artist { } fn play_next(&mut self, queue: Arc) { - self.load_albums(queue.get_spotify()); + self.load_top_tracks(queue.get_spotify()); if let Some(tracks) = self.tracks.as_ref() { for t in tracks.iter().rev() { @@ -196,9 +145,9 @@ impl ListItem for Artist { } fn queue(&mut self, queue: Arc) { - self.load_albums(queue.get_spotify()); + self.load_top_tracks(queue.get_spotify()); - if let Some(tracks) = self.tracks() { + if let Some(tracks) = &self.tracks { for t in tracks { queue.append(Playable::Track(t.clone())); } diff --git a/src/spotify.rs b/src/spotify.rs index 77a04b3..cdb82f1 100644 --- a/src/spotify.rs +++ b/src/spotify.rs @@ -11,14 +11,14 @@ use librespot_playback::config::Bitrate; use librespot_playback::player::Player; use rspotify::blocking::client::Spotify as SpotifyAPI; -use rspotify::model::album::{FullAlbum, SavedAlbum, SimplifiedAlbum}; +use rspotify::model::album::{FullAlbum, SavedAlbum}; use rspotify::model::artist::FullArtist; use rspotify::model::page::{CursorBasedPage, Page}; use rspotify::model::playlist::{FullPlaylist, PlaylistTrack, SimplifiedPlaylist}; use rspotify::model::search::SearchResult; use rspotify::model::track::{FullTrack, SavedTrack, SimplifiedTrack}; use rspotify::model::user::PrivateUser; -use rspotify::senum::SearchType; +use rspotify::senum::{AlbumType, SearchType}; use rspotify::{blocking::client::ApiError, senum::Country}; use serde_json::{json, Map}; @@ -49,6 +49,8 @@ use crate::playable::Playable; use crate::spotify_worker::{Worker, WorkerCommand}; use crate::track::Track; +use crate::album::Album; +use crate::ui::pagination::{ApiPage, ApiResult}; use rspotify::model::recommend::Recommendations; use rspotify::model::show::{FullEpisode, FullShow, Show, SimplifiedEpisode}; @@ -481,16 +483,6 @@ impl Spotify { self.api_with_retry(|api| api.album(album_id)) } - pub fn albums(&self, album_ids: &[String]) -> Option> { - const MAX_SIZE: usize = 20; - let mut collected = Vec::new(); - for ids in album_ids.chunks(MAX_SIZE) { - let fas = self.api_with_retry(|api| api.albums(ids.to_vec()))?; - collected.extend_from_slice(&fas.albums); - } - Some(collected) - } - pub fn artist(&self, artist_id: &str) -> Option { self.api_with_retry(|api| api.artist(artist_id)) } @@ -576,12 +568,39 @@ impl Spotify { pub fn artist_albums( &self, artist_id: &str, - limit: u32, - offset: u32, - ) -> Option> { - self.api_with_retry(|api| { - api.artist_albums(artist_id, None, self.country, Some(limit), Some(offset)) - }) + album_type: Option, + ) -> ApiResult { + const MAX_SIZE: u32 = 50; + let spotify = self.clone(); + let artist_id = artist_id.to_string(); + let fetch_page = move |offset: u32| { + spotify.api_with_retry(|api| { + match api.artist_albums( + &artist_id, + album_type, + spotify.country, + Some(MAX_SIZE), + Some(offset), + ) { + Ok(page) => { + let mut albums: Vec = page + .items + .iter() + .map(|sa| sa.into()) + .collect(); + albums.sort_by(|a, b| b.year.cmp(&a.year)); + Ok(ApiPage { + offset: page.offset, + total: page.total, + items: albums, + }) + } + Err(e) => Err(e), + } + }) + }; + + ApiResult::new(MAX_SIZE, Arc::new(fetch_page)) } pub fn show_episodes(&self, show_id: &str, offset: u32) -> Option> { diff --git a/src/ui/artist.rs b/src/ui/artist.rs index 652e887..dd86fe1 100644 --- a/src/ui/artist.rs +++ b/src/ui/artist.rs @@ -4,6 +4,7 @@ use std::thread; use cursive::view::ViewWrapper; use cursive::Cursive; +use crate::album::Album; use crate::artist::Artist; use crate::command::Command; use crate::commands::CommandResult; @@ -13,6 +14,7 @@ use crate::track::Track; use crate::traits::ViewExt; use crate::ui::listview::ListView; use crate::ui::tabview::TabView; +use rspotify::senum::AlbumType; pub struct ArtistView { artist: Artist, @@ -21,16 +23,12 @@ pub struct ArtistView { impl ArtistView { pub fn new(queue: Arc, library: Arc, artist: &Artist) -> Self { - let mut artist = artist.clone(); - let spotify = queue.get_spotify(); - artist.load_albums(spotify.clone()); - let albums = if let Some(a) = artist.albums.as_ref() { - a.clone() - } else { - Vec::new() - }; + let albums_view = + Self::albums_view(&artist, AlbumType::Album, queue.clone(), library.clone()); + let singles_view = + Self::albums_view(&artist, AlbumType::Single, queue.clone(), library.clone()); let top_tracks: Arc>> = Arc::new(RwLock::new(Vec::new())); { @@ -85,15 +83,8 @@ impl ArtistView { ListView::new(top_tracks, queue.clone(), library.clone()), ); - tabs.add_tab( - "albums", - "Albums", - ListView::new( - Arc::new(RwLock::new(albums)), - queue.clone(), - library.clone(), - ), - ); + tabs.add_tab("albums", "Albums", albums_view); + tabs.add_tab("singles", "Singles", singles_view); tabs.add_tab( "related", @@ -101,7 +92,39 @@ impl ArtistView { ListView::new(related, queue, library), ); - Self { artist, tabs } + Self { + artist: artist.clone(), + tabs, + } + } + + fn albums_view( + artist: &Artist, + album_type: AlbumType, + queue: Arc, + library: Arc, + ) -> ListView { + if let Some(artist_id) = &artist.id { + let spotify = queue.get_spotify(); + let albums_page = spotify.artist_albums(artist_id, Some(album_type)); + let view = ListView::new(albums_page.items.clone(), queue, library.clone()); + let pagination = view.get_pagination().clone(); + + pagination.set( + albums_page.total as usize, + Box::new(move |items| { + if let Some(next_page) = albums_page.next() { + let mut w = items.write().unwrap(); + w.extend(next_page); + w.sort_by(|a, b| b.year.cmp(&a.year)); + library.trigger_redraw(); + } + }), + ); + view + } else { + ListView::new(Arc::new(RwLock::new(Vec::new())), queue, library) + } } } diff --git a/src/ui/listview.rs b/src/ui/listview.rs index 0eecb9d..badafab 100644 --- a/src/ui/listview.rs +++ b/src/ui/listview.rs @@ -141,7 +141,7 @@ impl View for ListView { self.scrollbar.draw(printer, |printer, i| { // draw paginator after content - if i == content.len() { + if i == content.len() && self.can_paginate() { let style = ColorStyle::secondary(); let max = self.pagination.max_content().unwrap(); @@ -149,7 +149,7 @@ impl View for ListView { printer.with_color(style, |printer| { printer.print((0, 0), &buf); }); - } else { + } else if i < content.len() { let item = &content[i]; let currently_playing = item.is_playing(self.queue.clone()) && self.queue.get_current_index() == Some(i);