From c5d666f35c2762940f5d3b622a1cf56a8213c8db Mon Sep 17 00:00:00 2001 From: Thomas Frans Date: Thu, 1 Feb 2024 19:42:53 +0100 Subject: [PATCH] docs: small overall documentation improvements (#1381) * docs: small overall documentation improvements - Add documentation comments to various items - Change web API return types from bool/Option to Result - Create helper functions with descriptive names instead of comments - Remove redundant/confusing types - Fix some documentation comments as instructed by `cargo doc` - Rename variables to clear names * docs: small fixes to the documentation update --- src/commands.rs | 4 +- src/config.rs | 17 +++- src/events.rs | 11 ++- src/library.rs | 48 +++++---- src/model/album.rs | 16 +-- src/model/artist.rs | 3 +- src/model/playlist.rs | 8 +- src/model/track.rs | 3 +- src/mpris.rs | 15 +-- src/queue.rs | 5 +- src/spotify.rs | 64 +++++++++--- src/spotify_api.rs | 204 ++++++++++++++++++++++++++------------- src/theme.rs | 12 +++ src/traits.rs | 1 + src/ui/artist.rs | 4 +- src/ui/listview.rs | 20 ++-- src/ui/search_results.rs | 24 ++--- src/ui/tabbedview.rs | 4 +- 18 files changed, 312 insertions(+), 151 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index 8c4ab3999..d9319b89b 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -238,8 +238,8 @@ impl CommandManager { } Command::NewPlaylist(name) => { match self.spotify.api.create_playlist(name, None, None) { - Some(_) => self.library.update_library(), - None => error!("could not create playlist {}", name), + Ok(_) => self.library.update_library(), + Err(_) => error!("could not create playlist {}", name), } Ok(None) } diff --git a/src/config.rs b/src/config.rs index e3e7cca3c..ed5958e25 100644 --- a/src/config.rs +++ b/src/config.rs @@ -233,14 +233,17 @@ impl Config { } } + /// Get the user configuration values. pub fn values(&self) -> RwLockReadGuard { self.values.read().expect("can't readlock config values") } + /// Get the runtime user state values. pub fn state(&self) -> RwLockReadGuard { self.state.read().expect("can't readlock user state") } + /// Modify the internal user state through a shared reference using a closure. pub fn with_state_mut(&self, cb: F) where F: Fn(RwLockWriteGuard), @@ -249,9 +252,15 @@ impl Config { cb(state_guard); } - pub fn save_state(&self) { - // update cache version number + /// Update the version number of the runtime user state. This should be done before saving it to + /// disk. + fn update_state_cache_version(&self) { self.with_state_mut(|mut state| state.cache_version = CACHE_VERSION); + } + + /// Save runtime state to the user configuration directory. + pub fn save_state(&self) { + self.update_state_cache_version(); let path = config_path("userstate.cbor"); debug!("saving user state to {}", path.display()); @@ -260,9 +269,9 @@ impl Config { } } + /// Create a [Theme] from the user supplied theme in the configuration file. pub fn build_theme(&self) -> Theme { - let theme = &self.values().theme; - crate::theme::load(theme) + crate::theme::load(&self.values().theme) } /// Attempt to reload the configuration from the configuration file. diff --git a/src/events.rs b/src/events.rs index dea835726..fda79f822 100644 --- a/src/events.rs +++ b/src/events.rs @@ -4,6 +4,7 @@ use cursive::{CbSink, Cursive}; use crate::queue::QueueEvent; use crate::spotify::PlayerEvent; +/// Events that can be sent to and handled by the main event loop (the one drawing the TUI). pub enum Event { Player(PlayerEvent), Queue(QueueEvent), @@ -11,11 +12,10 @@ pub enum Event { IpcInput(String), } -pub type EventSender = Sender; - +/// Manager that can be used to send and receive messages across threads. #[derive(Clone)] pub struct EventManager { - tx: EventSender, + tx: Sender, rx: Receiver, cursive_sink: CbSink, } @@ -31,17 +31,20 @@ impl EventManager { } } + /// Return a non-blocking iterator over the messages awaiting handling. Calling `next()` on the + /// iterator never blocks. pub fn msg_iter(&self) -> TryIter { self.rx.try_iter() } + /// Send a new event to be handled. pub fn send(&self, event: Event) { self.tx.send(event).expect("could not send event"); self.trigger(); } + /// Send a no-op to the Cursive event loop to trigger immediate processing of events. pub fn trigger(&self) { - // send a no-op to trigger event loop processing self.cursive_sink .send(Box::new(Cursive::noop)) .expect("could not send no-op event to cursive"); diff --git a/src/library.rs b/src/library.rs index f9b6b8108..9fdddb2d2 100644 --- a/src/library.rs +++ b/src/library.rs @@ -21,11 +21,20 @@ use crate::model::show::Show; use crate::model::track::Track; use crate::spotify::Spotify; +/// Cached tracks database filename. const CACHE_TRACKS: &str = "tracks.db"; + +/// Cached albums database filename. const CACHE_ALBUMS: &str = "albums.db"; + +/// Cached artists database filename. const CACHE_ARTISTS: &str = "artists.db"; + +/// Cached playlists database filename. const CACHE_PLAYLISTS: &str = "playlists.db"; +/// The user library with all their saved tracks, albums, playlists... High level interface to the +/// Spotify API used to manage items in the user library. #[derive(Clone)] pub struct Library { pub tracks: Arc>>, @@ -43,7 +52,7 @@ pub struct Library { impl Library { pub fn new(ev: EventManager, spotify: Spotify, cfg: Arc) -> Self { - let current_user = spotify.api.current_user(); + let current_user = spotify.api.current_user().ok(); let user_id = current_user.as_ref().map(|u| u.id.id().to_string()); let display_name = current_user.as_ref().and_then(|u| u.display_name.clone()); @@ -149,7 +158,7 @@ impl Library { .position(|i| i.id == id); if let Some(position) = position { - if self.spotify.api.delete_playlist(id) { + if self.spotify.api.delete_playlist(id).is_ok() { self.playlists .write() .expect("can't writelock playlists") @@ -163,7 +172,7 @@ impl Library { } /// Set the playlist with `id` to contain only `tracks`. If the playlist already contains - /// tracks, they will be removed. + /// tracks, they will be removed. Update the cache to match the new state. pub fn overwrite_playlist(&self, id: &str, tracks: &[Playable]) { debug!("saving {} tracks to list {}", tracks.len(), id); self.spotify.api.overwrite_playlist(id, tracks); @@ -179,12 +188,12 @@ impl Library { pub fn save_playlist(&self, name: &str, tracks: &[Playable]) { debug!("saving {} tracks to new list {}", tracks.len(), name); match self.spotify.api.create_playlist(name, None, None) { - Some(id) => self.overwrite_playlist(&id, tracks), - None => error!("could not create new playlist.."), + Ok(id) => self.overwrite_playlist(&id, tracks), + Err(_) => error!("could not create new playlist.."), } } - /// Update the local copy and cache of the library with the remote data. + /// Update the local library and its cache on disk. pub fn update_library(&self) { *self.is_done.write().unwrap() = false; @@ -278,7 +287,7 @@ impl Library { debug!("loading shows"); let mut saved_shows: Vec = Vec::new(); - let mut shows_result = self.spotify.api.get_saved_shows(0); + let mut shows_result = self.spotify.api.get_saved_shows(0).ok(); while let Some(shows) = shows_result { saved_shows.extend(shows.items.iter().map(|show| (&show.show).into())); @@ -290,6 +299,7 @@ impl Library { self.spotify .api .get_saved_shows(shows.offset + shows.items.len() as u32) + .ok() } None => None, } @@ -364,7 +374,7 @@ impl Library { let page = self.spotify.api.current_user_followed_artists(last); debug!("artists page: {}", i); i += 1; - if page.is_none() { + if page.is_err() { error!("Failed to fetch artists."); return; } @@ -422,7 +432,7 @@ impl Library { i += 1; - if page.is_none() { + if page.is_err() { error!("Failed to fetch albums."); return; } @@ -465,7 +475,7 @@ impl Library { debug!("tracks page: {}", i); i += 1; - if page.is_none() { + if page.is_err() { error!("Failed to fetch tracks."); return; } @@ -604,7 +614,7 @@ impl Library { .api .current_user_saved_tracks_add(tracks.iter().filter_map(|t| t.id.as_deref()).collect()); - if save_tracks_result.is_none() { + if save_tracks_result.is_err() { return; } @@ -645,7 +655,7 @@ impl Library { .current_user_saved_tracks_delete( tracks.iter().filter_map(|t| t.id.as_deref()).collect(), ) - .is_none() + .is_err() { return; } @@ -692,7 +702,7 @@ impl Library { .spotify .api .current_user_saved_albums_add(vec![album_id.as_str()]) - .is_none() + .is_err() { return; } @@ -725,7 +735,7 @@ impl Library { .spotify .api .current_user_saved_albums_delete(vec![album_id.as_str()]) - .is_none() + .is_err() { return; } @@ -763,7 +773,7 @@ impl Library { .spotify .api .user_follow_artists(vec![artist_id.as_str()]) - .is_none() + .is_err() { return; } @@ -799,7 +809,7 @@ impl Library { .spotify .api .user_unfollow_artists(vec![artist_id.as_str()]) - .is_none() + .is_err() { return; } @@ -846,7 +856,7 @@ impl Library { let follow_playlist_result = self.spotify.api.user_playlist_follow_playlist(&playlist.id); - if follow_playlist_result.is_none() { + if follow_playlist_result.is_err() { return; } @@ -881,7 +891,7 @@ impl Library { return; } - if self.spotify.api.save_shows(&[show.id.as_str()]) { + if self.spotify.api.save_shows(&[show.id.as_str()]).is_ok() { { let mut store = self.shows.write().unwrap(); if !store.iter().any(|s| s.id == show.id) { @@ -897,7 +907,7 @@ impl Library { return; } - if self.spotify.api.unsave_shows(&[show.id.as_str()]) { + if self.spotify.api.unsave_shows(&[show.id.as_str()]).is_ok() { let mut store = self.shows.write().unwrap(); *store = store.iter().filter(|s| s.id != show.id).cloned().collect(); } diff --git a/src/model/album.rs b/src/model/album.rs index 96584e570..f0c291152 100644 --- a/src/model/album.rs +++ b/src/model/album.rs @@ -38,7 +38,7 @@ impl Album { if let Some(ref album_id) = self.id { let mut collected_tracks = Vec::new(); - if let Some(full_album) = spotify.api.album(album_id) { + if let Ok(full_album) = spotify.api.album(album_id) { let mut tracks_result = Some(full_album.tracks.clone()); while let Some(ref tracks) = tracks_result { for t in &tracks.items { @@ -51,11 +51,14 @@ impl Album { tracks_result = match tracks.next { Some(_) => { debug!("requesting tracks again.."); - spotify.api.album_tracks( - album_id, - 50, - tracks.offset + tracks.items.len() as u32, - ) + spotify + .api + .album_tracks( + album_id, + 50, + tracks.offset + tracks.items.len() as u32, + ) + .ok() } None => None, } @@ -273,6 +276,7 @@ impl ListItem for Album { None, Some(track_ids), ) + .ok() .map(|r| r.tracks) .map(|tracks| tracks.iter().map(Track::from).collect()); recommendations.map(|tracks| { diff --git a/src/model/artist.rs b/src/model/artist.rs index f7d41461f..6bc7d3587 100644 --- a/src/model/artist.rs +++ b/src/model/artist.rs @@ -35,7 +35,7 @@ impl Artist { fn load_top_tracks(&mut self, spotify: Spotify) { if let Some(artist_id) = &self.id { if self.tracks.is_none() { - self.tracks = spotify.api.artist_top_tracks(artist_id); + self.tracks = spotify.api.artist_top_tracks(artist_id).ok(); } } } @@ -182,6 +182,7 @@ impl ListItem for Artist { let recommendations: Option> = spotify .api .recommendations(Some(vec![&id]), None, None) + .ok() .map(|r| r.tracks) .map(|tracks| tracks.iter().map(Track::from).collect()); diff --git a/src/model/playlist.rs b/src/model/playlist.rs index 8d88d8e73..12c4c83d0 100644 --- a/src/model/playlist.rs +++ b/src/model/playlist.rs @@ -67,6 +67,7 @@ impl Playlist { match spotify .api .delete_tracks(&self.id, &self.snapshot_id, &[playable]) + .is_ok() { false => false, true => { @@ -83,7 +84,11 @@ impl Playlist { pub fn append_tracks(&mut self, new_tracks: &[Playable], spotify: &Spotify, library: &Library) { let mut has_modified = false; - if spotify.api.append_tracks(&self.id, new_tracks, None) { + if spotify + .api + .append_tracks(&self.id, new_tracks, None) + .is_ok() + { if let Some(tracks) = &mut self.tracks { tracks.append(&mut new_tracks.to_vec()); has_modified = true; @@ -304,6 +309,7 @@ impl ListItem for Playlist { None, Some(track_ids.iter().map(|t| t.as_ref()).collect()), ) + .ok() .map(|r| r.tracks) .map(|tracks| tracks.iter().map(Track::from).collect()); diff --git a/src/model/track.rs b/src/model/track.rs index 201119d4c..e68002700 100644 --- a/src/model/track.rs +++ b/src/model/track.rs @@ -278,6 +278,7 @@ impl ListItem for Track { spotify .api .recommendations(None, None, Some(vec![id])) + .ok() .map(|r| r.tracks) .map(|tracks| tracks.iter().map(Self::from).collect()) } else { @@ -309,7 +310,7 @@ impl ListItem for Track { let spotify = queue.get_spotify(); match self.album_id { - Some(ref album_id) => spotify.api.album(album_id).map(|ref fa| fa.into()), + Some(ref album_id) => spotify.api.album(album_id).map(|ref fa| fa.into()).ok(), None => None, } } diff --git a/src/mpris.rs b/src/mpris.rs index 96feaf278..470f1ae6b 100644 --- a/src/mpris.rs +++ b/src/mpris.rs @@ -137,6 +137,7 @@ impl MprisPlayer { .track(&track.id.unwrap_or_default()) .as_ref() .map(|t| Playable::Track(t.into())) + .ok() } } Playable::Episode(episode) => Some(Playable::Episode(episode)), @@ -386,7 +387,7 @@ impl MprisPlayer { let uri_type = spotify_url.map(|s| s.uri_type); match uri_type { Some(UriType::Album) => { - if let Some(a) = self.spotify.api.album(&id) { + if let Ok(a) = self.spotify.api.album(&id) { if let Some(t) = &Album::from(&a).tracks { let should_shuffle = self.queue.get_shuffle(); self.queue.clear(); @@ -400,14 +401,14 @@ impl MprisPlayer { } } Some(UriType::Track) => { - if let Some(t) = self.spotify.api.track(&id) { + if let Ok(t) = self.spotify.api.track(&id) { self.queue.clear(); self.queue.append(Playable::Track(Track::from(&t))); self.queue.play(0, false, false) } } Some(UriType::Playlist) => { - if let Some(p) = self.spotify.api.playlist(&id) { + if let Ok(p) = self.spotify.api.playlist(&id) { let mut playlist = Playlist::from(&p); playlist.load_tracks(&self.spotify); if let Some(tracks) = &playlist.tracks { @@ -419,7 +420,7 @@ impl MprisPlayer { } } Some(UriType::Show) => { - if let Some(s) = self.spotify.api.get_show(&id) { + if let Ok(s) = self.spotify.api.show(&id) { let mut show: Show = (&s).into(); let spotify = self.spotify.clone(); show.load_all_episodes(spotify); @@ -438,14 +439,14 @@ impl MprisPlayer { } } Some(UriType::Episode) => { - if let Some(e) = self.spotify.api.episode(&id) { + if let Ok(e) = self.spotify.api.episode(&id) { self.queue.clear(); self.queue.append(Playable::Episode(Episode::from(&e))); self.queue.play(0, false, false) } } Some(UriType::Artist) => { - if let Some(a) = self.spotify.api.artist_top_tracks(&id) { + if let Ok(a) = self.spotify.api.artist_top_tracks(&id) { let should_shuffle = self.queue.get_shuffle(); self.queue.clear(); let index = self.queue.append_next( @@ -527,7 +528,7 @@ impl MprisManager { /// Get the D-Bus bus name for this instance according to the MPRIS specification. /// -/// https://specifications.freedesktop.org/mpris-spec/2.2/#Bus-Name-Policy +/// pub fn instance_bus_name() -> String { format!( "org.mpris.MediaPlayer2.ncspot.instance{}", diff --git a/src/queue.rs b/src/queue.rs index 9b030f3f8..a981d06b1 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -33,9 +33,8 @@ pub enum QueueEvent { PreloadTrackRequest, } -/// The queue determines the playback order of -/// [Playable](crate::model::playable::Playable) items, and is also used to -/// control playback itself. +/// The queue determines the playback order of [Playable] items, and is also used to control +/// playback itself. pub struct Queue { /// The internal data, which doesn't change with shuffle or repeat. This is /// the raw data only. diff --git a/src/spotify.rs b/src/spotify.rs index c8ce276ef..d6555efae 100644 --- a/src/spotify.rs +++ b/src/spotify.rs @@ -1,28 +1,25 @@ +use std::env; +use std::str::FromStr; +use std::sync::{Arc, RwLock}; +use std::time::{Duration, SystemTime}; + +use futures::channel::oneshot; use librespot_core::authentication::Credentials; use librespot_core::cache::Cache; use librespot_core::config::SessionConfig; use librespot_core::session::Session; use librespot_core::session::SessionError; +use librespot_playback::audio_backend; use librespot_playback::audio_backend::SinkBuilder; +use librespot_playback::config::Bitrate; use librespot_playback::config::PlayerConfig; use librespot_playback::mixer::softmixer::SoftMixer; use librespot_playback::mixer::MixerConfig; -use log::{debug, error, info}; - -use librespot_playback::audio_backend; -use librespot_playback::config::Bitrate; use librespot_playback::player::Player; - -use futures::channel::oneshot; +use log::{debug, error, info}; use tokio::sync::mpsc; - use url::Url; -use std::env; -use std::str::FromStr; -use std::sync::{Arc, RwLock}; -use std::time::{Duration, SystemTime}; - use crate::application::ASYNC_RUNTIME; use crate::config; use crate::events::{Event, EventManager}; @@ -30,8 +27,11 @@ use crate::model::playable::Playable; use crate::spotify_api::WebApi; use crate::spotify_worker::{Worker, WorkerCommand}; +/// One percent of the maximum supported [Player] volume, used when setting the volume to a certain +/// percent. pub const VOLUME_PERCENT: u16 = ((u16::max_value() as f64) * 1.0 / 100.0) as u16; +/// Events sent by the [Player]. #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub enum PlayerEvent { Playing(SystemTime), @@ -40,18 +40,23 @@ pub enum PlayerEvent { FinishedTrack, } -// TODO: Rename or document this as it isn't immediately clear what it represents/does from the -// name. +/// Wrapper around a worker thread that exposes methods to safely control it. #[derive(Clone)] pub struct Spotify { events: EventManager, + /// The credentials for the currently logged in user, used to authenticate to the Spotify API. credentials: Credentials, cfg: Arc, + /// Playback status of the [Player] owned by the worker thread. status: Arc>, pub api: WebApi, + /// The amount of the current [Playable] that had elapsed when last paused. elapsed: Arc>>, + /// The amount of the current [Playable] that has been played in total. since: Arc>>, + /// Channel to send commands to the worker thread. channel: Arc>>>, + /// The username of the logged in user. user: Option, } @@ -83,6 +88,8 @@ impl Spotify { spotify } + /// Start the worker thread. If `user_tx` is given, it will receive the username of the logged + /// in user. pub fn start_worker(&self, user_tx: Option>) { let (tx, rx) = mpsc::unbounded_channel(); *self @@ -107,6 +114,7 @@ impl Spotify { } } + /// Generate the librespot [SessionConfig] used when creating a [Session]. pub fn session_config() -> SessionConfig { let mut session_config = SessionConfig::default(); match env::var("http_proxy") { @@ -119,6 +127,7 @@ impl Spotify { session_config } + /// Test whether `credentials` are valid Spotify credentials. pub fn test_credentials(credentials: Credentials) -> Result { let config = Self::session_config(); ASYNC_RUNTIME @@ -128,6 +137,8 @@ impl Spotify { .map(|r| r.0) } + /// Create a [Session] that respects the user configuration in `cfg` and with the given + /// credentials. async fn create_session( cfg: &config::Config, credentials: Credentials, @@ -153,6 +164,7 @@ impl Spotify { .map(|r| r.0) } + /// Create and initialize the requested audio backend. fn init_backend(desired_backend: Option) -> Option { let backend = if let Some(name) = desired_backend { audio_backend::BACKENDS @@ -174,6 +186,7 @@ impl Spotify { Some(backend.1) } + /// Create and run the worker thread. async fn worker( worker_channel: Arc>>>, events: EventManager, @@ -236,6 +249,7 @@ impl Spotify { events.send(Event::SessionDied) } + /// Get the current playback status of the [Player]. pub fn get_current_status(&self) -> PlayerEvent { let status = self .status @@ -244,6 +258,7 @@ impl Spotify { (*status).clone() } + /// Get the total amount of the current [Playable] that has been played. pub fn get_current_progress(&self) -> Duration { self.get_elapsed().unwrap_or_else(|| Duration::from_secs(0)) + self @@ -284,6 +299,8 @@ impl Spotify { *since } + /// Load `track` into the [Player]. Start playing immediately if + /// `start_playing` is true. Start playing from `position_ms` in the song. pub fn load(&self, track: &Playable, start_playing: bool, position_ms: u32) { info!("loading track: {:?}", track); self.send_worker(WorkerCommand::Load( @@ -293,6 +310,9 @@ impl Spotify { )); } + /// Update the cached status of the [Player]. This makes sure the status + /// doesn't have to be retrieved every time from the thread, which would be harder and more + /// expensive. pub fn update_status(&self, new_status: PlayerEvent) { match new_status { PlayerEvent::Paused(position) => { @@ -316,16 +336,20 @@ impl Spotify { *status = new_status; } + /// Reset the time tracking stats for the current song. This should be called when a new song is + /// loaded. pub fn update_track(&self) { self.set_elapsed(None); self.set_since(None); } + /// Start playback of the [Player]. pub fn play(&self) { info!("play()"); self.send_worker(WorkerCommand::Play); } + /// Toggle playback (play/pause) of the [Player]. pub fn toggleplayback(&self) { match self.get_current_status() { PlayerEvent::Playing(_) => self.pause(), @@ -334,6 +358,7 @@ impl Spotify { } } + /// Send a [WorkerCommand] to the worker thread. fn send_worker(&self, cmd: WorkerCommand) { info!("sending command to worker: {:?}", cmd); let channel = self.channel.read().expect("can't readlock worker channel"); @@ -350,45 +375,55 @@ impl Spotify { } } + /// Pause playback of the [Player]. pub fn pause(&self) { info!("pause()"); self.send_worker(WorkerCommand::Pause); } + /// Stop playback of the [Player]. pub fn stop(&self) { info!("stop()"); self.send_worker(WorkerCommand::Stop); } + /// Seek in the currently played [Playable] played by the [Player]. pub fn seek(&self, position_ms: u32) { self.send_worker(WorkerCommand::Seek(position_ms)); } + /// Seek relatively to the current playback position of the [Player]. pub fn seek_relative(&self, delta: i32) { let progress = self.get_current_progress(); let new = (progress.as_secs() * 1000) as i32 + progress.subsec_millis() as i32 + delta; self.seek(std::cmp::max(0, new) as u32); } + /// Get the current volume of the [Player]. pub fn volume(&self) -> u16 { self.cfg.state().volume } + /// Set the current volume of the [Player]. pub fn set_volume(&self, volume: u16) { info!("setting volume to {}", volume); self.cfg.with_state_mut(|mut s| s.volume = volume); self.send_worker(WorkerCommand::SetVolume(volume)); } + /// Preload the given [Playable] in the [Player]. This makes sure it can be played immediately + /// after the current [Playable] is finished. pub fn preload(&self, track: &Playable) { self.send_worker(WorkerCommand::Preload(track.clone())); } + /// Shut down the worker thread. pub fn shutdown(&self) { self.send_worker(WorkerCommand::Shutdown); } } +/// A type of Spotify URI. #[derive(Copy, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] pub enum UriType { Album, @@ -400,6 +435,7 @@ pub enum UriType { } impl UriType { + /// Try to create a [UriType] from the given string. pub fn from_uri(s: &str) -> Option { if s.starts_with("spotify:album:") { Some(Self::Album) diff --git a/src/spotify_api.rs b/src/spotify_api.rs index 552e52312..a2a7c16fb 100644 --- a/src/spotify_api.rs +++ b/src/spotify_api.rs @@ -1,35 +1,41 @@ -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; -use crate::model::track::Track; -use crate::spotify_worker::WorkerCommand; -use crate::ui::pagination::{ApiPage, ApiResult}; +use std::collections::HashSet; +use std::iter::FromIterator; +use std::sync::{Arc, RwLock}; +use std::thread; +use std::time::Duration; + use chrono::{DateTime, Duration as ChronoDuration, Utc}; use log::{debug, error, info}; - use rspotify::http::HttpError; use rspotify::model::{ AlbumId, AlbumType, ArtistId, CursorBasedPage, EpisodeId, FullAlbum, FullArtist, FullEpisode, FullPlaylist, FullShow, FullTrack, ItemPositions, Market, Page, PlayableId, PlaylistId, - PrivateUser, Recommendations, SavedAlbum, SavedTrack, SearchResult, SearchType, Show, ShowId, - SimplifiedTrack, TrackId, UserId, + PlaylistResult, PrivateUser, Recommendations, SavedAlbum, SavedTrack, SearchResult, SearchType, + Show, ShowId, SimplifiedTrack, TrackId, UserId, }; use rspotify::{prelude::*, AuthCodeSpotify, ClientError, ClientResult, Config, Token}; -use std::collections::HashSet; -use std::iter::FromIterator; -use std::sync::{Arc, RwLock}; -use std::thread; -use std::time::Duration; use tokio::sync::mpsc; +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; +use crate::model::track::Track; +use crate::spotify_worker::WorkerCommand; +use crate::ui::pagination::{ApiPage, ApiResult}; + +/// Convenient wrapper around the rspotify web API functionality. #[derive(Clone)] pub struct WebApi { + /// Rspotify web API. api: AuthCodeSpotify, + /// The username of the logged in user. user: Option, + /// Sender of the mpsc channel to the [Spotify](crate::spotify::Spotify) worker thread. worker_channel: Arc>>>, + /// Time at which the token expires. token_expiration: Arc>>, } @@ -58,10 +64,13 @@ impl WebApi { Self::default() } + /// Set the username for use with the API. pub fn set_user(&mut self, user: Option) { self.user = user; } + /// Set the sending end of the channel to the worker thread, managed by + /// [Spotify](crate::spotify::Spotify). pub(crate) fn set_worker_channel( &mut self, channel: Arc>>>, @@ -115,12 +124,12 @@ impl WebApi { } } - /// retries once when rate limits are hit - fn api_with_retry(&self, cb: F) -> Option + /// Execute `api_call` and retry once if a rate limit occurs. + fn api_with_retry(&self, api_call: F) -> Option where F: Fn(&AuthCodeSpotify) -> ClientResult, { - let result = { cb(&self.api) }; + let result = { api_call(&self.api) }; match result { Ok(v) => Some(v), Err(ClientError::Http(error)) => { @@ -133,12 +142,12 @@ impl WebApi { .and_then(|v| v.parse::().ok()); debug!("rate limit hit. waiting {:?} seconds", waiting_duration); thread::sleep(Duration::from_secs(waiting_duration.unwrap_or(0))); - cb(&self.api).ok() + api_call(&self.api).ok() } 401 => { debug!("token unauthorized. trying refresh.."); self.update_token(); - cb(&self.api).ok() + api_call(&self.api).ok() } _ => { error!("unhandled api error: {:?}", response); @@ -156,12 +165,13 @@ impl WebApi { } } + /// Append `tracks` at `position` in the playlist with `playlist_id`. pub fn append_tracks( &self, playlist_id: &str, tracks: &[Playable], position: Option, - ) -> bool { + ) -> Result { self.api_with_retry(|api| { let trackids: Vec = tracks .iter() @@ -173,7 +183,7 @@ impl WebApi { position, ) }) - .is_some() + .ok_or(()) } pub fn delete_tracks( @@ -181,7 +191,7 @@ impl WebApi { playlist_id: &str, snapshot_id: &str, playables: &[Playable], - ) -> bool { + ) -> Result { self.api_with_retry(move |api| { let playable_ids: Vec = playables .iter() @@ -205,9 +215,11 @@ impl WebApi { Some(snapshot_id), ) }) - .is_some() + .ok_or(()) } + /// Set the playlist with `id` to contain only `tracks`. If the playlist already contains + /// tracks, they will be removed. pub fn overwrite_playlist(&self, id: &str, tracks: &[Playable]) { // create mutable copy for chunking let mut tracks: Vec = tracks.to_vec(); @@ -239,7 +251,7 @@ impl WebApi { }; debug!("adding another {} tracks to playlist", tracks.len()); - if self.append_tracks(id, tracks, None) { + if self.append_tracks(id, tracks, None).is_ok() { debug!("{} tracks successfully added", tracks.len()); } else { error!("error saving tracks to playlists {}", id); @@ -251,17 +263,20 @@ impl WebApi { } } - pub fn delete_playlist(&self, id: &str) -> bool { + /// Delete the playlist with the given `id`. + pub fn delete_playlist(&self, id: &str) -> Result<(), ()> { self.api_with_retry(|api| api.playlist_unfollow(PlaylistId::from_id(id).unwrap())) - .is_some() + .ok_or(()) } + /// Create a playlist with the given `name`, `public` visibility and `description`. Returns the + /// id of the newly created playlist. pub fn create_playlist( &self, name: &str, public: Option, description: Option<&str>, - ) -> Option { + ) -> Result { let result = self.api_with_retry(|api| { api.user_playlist_create( UserId::from_id(self.user.as_ref().unwrap()).unwrap(), @@ -271,46 +286,59 @@ impl WebApi { description, ) }); - result.map(|r| r.id.id().to_string()) + result.map(|r| r.id.id().to_string()).ok_or(()) } - pub fn album(&self, album_id: &str) -> Option { + /// Fetch the album with the given `album_id`. + pub fn album(&self, album_id: &str) -> Result { debug!("fetching album {}", album_id); - let aid = AlbumId::from_id(album_id).ok()?; + let aid = AlbumId::from_id(album_id).map_err(|_| ())?; self.api_with_retry(|api| api.album(aid.clone(), Some(Market::FromToken))) + .ok_or(()) } - pub fn artist(&self, artist_id: &str) -> Option { - let aid = ArtistId::from_id(artist_id).ok()?; - self.api_with_retry(|api| api.artist(aid.clone())) + /// Fetch the artist with the given `artist_id`. + pub fn artist(&self, artist_id: &str) -> Result { + let aid = ArtistId::from_id(artist_id).map_err(|_| ())?; + self.api_with_retry(|api| api.artist(aid.clone())).ok_or(()) } - pub fn playlist(&self, playlist_id: &str) -> Option { - let pid = PlaylistId::from_id(playlist_id).ok()?; + /// Fetch the playlist with the given `playlist_id`. + pub fn playlist(&self, playlist_id: &str) -> Result { + let pid = PlaylistId::from_id(playlist_id).map_err(|_| ())?; self.api_with_retry(|api| api.playlist(pid.clone(), None, Some(Market::FromToken))) + .ok_or(()) } - pub fn track(&self, track_id: &str) -> Option { - let tid = TrackId::from_id(track_id).ok()?; + /// Fetch the track with the given `track_id`. + pub fn track(&self, track_id: &str) -> Result { + let tid = TrackId::from_id(track_id).map_err(|_| ())?; self.api_with_retry(|api| api.track(tid.clone(), Some(Market::FromToken))) + .ok_or(()) } - pub fn get_show(&self, show_id: &str) -> Option { - let sid = ShowId::from_id(show_id).ok()?; + /// Fetch the show with the given `show_id`. + pub fn show(&self, show_id: &str) -> Result { + let sid = ShowId::from_id(show_id).map_err(|_| ())?; self.api_with_retry(|api| api.get_a_show(sid.clone(), Some(Market::FromToken))) + .ok_or(()) } - pub fn episode(&self, episode_id: &str) -> Option { - let eid = EpisodeId::from_id(episode_id).ok()?; + /// Fetch the episode with the given `episode_id`. + pub fn episode(&self, episode_id: &str) -> Result { + let eid = EpisodeId::from_id(episode_id).map_err(|_| ())?; self.api_with_retry(|api| api.get_an_episode(eid.clone(), Some(Market::FromToken))) + .ok_or(()) } + /// Get recommendations based on the seeds provided with `seed_artists`, `seed_genres` and + /// `seed_tracks`. pub fn recommendations( &self, seed_artists: Option>, seed_genres: Option>, seed_tracks: Option>, - ) -> Option { + ) -> Result { self.api_with_retry(|api| { let seed_artistids = seed_artists.as_ref().map(|artistids| { artistids @@ -333,15 +361,18 @@ impl WebApi { Some(100), ) }) + .ok_or(()) } + /// Search for items of `searchtype` using the provided `query`. Limit the results to `limit` + /// items with the given `offset` from the start. pub fn search( &self, searchtype: SearchType, query: &str, limit: u32, offset: u32, - ) -> Option { + ) -> Result { self.api_with_retry(|api| { api.search( query, @@ -353,8 +384,10 @@ impl WebApi { ) }) .take() + .ok_or(()) } + /// Fetch all the current user's playlists. pub fn current_user_playlist(&self) -> ApiResult { const MAX_LIMIT: u32 = 50; let spotify = self.clone(); @@ -374,6 +407,7 @@ impl WebApi { ApiResult::new(MAX_LIMIT, Arc::new(fetch_page)) } + /// Get the tracks in the playlist given by `playlist_id`. pub fn user_playlist_tracks(&self, playlist_id: &str) -> ApiResult { const MAX_LIMIT: u32 = 100; let spotify = self.clone(); @@ -416,12 +450,14 @@ impl WebApi { ApiResult::new(MAX_LIMIT, Arc::new(fetch_page)) } + /// Fetch all the tracks in the album with the given `album_id`. Limit the results to `limit` + /// items, with `offset` from the beginning. pub fn album_tracks( &self, album_id: &str, limit: u32, offset: u32, - ) -> Option> { + ) -> Result, ()> { debug!("fetching album tracks {}", album_id); self.api_with_retry(|api| { api.album_track_manual( @@ -431,8 +467,11 @@ impl WebApi { Some(offset), ) }) + .ok_or(()) } + /// Fetch all the albums of the given `artist_id`. `album_type` determines which type of albums + /// to fetch. pub fn artist_albums( &self, artist_id: &str, @@ -469,6 +508,7 @@ impl WebApi { ApiResult::new(MAX_SIZE, Arc::new(fetch_page)) } + /// Get all the episodes of the show with the given `show_id`. pub fn show_episodes(&self, show_id: &str) -> ApiResult { const MAX_SIZE: u32 = 50; let spotify = self.clone(); @@ -495,11 +535,14 @@ impl WebApi { ApiResult::new(MAX_SIZE, Arc::new(fetch_page)) } - pub fn get_saved_shows(&self, offset: u32) -> Option> { + /// Get the user's saved shows. + pub fn get_saved_shows(&self, offset: u32) -> Result, ()> { self.api_with_retry(|api| api.get_saved_show_manual(Some(50), Some(offset))) + .ok_or(()) } - pub fn save_shows(&self, ids: &[&str]) -> bool { + /// Add the shows with the given `ids` to the user's library. + pub fn save_shows(&self, ids: &[&str]) -> Result<(), ()> { self.api_with_retry(|api| { api.save_shows( ids.iter() @@ -507,10 +550,11 @@ impl WebApi { .collect::>(), ) }) - .is_some() + .ok_or(()) } - pub fn unsave_shows(&self, ids: &[&str]) -> bool { + /// Remove the shows with `ids` from the user's library. + pub fn unsave_shows(&self, ids: &[&str]) -> Result<(), ()> { self.api_with_retry(|api| { api.remove_users_saved_shows( ids.iter() @@ -519,17 +563,21 @@ impl WebApi { Some(Market::FromToken), ) }) - .is_some() + .ok_or(()) } + /// Get the user's followed artists. `last` is an artist id. If it is specified, the artists + /// after the one with this id will be retrieved. pub fn current_user_followed_artists( &self, last: Option<&str>, - ) -> Option> { + ) -> Result, ()> { self.api_with_retry(|api| api.current_user_followed_artists(last, Some(50))) + .ok_or(()) } - pub fn user_follow_artists(&self, ids: Vec<&str>) -> Option<()> { + /// Add the logged in user to the followers of the artists with the given `ids`. + pub fn user_follow_artists(&self, ids: Vec<&str>) -> Result<(), ()> { self.api_with_retry(|api| { api.user_follow_artists( ids.iter() @@ -537,9 +585,11 @@ impl WebApi { .collect::>(), ) }) + .ok_or(()) } - pub fn user_unfollow_artists(&self, ids: Vec<&str>) -> Option<()> { + /// Remove the logged in user to the followers of the artists with the given `ids`. + pub fn user_unfollow_artists(&self, ids: Vec<&str>) -> Result<(), ()> { self.api_with_retry(|api| { api.user_unfollow_artists( ids.iter() @@ -547,15 +597,19 @@ impl WebApi { .collect::>(), ) }) + .ok_or(()) } - pub fn current_user_saved_albums(&self, offset: u32) -> Option> { + /// Get the user's saved albums, starting at the given `offset`. The result is paginated. + pub fn current_user_saved_albums(&self, offset: u32) -> Result, ()> { self.api_with_retry(|api| { api.current_user_saved_albums_manual(Some(Market::FromToken), Some(50), Some(offset)) }) + .ok_or(()) } - pub fn current_user_saved_albums_add(&self, ids: Vec<&str>) -> Option<()> { + /// Add the albums with the given `ids` to the user's saved albums. + pub fn current_user_saved_albums_add(&self, ids: Vec<&str>) -> Result<(), ()> { self.api_with_retry(|api| { api.current_user_saved_albums_add( ids.iter() @@ -563,9 +617,11 @@ impl WebApi { .collect::>(), ) }) + .ok_or(()) } - pub fn current_user_saved_albums_delete(&self, ids: Vec<&str>) -> Option<()> { + /// Remove the albums with the given `ids` from the user's saved albums. + pub fn current_user_saved_albums_delete(&self, ids: Vec<&str>) -> Result<(), ()> { self.api_with_retry(|api| { api.current_user_saved_albums_delete( ids.iter() @@ -573,15 +629,19 @@ impl WebApi { .collect::>(), ) }) + .ok_or(()) } - pub fn current_user_saved_tracks(&self, offset: u32) -> Option> { + /// Get the user's saved tracks, starting at the given `offset`. The result is paginated. + pub fn current_user_saved_tracks(&self, offset: u32) -> Result, ()> { self.api_with_retry(|api| { api.current_user_saved_tracks_manual(Some(Market::FromToken), Some(50), Some(offset)) }) + .ok_or(()) } - pub fn current_user_saved_tracks_add(&self, ids: Vec<&str>) -> Option<()> { + /// Add the tracks with the given `ids` to the user's saved tracks. + pub fn current_user_saved_tracks_add(&self, ids: Vec<&str>) -> Result<(), ()> { self.api_with_retry(|api| { api.current_user_saved_tracks_add( ids.iter() @@ -589,9 +649,11 @@ impl WebApi { .collect::>(), ) }) + .ok_or(()) } - pub fn current_user_saved_tracks_delete(&self, ids: Vec<&str>) -> Option<()> { + /// Remove the tracks with the given `ids` from the user's saved tracks. + pub fn current_user_saved_tracks_delete(&self, ids: Vec<&str>) -> Result<(), ()> { self.api_with_retry(|api| { api.current_user_saved_tracks_delete( ids.iter() @@ -599,24 +661,32 @@ impl WebApi { .collect::>(), ) }) + .ok_or(()) } - pub fn user_playlist_follow_playlist(&self, id: &str) -> Option<()> { + /// Add the logged in user to the followers of the playlist with the given `id`. + pub fn user_playlist_follow_playlist(&self, id: &str) -> Result<(), ()> { self.api_with_retry(|api| api.playlist_follow(PlaylistId::from_id(id).unwrap(), None)) + .ok_or(()) } - pub fn artist_top_tracks(&self, id: &str) -> Option> { + /// Get the top tracks of the artist with the given `id`. + pub fn artist_top_tracks(&self, id: &str) -> Result, ()> { self.api_with_retry(|api| { api.artist_top_tracks(ArtistId::from_id(id).unwrap(), Some(Market::FromToken)) }) .map(|ft| ft.iter().map(|t| t.into()).collect()) + .ok_or(()) } - pub fn artist_related_artists(&self, id: &str) -> Option> { + /// Get artists related to the artist with the given `id`. + pub fn artist_related_artists(&self, id: &str) -> Result, ()> { self.api_with_retry(|api| api.artist_related_artists(ArtistId::from_id(id).unwrap())) .map(|fa| fa.iter().map(|a| a.into()).collect()) + .ok_or(()) } + /// Get the available categories. pub fn categories(&self) -> ApiResult { const MAX_LIMIT: u32 = 50; let spotify = self.clone(); @@ -641,6 +711,7 @@ impl WebApi { ApiResult::new(MAX_LIMIT, Arc::new(fetch_page)) } + /// Get the playlists in the category given by `category_id`. pub fn category_playlists(&self, category_id: &str) -> ApiResult { const MAX_LIMIT: u32 = 50; let spotify = self.clone(); @@ -666,7 +737,8 @@ impl WebApi { ApiResult::new(MAX_LIMIT, Arc::new(fetch_page)) } - pub fn current_user(&self) -> Option { - self.api_with_retry(|api| api.current_user()) + /// Get details about the logged in user. + pub fn current_user(&self) -> Result { + self.api_with_retry(|api| api.current_user()).ok_or(()) } } diff --git a/src/theme.rs b/src/theme.rs index 2c345f7c2..e5ff34831 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -6,6 +6,17 @@ use log::warn; use crate::config::ConfigTheme; +/// Get the given color from the given [ConfigTheme]. The first argument is the [ConfigTheme] to get +/// the color out of. The second argument is the name of the color to get and is an identifier. The +/// third argument is a [Color] that is used as the default when no color can be parsed from the +/// provided [ConfigTheme]. +/// +/// # Examples +/// +/// ```rust +/// load_color!(config_theme, background, TerminalDefault) +/// load_color!(config_theme, primary, TerminalDefault) +/// ``` macro_rules! load_color { ( $theme: expr, $member: ident, $default: expr ) => { $theme @@ -22,6 +33,7 @@ macro_rules! load_color { }; } +/// Create a [cursive::theme::Theme] from `theme_cfg`. pub fn load(theme_cfg: &Option) -> Theme { let mut palette = Palette::default(); let borders = BorderStyle::Simple; diff --git a/src/traits.rs b/src/traits.rs index ba4c1cfb5..0578023ac 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -35,6 +35,7 @@ pub trait ListItem: Sync + Send + 'static { } fn share_url(&self) -> Option; + /// Get the album that contains this [ListItem]. fn album(&self, _queue: &Queue) -> Option { None } diff --git a/src/ui/artist.rs b/src/ui/artist.rs index 8b1d53758..e0628176b 100644 --- a/src/ui/artist.rs +++ b/src/ui/artist.rs @@ -38,7 +38,7 @@ impl ArtistView { let library = library.clone(); thread::spawn(move || { if let Some(id) = id { - if let Some(tracks) = spotify.api.artist_top_tracks(&id) { + if let Ok(tracks) = spotify.api.artist_top_tracks(&id) { top_tracks.write().unwrap().extend(tracks); library.trigger_redraw(); } @@ -53,7 +53,7 @@ impl ArtistView { let library = library.clone(); thread::spawn(move || { if let Some(id) = id { - if let Some(artists) = spotify.api.artist_related_artists(&id) { + if let Ok(artists) = spotify.api.artist_related_artists(&id) { related.write().unwrap().extend(artists); library.trigger_redraw(); } diff --git a/src/ui/listview.rs b/src/ui/listview.rs index 485b659d7..5193ef86f 100644 --- a/src/ui/listview.rs +++ b/src/ui/listview.rs @@ -720,27 +720,33 @@ impl ViewExt for ListView { UriType::Track => spotify .api .track(&url.id) - .map(|track| Track::from(&track).as_listitem()), + .map(|track| Track::from(&track).as_listitem()) + .ok(), UriType::Album => spotify .api .album(&url.id) - .map(|album| Album::from(&album).as_listitem()), + .map(|album| Album::from(&album).as_listitem()) + .ok(), UriType::Playlist => spotify .api .playlist(&url.id) - .map(|playlist| Playlist::from(&playlist).as_listitem()), + .map(|playlist| Playlist::from(&playlist).as_listitem()) + .ok(), UriType::Artist => spotify .api .artist(&url.id) - .map(|artist| Artist::from(&artist).as_listitem()), + .map(|artist| Artist::from(&artist).as_listitem()) + .ok(), UriType::Episode => spotify .api .episode(&url.id) - .map(|episode| Episode::from(&episode).as_listitem()), + .map(|episode| Episode::from(&episode).as_listitem()) + .ok(), UriType::Show => spotify .api - .get_show(&url.id) - .map(|show| Show::from(&show).as_listitem()), + .show(&url.id) + .map(|show| Show::from(&show).as_listitem()) + .ok(), }; let queue = self.queue.clone(); diff --git a/src/ui/search_results.rs b/src/ui/search_results.rs index 60f2c8070..0b48ae3d9 100644 --- a/src/ui/search_results.rs +++ b/src/ui/search_results.rs @@ -109,7 +109,7 @@ impl SearchResultsView { _offset: usize, _append: bool, ) -> u32 { - if let Some(results) = spotify.api.track(query) { + if let Ok(results) = spotify.api.track(query) { let t = vec![(&results).into()]; let mut r = tracks.write().unwrap(); *r = t; @@ -125,7 +125,7 @@ impl SearchResultsView { offset: usize, append: bool, ) -> u32 { - if let Some(SearchResult::Tracks(results)) = + if let Ok(SearchResult::Tracks(results)) = spotify .api .search(SearchType::Track, query, 50, offset as u32) @@ -150,7 +150,7 @@ impl SearchResultsView { _offset: usize, _append: bool, ) -> u32 { - if let Some(results) = spotify.api.album(query) { + if let Ok(results) = spotify.api.album(query) { let a = vec![(&results).into()]; let mut r = albums.write().unwrap(); *r = a; @@ -166,7 +166,7 @@ impl SearchResultsView { offset: usize, append: bool, ) -> u32 { - if let Some(SearchResult::Albums(results)) = + if let Ok(SearchResult::Albums(results)) = spotify .api .search(SearchType::Album, query, 50, offset as u32) @@ -191,7 +191,7 @@ impl SearchResultsView { _offset: usize, _append: bool, ) -> u32 { - if let Some(results) = spotify.api.artist(query) { + if let Ok(results) = spotify.api.artist(query) { let a = vec![(&results).into()]; let mut r = artists.write().unwrap(); *r = a; @@ -207,7 +207,7 @@ impl SearchResultsView { offset: usize, append: bool, ) -> u32 { - if let Some(SearchResult::Artists(results)) = + if let Ok(SearchResult::Artists(results)) = spotify .api .search(SearchType::Artist, query, 50, offset as u32) @@ -232,7 +232,7 @@ impl SearchResultsView { _offset: usize, _append: bool, ) -> u32 { - if let Some(result) = spotify.api.playlist(query).as_ref() { + if let Ok(result) = spotify.api.playlist(query).as_ref() { let pls = vec![result.into()]; let mut r = playlists.write().unwrap(); *r = pls; @@ -248,7 +248,7 @@ impl SearchResultsView { offset: usize, append: bool, ) -> u32 { - if let Some(SearchResult::Playlists(results)) = + if let Ok(SearchResult::Playlists(results)) = spotify .api .search(SearchType::Playlist, query, 50, offset as u32) @@ -273,7 +273,7 @@ impl SearchResultsView { _offset: usize, _append: bool, ) -> u32 { - if let Some(result) = spotify.api.get_show(query).as_ref() { + if let Ok(result) = spotify.api.show(query).as_ref() { let pls = vec![result.into()]; let mut r = shows.write().unwrap(); *r = pls; @@ -289,7 +289,7 @@ impl SearchResultsView { offset: usize, append: bool, ) -> u32 { - if let Some(SearchResult::Shows(results)) = + if let Ok(SearchResult::Shows(results)) = spotify .api .search(SearchType::Show, query, 50, offset as u32) @@ -314,7 +314,7 @@ impl SearchResultsView { _offset: usize, _append: bool, ) -> u32 { - if let Some(result) = spotify.api.episode(query).as_ref() { + if let Ok(result) = spotify.api.episode(query).as_ref() { let e = vec![result.into()]; let mut r = episodes.write().unwrap(); *r = e; @@ -330,7 +330,7 @@ impl SearchResultsView { offset: usize, append: bool, ) -> u32 { - if let Some(SearchResult::Episodes(results)) = + if let Ok(SearchResult::Episodes(results)) = spotify .api .search(SearchType::Episode, query, 50, offset as u32) diff --git a/src/ui/tabbedview.rs b/src/ui/tabbedview.rs index 936745378..26884fdb3 100644 --- a/src/ui/tabbedview.rs +++ b/src/ui/tabbedview.rs @@ -32,7 +32,7 @@ impl TabbedView { Default::default() } - /// Add `view` as a new tab to the end of this [TabsView]. + /// Add `view` as a new tab to the end of this [TabbedView]. pub fn add_tab(&mut self, title: impl Into, view: impl IntoBoxedViewExt) { let tab = BoxedViewExt::new(view.into_boxed_view_ext()).with_name(title); self.tabs.push(tab); @@ -54,7 +54,7 @@ impl TabbedView { self.tabs.len() } - /// Check whether there are tabs in this [TabsView]. + /// Check whether there are tabs in this [TabbedView]. pub fn is_empty(&self) -> bool { self.len() == 0 }