Implement category playlist browsing in library

Fixes #187
This commit is contained in:
Henrik Friedrichsen
2022-08-20 22:27:23 +02:00
parent a04bc40051
commit 8905db457a
9 changed files with 185 additions and 4 deletions

View File

@@ -31,6 +31,7 @@ pub enum LibraryTab {
Artists,
Playlists,
Podcasts,
Browse,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone)]

66
src/model/category.rs Normal file
View File

@@ -0,0 +1,66 @@
use std::sync::Arc;
use crate::{
traits::{IntoBoxedViewExt, ListItem},
ui::listview::ListView,
};
#[derive(Clone, Deserialize, Serialize)]
pub struct Category {
pub id: String,
pub name: String,
}
impl From<&rspotify::model::Category> for Category {
fn from(c: &rspotify::model::Category) -> Self {
Category {
id: c.id.clone(),
name: c.name.clone(),
}
}
}
impl ListItem for Category {
fn is_playing(&self, _queue: Arc<crate::queue::Queue>) -> bool {
false
}
fn display_left(&self, _library: Arc<crate::library::Library>) -> String {
self.name.clone()
}
fn display_right(&self, _library: Arc<crate::library::Library>) -> String {
"".to_string()
}
fn play(&mut self, _queue: Arc<crate::queue::Queue>) {}
fn play_next(&mut self, _queue: Arc<crate::queue::Queue>) {}
fn queue(&mut self, _queue: Arc<crate::queue::Queue>) {}
fn toggle_saved(&mut self, _library: Arc<crate::library::Library>) {}
fn save(&mut self, _library: Arc<crate::library::Library>) {}
fn unsave(&mut self, _library: Arc<crate::library::Library>) {}
fn open(
&self,
queue: Arc<crate::queue::Queue>,
library: Arc<crate::library::Library>,
) -> Option<Box<dyn crate::traits::ViewExt>> {
let playlists = queue.get_spotify().api.category_playlists(&self.id);
let view = ListView::new(playlists.items.clone(), queue, library).with_title(&self.name);
playlists.apply_pagination(view.get_pagination());
Some(view.into_boxed_view_ext())
}
fn share_url(&self) -> Option<String> {
Some(format!("https://open.spotify.com/genre/{}", self.id))
}
fn as_listitem(&self) -> Box<dyn ListItem> {
Box::new(self.clone())
}
}

View File

@@ -1,5 +1,6 @@
pub mod album;
pub mod artist;
pub mod category;
pub mod episode;
pub mod playable;
pub mod playlist;

View File

@@ -1,5 +1,6 @@
use crate::model::album::Album;
use crate::model::artist::Artist;
use crate::model::category::Category;
use crate::model::episode::Episode;
use crate::model::playable::Playable;
use crate::model::playlist::Playlist;
@@ -625,6 +626,55 @@ impl WebApi {
.map(|fa| fa.iter().map(|a| a.into()).collect())
}
pub fn categories(&self) -> ApiResult<Category> {
const MAX_LIMIT: u32 = 50;
let spotify = self.clone();
let fetch_page = move |offset: u32| {
debug!("fetching categories, offset: {}", offset);
spotify.api_with_retry(|api| {
match api.categories_manual(
None,
Some(&Market::FromToken),
Some(MAX_LIMIT),
Some(offset),
) {
Ok(page) => Ok(ApiPage {
offset: page.offset,
total: page.total,
items: page.items.iter().map(|cat| cat.into()).collect(),
}),
Err(e) => Err(e),
}
})
};
ApiResult::new(MAX_LIMIT, Arc::new(fetch_page))
}
pub fn category_playlists(&self, category_id: &str) -> ApiResult<Playlist> {
const MAX_LIMIT: u32 = 50;
let spotify = self.clone();
let category_id = category_id.to_string();
let fetch_page = move |offset: u32| {
debug!("fetching category playlists, offset: {}", offset);
spotify.api_with_retry(|api| {
match api.category_playlists_manual(
&category_id,
Some(&Market::FromToken),
Some(MAX_LIMIT),
Some(offset),
) {
Ok(page) => Ok(ApiPage {
offset: page.offset,
total: page.total,
items: page.items.iter().map(|sp| sp.into()).collect(),
}),
Err(e) => Err(e),
}
})
};
ApiResult::new(MAX_LIMIT, Arc::new(fetch_page))
}
pub fn current_user(&self) -> Option<PrivateUser> {
self.api_with_retry(|api| api.current_user())
}

56
src/ui/browse.rs Normal file
View File

@@ -0,0 +1,56 @@
use std::sync::{Arc, RwLock};
use cursive::view::ViewWrapper;
use cursive::Cursive;
use crate::command::Command;
use crate::commands::CommandResult;
use crate::library::Library;
use crate::model::category::Category;
use crate::queue::Queue;
use crate::traits::ViewExt;
use crate::ui::listview::ListView;
pub struct BrowseView {
list: ListView<Category>,
}
impl BrowseView {
pub fn new(queue: Arc<Queue>, library: Arc<Library>) -> Self {
let items = Arc::new(RwLock::new(Vec::new()));
let list = ListView::new(items.clone(), queue.clone(), library);
let pagination = list.get_pagination().clone();
std::thread::spawn(move || {
let categories = queue.get_spotify().api.categories();
items
.write()
.expect("could not writelock category items")
.extend(
categories
.items
.read()
.expect("could not readlock fetched categories")
.clone(),
);
categories.apply_pagination(&pagination);
});
Self { list }
}
}
impl ViewWrapper for BrowseView {
wrap_impl!(self.list: ListView<Category>);
}
impl ViewExt for BrowseView {
fn title(&self) -> String {
"Browse".to_string()
}
fn on_command(&mut self, s: &mut Cursive, cmd: &Command) -> Result<CommandResult, String> {
self.list.on_command(s, cmd)
}
}

View File

@@ -18,10 +18,7 @@ use crate::sharing::write_share;
use crate::traits::{ListItem, ViewExt};
use crate::ui::layout::Layout;
use crate::ui::modal::Modal;
use crate::{
command::{Command, MoveAmount, MoveMode},
spotify::Spotify,
};
use crate::{command::Command, spotify::Spotify};
use cursive::traits::{Finder, Nameable};
pub struct ContextMenu {

View File

@@ -10,6 +10,7 @@ use crate::config::LibraryTab;
use crate::library::Library;
use crate::queue::Queue;
use crate::traits::ViewExt;
use crate::ui::browse::BrowseView;
use crate::ui::listview::ListView;
use crate::ui::playlists::PlaylistsView;
use crate::ui::tabview::TabView;
@@ -55,6 +56,9 @@ impl LibraryView {
ListView::new(library.shows.clone(), queue.clone(), library.clone())
.with_title("Podcasts"),
),
LibraryTab::Browse => {
tabview.add_tab("browse", BrowseView::new(queue.clone(), library.clone()))
}
}
}

View File

@@ -1,5 +1,6 @@
pub mod album;
pub mod artist;
pub mod browse;
pub mod contextmenu;
pub mod help;
pub mod layout;

View File

@@ -21,6 +21,11 @@ impl<I: ListItem + Clone> ApiResult<I> {
pub fn new(limit: u32, fetch_page: Arc<FetchPageFn<I>>) -> ApiResult<I> {
let items = Arc::new(RwLock::new(Vec::new()));
if let Some(first_page) = fetch_page(0) {
debug!(
"fetched first page, items: {}, total: {}",
first_page.items.len(),
first_page.total
);
items.write().unwrap().extend(first_page.items);
ApiResult {
offset: Arc::new(RwLock::new(first_page.offset)),