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
This commit is contained in:
@@ -3,7 +3,6 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use rspotify::model::artist::{FullArtist, SimplifiedArtist};
|
use rspotify::model::artist::{FullArtist, SimplifiedArtist};
|
||||||
|
|
||||||
use crate::album::Album;
|
|
||||||
use crate::library::Library;
|
use crate::library::Library;
|
||||||
use crate::playable::Playable;
|
use crate::playable::Playable;
|
||||||
use crate::queue::Queue;
|
use crate::queue::Queue;
|
||||||
@@ -17,7 +16,6 @@ pub struct Artist {
|
|||||||
pub id: Option<String>,
|
pub id: Option<String>,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub url: Option<String>,
|
pub url: Option<String>,
|
||||||
pub albums: Option<Vec<Album>>,
|
|
||||||
pub tracks: Option<Vec<Track>>,
|
pub tracks: Option<Vec<Track>>,
|
||||||
pub is_followed: bool,
|
pub is_followed: bool,
|
||||||
}
|
}
|
||||||
@@ -28,63 +26,16 @@ impl Artist {
|
|||||||
id: Some(id),
|
id: Some(id),
|
||||||
name,
|
name,
|
||||||
url: None,
|
url: None,
|
||||||
albums: None,
|
|
||||||
tracks: None,
|
tracks: None,
|
||||||
is_followed: false,
|
is_followed: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_albums(&mut self, spotify: Spotify) {
|
fn load_top_tracks(&mut self, spotify: Spotify) {
|
||||||
if let Some(albums) = self.albums.as_mut() {
|
if let Some(artist_id) = &self.id {
|
||||||
for album in albums {
|
if self.tracks.is_none() {
|
||||||
album.load_tracks(spotify.clone());
|
self.tracks = spotify.artist_top_tracks(artist_id);
|
||||||
}
|
}
|
||||||
} else if let Some(ref artist_id) = self.id {
|
|
||||||
let mut collected_ids: Vec<String> = 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<Vec<&Track>> {
|
|
||||||
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(),
|
id: sa.id.clone(),
|
||||||
name: sa.name.clone(),
|
name: sa.name.clone(),
|
||||||
url: sa.uri.clone(),
|
url: sa.uri.clone(),
|
||||||
albums: None,
|
|
||||||
tracks: None,
|
tracks: None,
|
||||||
is_followed: false,
|
is_followed: false,
|
||||||
}
|
}
|
||||||
@@ -108,7 +58,6 @@ impl From<&FullArtist> for Artist {
|
|||||||
id: Some(fa.id.clone()),
|
id: Some(fa.id.clone()),
|
||||||
name: fa.name.clone(),
|
name: fa.name.clone(),
|
||||||
url: Some(fa.uri.clone()),
|
url: Some(fa.uri.clone()),
|
||||||
albums: None,
|
|
||||||
tracks: None,
|
tracks: None,
|
||||||
is_followed: false,
|
is_followed: false,
|
||||||
}
|
}
|
||||||
@@ -129,7 +78,7 @@ impl fmt::Debug for Artist {
|
|||||||
|
|
||||||
impl ListItem for Artist {
|
impl ListItem for Artist {
|
||||||
fn is_playing(&self, queue: Arc<Queue>) -> bool {
|
fn is_playing(&self, queue: Arc<Queue>) -> bool {
|
||||||
if let Some(tracks) = self.tracks() {
|
if let Some(tracks) = &self.tracks {
|
||||||
let playing: Vec<String> = queue
|
let playing: Vec<String> = queue
|
||||||
.queue
|
.queue
|
||||||
.read()
|
.read()
|
||||||
@@ -173,7 +122,7 @@ impl ListItem for Artist {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn play(&mut self, queue: Arc<Queue>) {
|
fn play(&mut self, queue: Arc<Queue>) {
|
||||||
self.load_albums(queue.get_spotify());
|
self.load_top_tracks(queue.get_spotify());
|
||||||
|
|
||||||
if let Some(tracks) = self.tracks.as_ref() {
|
if let Some(tracks) = self.tracks.as_ref() {
|
||||||
let tracks: Vec<Playable> = tracks
|
let tracks: Vec<Playable> = tracks
|
||||||
@@ -186,7 +135,7 @@ impl ListItem for Artist {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn play_next(&mut self, queue: Arc<Queue>) {
|
fn play_next(&mut self, queue: Arc<Queue>) {
|
||||||
self.load_albums(queue.get_spotify());
|
self.load_top_tracks(queue.get_spotify());
|
||||||
|
|
||||||
if let Some(tracks) = self.tracks.as_ref() {
|
if let Some(tracks) = self.tracks.as_ref() {
|
||||||
for t in tracks.iter().rev() {
|
for t in tracks.iter().rev() {
|
||||||
@@ -196,9 +145,9 @@ impl ListItem for Artist {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn queue(&mut self, queue: Arc<Queue>) {
|
fn queue(&mut self, queue: Arc<Queue>) {
|
||||||
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 {
|
for t in tracks {
|
||||||
queue.append(Playable::Track(t.clone()));
|
queue.append(Playable::Track(t.clone()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,14 @@ use librespot_playback::config::Bitrate;
|
|||||||
use librespot_playback::player::Player;
|
use librespot_playback::player::Player;
|
||||||
|
|
||||||
use rspotify::blocking::client::Spotify as SpotifyAPI;
|
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::artist::FullArtist;
|
||||||
use rspotify::model::page::{CursorBasedPage, Page};
|
use rspotify::model::page::{CursorBasedPage, Page};
|
||||||
use rspotify::model::playlist::{FullPlaylist, PlaylistTrack, SimplifiedPlaylist};
|
use rspotify::model::playlist::{FullPlaylist, PlaylistTrack, SimplifiedPlaylist};
|
||||||
use rspotify::model::search::SearchResult;
|
use rspotify::model::search::SearchResult;
|
||||||
use rspotify::model::track::{FullTrack, SavedTrack, SimplifiedTrack};
|
use rspotify::model::track::{FullTrack, SavedTrack, SimplifiedTrack};
|
||||||
use rspotify::model::user::PrivateUser;
|
use rspotify::model::user::PrivateUser;
|
||||||
use rspotify::senum::SearchType;
|
use rspotify::senum::{AlbumType, SearchType};
|
||||||
use rspotify::{blocking::client::ApiError, senum::Country};
|
use rspotify::{blocking::client::ApiError, senum::Country};
|
||||||
|
|
||||||
use serde_json::{json, Map};
|
use serde_json::{json, Map};
|
||||||
@@ -49,6 +49,8 @@ use crate::playable::Playable;
|
|||||||
use crate::spotify_worker::{Worker, WorkerCommand};
|
use crate::spotify_worker::{Worker, WorkerCommand};
|
||||||
use crate::track::Track;
|
use crate::track::Track;
|
||||||
|
|
||||||
|
use crate::album::Album;
|
||||||
|
use crate::ui::pagination::{ApiPage, ApiResult};
|
||||||
use rspotify::model::recommend::Recommendations;
|
use rspotify::model::recommend::Recommendations;
|
||||||
use rspotify::model::show::{FullEpisode, FullShow, Show, SimplifiedEpisode};
|
use rspotify::model::show::{FullEpisode, FullShow, Show, SimplifiedEpisode};
|
||||||
|
|
||||||
@@ -481,16 +483,6 @@ impl Spotify {
|
|||||||
self.api_with_retry(|api| api.album(album_id))
|
self.api_with_retry(|api| api.album(album_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn albums(&self, album_ids: &[String]) -> Option<Vec<FullAlbum>> {
|
|
||||||
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<FullArtist> {
|
pub fn artist(&self, artist_id: &str) -> Option<FullArtist> {
|
||||||
self.api_with_retry(|api| api.artist(artist_id))
|
self.api_with_retry(|api| api.artist(artist_id))
|
||||||
}
|
}
|
||||||
@@ -576,12 +568,39 @@ impl Spotify {
|
|||||||
pub fn artist_albums(
|
pub fn artist_albums(
|
||||||
&self,
|
&self,
|
||||||
artist_id: &str,
|
artist_id: &str,
|
||||||
limit: u32,
|
album_type: Option<AlbumType>,
|
||||||
offset: u32,
|
) -> ApiResult<Album> {
|
||||||
) -> Option<Page<SimplifiedAlbum>> {
|
const MAX_SIZE: u32 = 50;
|
||||||
self.api_with_retry(|api| {
|
let spotify = self.clone();
|
||||||
api.artist_albums(artist_id, None, self.country, Some(limit), Some(offset))
|
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<Album> = 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<Page<SimplifiedEpisode>> {
|
pub fn show_episodes(&self, show_id: &str, offset: u32) -> Option<Page<SimplifiedEpisode>> {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use std::thread;
|
|||||||
use cursive::view::ViewWrapper;
|
use cursive::view::ViewWrapper;
|
||||||
use cursive::Cursive;
|
use cursive::Cursive;
|
||||||
|
|
||||||
|
use crate::album::Album;
|
||||||
use crate::artist::Artist;
|
use crate::artist::Artist;
|
||||||
use crate::command::Command;
|
use crate::command::Command;
|
||||||
use crate::commands::CommandResult;
|
use crate::commands::CommandResult;
|
||||||
@@ -13,6 +14,7 @@ use crate::track::Track;
|
|||||||
use crate::traits::ViewExt;
|
use crate::traits::ViewExt;
|
||||||
use crate::ui::listview::ListView;
|
use crate::ui::listview::ListView;
|
||||||
use crate::ui::tabview::TabView;
|
use crate::ui::tabview::TabView;
|
||||||
|
use rspotify::senum::AlbumType;
|
||||||
|
|
||||||
pub struct ArtistView {
|
pub struct ArtistView {
|
||||||
artist: Artist,
|
artist: Artist,
|
||||||
@@ -21,16 +23,12 @@ pub struct ArtistView {
|
|||||||
|
|
||||||
impl ArtistView {
|
impl ArtistView {
|
||||||
pub fn new(queue: Arc<Queue>, library: Arc<Library>, artist: &Artist) -> Self {
|
pub fn new(queue: Arc<Queue>, library: Arc<Library>, artist: &Artist) -> Self {
|
||||||
let mut artist = artist.clone();
|
|
||||||
|
|
||||||
let spotify = queue.get_spotify();
|
let spotify = queue.get_spotify();
|
||||||
artist.load_albums(spotify.clone());
|
|
||||||
|
|
||||||
let albums = if let Some(a) = artist.albums.as_ref() {
|
let albums_view =
|
||||||
a.clone()
|
Self::albums_view(&artist, AlbumType::Album, queue.clone(), library.clone());
|
||||||
} else {
|
let singles_view =
|
||||||
Vec::new()
|
Self::albums_view(&artist, AlbumType::Single, queue.clone(), library.clone());
|
||||||
};
|
|
||||||
|
|
||||||
let top_tracks: Arc<RwLock<Vec<Track>>> = Arc::new(RwLock::new(Vec::new()));
|
let top_tracks: Arc<RwLock<Vec<Track>>> = Arc::new(RwLock::new(Vec::new()));
|
||||||
{
|
{
|
||||||
@@ -85,15 +83,8 @@ impl ArtistView {
|
|||||||
ListView::new(top_tracks, queue.clone(), library.clone()),
|
ListView::new(top_tracks, queue.clone(), library.clone()),
|
||||||
);
|
);
|
||||||
|
|
||||||
tabs.add_tab(
|
tabs.add_tab("albums", "Albums", albums_view);
|
||||||
"albums",
|
tabs.add_tab("singles", "Singles", singles_view);
|
||||||
"Albums",
|
|
||||||
ListView::new(
|
|
||||||
Arc::new(RwLock::new(albums)),
|
|
||||||
queue.clone(),
|
|
||||||
library.clone(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
tabs.add_tab(
|
tabs.add_tab(
|
||||||
"related",
|
"related",
|
||||||
@@ -101,7 +92,39 @@ impl ArtistView {
|
|||||||
ListView::new(related, queue, library),
|
ListView::new(related, queue, library),
|
||||||
);
|
);
|
||||||
|
|
||||||
Self { artist, tabs }
|
Self {
|
||||||
|
artist: artist.clone(),
|
||||||
|
tabs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn albums_view(
|
||||||
|
artist: &Artist,
|
||||||
|
album_type: AlbumType,
|
||||||
|
queue: Arc<Queue>,
|
||||||
|
library: Arc<Library>,
|
||||||
|
) -> ListView<Album> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ impl<I: ListItem> View for ListView<I> {
|
|||||||
|
|
||||||
self.scrollbar.draw(printer, |printer, i| {
|
self.scrollbar.draw(printer, |printer, i| {
|
||||||
// draw paginator after content
|
// draw paginator after content
|
||||||
if i == content.len() {
|
if i == content.len() && self.can_paginate() {
|
||||||
let style = ColorStyle::secondary();
|
let style = ColorStyle::secondary();
|
||||||
|
|
||||||
let max = self.pagination.max_content().unwrap();
|
let max = self.pagination.max_content().unwrap();
|
||||||
@@ -149,7 +149,7 @@ impl<I: ListItem> View for ListView<I> {
|
|||||||
printer.with_color(style, |printer| {
|
printer.with_color(style, |printer| {
|
||||||
printer.print((0, 0), &buf);
|
printer.print((0, 0), &buf);
|
||||||
});
|
});
|
||||||
} else {
|
} else if i < content.len() {
|
||||||
let item = &content[i];
|
let item = &content[i];
|
||||||
let currently_playing = item.is_playing(self.queue.clone())
|
let currently_playing = item.is_playing(self.queue.clone())
|
||||||
&& self.queue.get_current_index() == Some(i);
|
&& self.queue.get_current_index() == Some(i);
|
||||||
|
|||||||
Reference in New Issue
Block a user