@@ -31,6 +31,7 @@ pub enum LibraryTab {
|
||||
Artists,
|
||||
Playlists,
|
||||
Podcasts,
|
||||
Browse,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
|
||||
|
||||
66
src/model/category.rs
Normal file
66
src/model/category.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod album;
|
||||
pub mod artist;
|
||||
pub mod category;
|
||||
pub mod episode;
|
||||
pub mod playable;
|
||||
pub mod playlist;
|
||||
|
||||
@@ -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
56
src/ui/browse.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod album;
|
||||
pub mod artist;
|
||||
pub mod browse;
|
||||
pub mod contextmenu;
|
||||
pub mod help;
|
||||
pub mod layout;
|
||||
|
||||
@@ -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)),
|
||||
|
||||
Reference in New Issue
Block a user