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
This commit is contained in:
Thomas Frans
2024-02-01 19:42:53 +01:00
committed by GitHub
parent 8805464b1d
commit c5d666f35c
18 changed files with 312 additions and 151 deletions

View File

@@ -238,8 +238,8 @@ impl CommandManager {
} }
Command::NewPlaylist(name) => { Command::NewPlaylist(name) => {
match self.spotify.api.create_playlist(name, None, None) { match self.spotify.api.create_playlist(name, None, None) {
Some(_) => self.library.update_library(), Ok(_) => self.library.update_library(),
None => error!("could not create playlist {}", name), Err(_) => error!("could not create playlist {}", name),
} }
Ok(None) Ok(None)
} }

View File

@@ -233,14 +233,17 @@ impl Config {
} }
} }
/// Get the user configuration values.
pub fn values(&self) -> RwLockReadGuard<ConfigValues> { pub fn values(&self) -> RwLockReadGuard<ConfigValues> {
self.values.read().expect("can't readlock config values") self.values.read().expect("can't readlock config values")
} }
/// Get the runtime user state values.
pub fn state(&self) -> RwLockReadGuard<UserState> { pub fn state(&self) -> RwLockReadGuard<UserState> {
self.state.read().expect("can't readlock user state") 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<F>(&self, cb: F) pub fn with_state_mut<F>(&self, cb: F)
where where
F: Fn(RwLockWriteGuard<UserState>), F: Fn(RwLockWriteGuard<UserState>),
@@ -249,9 +252,15 @@ impl Config {
cb(state_guard); cb(state_guard);
} }
pub fn save_state(&self) { /// Update the version number of the runtime user state. This should be done before saving it to
// update cache version number /// disk.
fn update_state_cache_version(&self) {
self.with_state_mut(|mut state| state.cache_version = CACHE_VERSION); 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"); let path = config_path("userstate.cbor");
debug!("saving user state to {}", path.display()); 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 { pub fn build_theme(&self) -> Theme {
let theme = &self.values().theme; crate::theme::load(&self.values().theme)
crate::theme::load(theme)
} }
/// Attempt to reload the configuration from the configuration file. /// Attempt to reload the configuration from the configuration file.

View File

@@ -4,6 +4,7 @@ use cursive::{CbSink, Cursive};
use crate::queue::QueueEvent; use crate::queue::QueueEvent;
use crate::spotify::PlayerEvent; 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 { pub enum Event {
Player(PlayerEvent), Player(PlayerEvent),
Queue(QueueEvent), Queue(QueueEvent),
@@ -11,11 +12,10 @@ pub enum Event {
IpcInput(String), IpcInput(String),
} }
pub type EventSender = Sender<Event>; /// Manager that can be used to send and receive messages across threads.
#[derive(Clone)] #[derive(Clone)]
pub struct EventManager { pub struct EventManager {
tx: EventSender, tx: Sender<Event>,
rx: Receiver<Event>, rx: Receiver<Event>,
cursive_sink: CbSink, 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<Event> { pub fn msg_iter(&self) -> TryIter<Event> {
self.rx.try_iter() self.rx.try_iter()
} }
/// Send a new event to be handled.
pub fn send(&self, event: Event) { pub fn send(&self, event: Event) {
self.tx.send(event).expect("could not send event"); self.tx.send(event).expect("could not send event");
self.trigger(); self.trigger();
} }
/// Send a no-op to the Cursive event loop to trigger immediate processing of events.
pub fn trigger(&self) { pub fn trigger(&self) {
// send a no-op to trigger event loop processing
self.cursive_sink self.cursive_sink
.send(Box::new(Cursive::noop)) .send(Box::new(Cursive::noop))
.expect("could not send no-op event to cursive"); .expect("could not send no-op event to cursive");

View File

@@ -21,11 +21,20 @@ use crate::model::show::Show;
use crate::model::track::Track; use crate::model::track::Track;
use crate::spotify::Spotify; use crate::spotify::Spotify;
/// Cached tracks database filename.
const CACHE_TRACKS: &str = "tracks.db"; const CACHE_TRACKS: &str = "tracks.db";
/// Cached albums database filename.
const CACHE_ALBUMS: &str = "albums.db"; const CACHE_ALBUMS: &str = "albums.db";
/// Cached artists database filename.
const CACHE_ARTISTS: &str = "artists.db"; const CACHE_ARTISTS: &str = "artists.db";
/// Cached playlists database filename.
const CACHE_PLAYLISTS: &str = "playlists.db"; 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)] #[derive(Clone)]
pub struct Library { pub struct Library {
pub tracks: Arc<RwLock<Vec<Track>>>, pub tracks: Arc<RwLock<Vec<Track>>>,
@@ -43,7 +52,7 @@ pub struct Library {
impl Library { impl Library {
pub fn new(ev: EventManager, spotify: Spotify, cfg: Arc<Config>) -> Self { pub fn new(ev: EventManager, spotify: Spotify, cfg: Arc<Config>) -> 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 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()); 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); .position(|i| i.id == id);
if let Some(position) = position { if let Some(position) = position {
if self.spotify.api.delete_playlist(id) { if self.spotify.api.delete_playlist(id).is_ok() {
self.playlists self.playlists
.write() .write()
.expect("can't writelock playlists") .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 /// 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]) { pub fn overwrite_playlist(&self, id: &str, tracks: &[Playable]) {
debug!("saving {} tracks to list {}", tracks.len(), id); debug!("saving {} tracks to list {}", tracks.len(), id);
self.spotify.api.overwrite_playlist(id, tracks); self.spotify.api.overwrite_playlist(id, tracks);
@@ -179,12 +188,12 @@ impl Library {
pub fn save_playlist(&self, name: &str, tracks: &[Playable]) { pub fn save_playlist(&self, name: &str, tracks: &[Playable]) {
debug!("saving {} tracks to new list {}", tracks.len(), name); debug!("saving {} tracks to new list {}", tracks.len(), name);
match self.spotify.api.create_playlist(name, None, None) { match self.spotify.api.create_playlist(name, None, None) {
Some(id) => self.overwrite_playlist(&id, tracks), Ok(id) => self.overwrite_playlist(&id, tracks),
None => error!("could not create new playlist.."), 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) { pub fn update_library(&self) {
*self.is_done.write().unwrap() = false; *self.is_done.write().unwrap() = false;
@@ -278,7 +287,7 @@ impl Library {
debug!("loading shows"); debug!("loading shows");
let mut saved_shows: Vec<Show> = Vec::new(); let mut saved_shows: Vec<Show> = 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 { while let Some(shows) = shows_result {
saved_shows.extend(shows.items.iter().map(|show| (&show.show).into())); saved_shows.extend(shows.items.iter().map(|show| (&show.show).into()));
@@ -290,6 +299,7 @@ impl Library {
self.spotify self.spotify
.api .api
.get_saved_shows(shows.offset + shows.items.len() as u32) .get_saved_shows(shows.offset + shows.items.len() as u32)
.ok()
} }
None => None, None => None,
} }
@@ -364,7 +374,7 @@ impl Library {
let page = self.spotify.api.current_user_followed_artists(last); let page = self.spotify.api.current_user_followed_artists(last);
debug!("artists page: {}", i); debug!("artists page: {}", i);
i += 1; i += 1;
if page.is_none() { if page.is_err() {
error!("Failed to fetch artists."); error!("Failed to fetch artists.");
return; return;
} }
@@ -422,7 +432,7 @@ impl Library {
i += 1; i += 1;
if page.is_none() { if page.is_err() {
error!("Failed to fetch albums."); error!("Failed to fetch albums.");
return; return;
} }
@@ -465,7 +475,7 @@ impl Library {
debug!("tracks page: {}", i); debug!("tracks page: {}", i);
i += 1; i += 1;
if page.is_none() { if page.is_err() {
error!("Failed to fetch tracks."); error!("Failed to fetch tracks.");
return; return;
} }
@@ -604,7 +614,7 @@ impl Library {
.api .api
.current_user_saved_tracks_add(tracks.iter().filter_map(|t| t.id.as_deref()).collect()); .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; return;
} }
@@ -645,7 +655,7 @@ impl Library {
.current_user_saved_tracks_delete( .current_user_saved_tracks_delete(
tracks.iter().filter_map(|t| t.id.as_deref()).collect(), tracks.iter().filter_map(|t| t.id.as_deref()).collect(),
) )
.is_none() .is_err()
{ {
return; return;
} }
@@ -692,7 +702,7 @@ impl Library {
.spotify .spotify
.api .api
.current_user_saved_albums_add(vec![album_id.as_str()]) .current_user_saved_albums_add(vec![album_id.as_str()])
.is_none() .is_err()
{ {
return; return;
} }
@@ -725,7 +735,7 @@ impl Library {
.spotify .spotify
.api .api
.current_user_saved_albums_delete(vec![album_id.as_str()]) .current_user_saved_albums_delete(vec![album_id.as_str()])
.is_none() .is_err()
{ {
return; return;
} }
@@ -763,7 +773,7 @@ impl Library {
.spotify .spotify
.api .api
.user_follow_artists(vec![artist_id.as_str()]) .user_follow_artists(vec![artist_id.as_str()])
.is_none() .is_err()
{ {
return; return;
} }
@@ -799,7 +809,7 @@ impl Library {
.spotify .spotify
.api .api
.user_unfollow_artists(vec![artist_id.as_str()]) .user_unfollow_artists(vec![artist_id.as_str()])
.is_none() .is_err()
{ {
return; return;
} }
@@ -846,7 +856,7 @@ impl Library {
let follow_playlist_result = self.spotify.api.user_playlist_follow_playlist(&playlist.id); 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; return;
} }
@@ -881,7 +891,7 @@ impl Library {
return; 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(); let mut store = self.shows.write().unwrap();
if !store.iter().any(|s| s.id == show.id) { if !store.iter().any(|s| s.id == show.id) {
@@ -897,7 +907,7 @@ impl Library {
return; 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(); let mut store = self.shows.write().unwrap();
*store = store.iter().filter(|s| s.id != show.id).cloned().collect(); *store = store.iter().filter(|s| s.id != show.id).cloned().collect();
} }

View File

@@ -38,7 +38,7 @@ impl Album {
if let Some(ref album_id) = self.id { if let Some(ref album_id) = self.id {
let mut collected_tracks = Vec::new(); 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()); let mut tracks_result = Some(full_album.tracks.clone());
while let Some(ref tracks) = tracks_result { while let Some(ref tracks) = tracks_result {
for t in &tracks.items { for t in &tracks.items {
@@ -51,11 +51,14 @@ impl Album {
tracks_result = match tracks.next { tracks_result = match tracks.next {
Some(_) => { Some(_) => {
debug!("requesting tracks again.."); debug!("requesting tracks again..");
spotify.api.album_tracks( spotify
album_id, .api
50, .album_tracks(
tracks.offset + tracks.items.len() as u32, album_id,
) 50,
tracks.offset + tracks.items.len() as u32,
)
.ok()
} }
None => None, None => None,
} }
@@ -273,6 +276,7 @@ impl ListItem for Album {
None, None,
Some(track_ids), Some(track_ids),
) )
.ok()
.map(|r| r.tracks) .map(|r| r.tracks)
.map(|tracks| tracks.iter().map(Track::from).collect()); .map(|tracks| tracks.iter().map(Track::from).collect());
recommendations.map(|tracks| { recommendations.map(|tracks| {

View File

@@ -35,7 +35,7 @@ impl Artist {
fn load_top_tracks(&mut self, spotify: Spotify) { fn load_top_tracks(&mut self, spotify: Spotify) {
if let Some(artist_id) = &self.id { if let Some(artist_id) = &self.id {
if self.tracks.is_none() { 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<Vec<Track>> = spotify let recommendations: Option<Vec<Track>> = spotify
.api .api
.recommendations(Some(vec![&id]), None, None) .recommendations(Some(vec![&id]), None, None)
.ok()
.map(|r| r.tracks) .map(|r| r.tracks)
.map(|tracks| tracks.iter().map(Track::from).collect()); .map(|tracks| tracks.iter().map(Track::from).collect());

View File

@@ -67,6 +67,7 @@ impl Playlist {
match spotify match spotify
.api .api
.delete_tracks(&self.id, &self.snapshot_id, &[playable]) .delete_tracks(&self.id, &self.snapshot_id, &[playable])
.is_ok()
{ {
false => false, false => false,
true => { true => {
@@ -83,7 +84,11 @@ impl Playlist {
pub fn append_tracks(&mut self, new_tracks: &[Playable], spotify: &Spotify, library: &Library) { pub fn append_tracks(&mut self, new_tracks: &[Playable], spotify: &Spotify, library: &Library) {
let mut has_modified = false; 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 { if let Some(tracks) = &mut self.tracks {
tracks.append(&mut new_tracks.to_vec()); tracks.append(&mut new_tracks.to_vec());
has_modified = true; has_modified = true;
@@ -304,6 +309,7 @@ impl ListItem for Playlist {
None, None,
Some(track_ids.iter().map(|t| t.as_ref()).collect()), Some(track_ids.iter().map(|t| t.as_ref()).collect()),
) )
.ok()
.map(|r| r.tracks) .map(|r| r.tracks)
.map(|tracks| tracks.iter().map(Track::from).collect()); .map(|tracks| tracks.iter().map(Track::from).collect());

View File

@@ -278,6 +278,7 @@ impl ListItem for Track {
spotify spotify
.api .api
.recommendations(None, None, Some(vec![id])) .recommendations(None, None, Some(vec![id]))
.ok()
.map(|r| r.tracks) .map(|r| r.tracks)
.map(|tracks| tracks.iter().map(Self::from).collect()) .map(|tracks| tracks.iter().map(Self::from).collect())
} else { } else {
@@ -309,7 +310,7 @@ impl ListItem for Track {
let spotify = queue.get_spotify(); let spotify = queue.get_spotify();
match self.album_id { 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, None => None,
} }
} }

View File

@@ -137,6 +137,7 @@ impl MprisPlayer {
.track(&track.id.unwrap_or_default()) .track(&track.id.unwrap_or_default())
.as_ref() .as_ref()
.map(|t| Playable::Track(t.into())) .map(|t| Playable::Track(t.into()))
.ok()
} }
} }
Playable::Episode(episode) => Some(Playable::Episode(episode)), Playable::Episode(episode) => Some(Playable::Episode(episode)),
@@ -386,7 +387,7 @@ impl MprisPlayer {
let uri_type = spotify_url.map(|s| s.uri_type); let uri_type = spotify_url.map(|s| s.uri_type);
match uri_type { match uri_type {
Some(UriType::Album) => { 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 { if let Some(t) = &Album::from(&a).tracks {
let should_shuffle = self.queue.get_shuffle(); let should_shuffle = self.queue.get_shuffle();
self.queue.clear(); self.queue.clear();
@@ -400,14 +401,14 @@ impl MprisPlayer {
} }
} }
Some(UriType::Track) => { 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.clear();
self.queue.append(Playable::Track(Track::from(&t))); self.queue.append(Playable::Track(Track::from(&t)));
self.queue.play(0, false, false) self.queue.play(0, false, false)
} }
} }
Some(UriType::Playlist) => { 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); let mut playlist = Playlist::from(&p);
playlist.load_tracks(&self.spotify); playlist.load_tracks(&self.spotify);
if let Some(tracks) = &playlist.tracks { if let Some(tracks) = &playlist.tracks {
@@ -419,7 +420,7 @@ impl MprisPlayer {
} }
} }
Some(UriType::Show) => { 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 mut show: Show = (&s).into();
let spotify = self.spotify.clone(); let spotify = self.spotify.clone();
show.load_all_episodes(spotify); show.load_all_episodes(spotify);
@@ -438,14 +439,14 @@ impl MprisPlayer {
} }
} }
Some(UriType::Episode) => { 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.clear();
self.queue.append(Playable::Episode(Episode::from(&e))); self.queue.append(Playable::Episode(Episode::from(&e)));
self.queue.play(0, false, false) self.queue.play(0, false, false)
} }
} }
Some(UriType::Artist) => { 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(); let should_shuffle = self.queue.get_shuffle();
self.queue.clear(); self.queue.clear();
let index = self.queue.append_next( 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. /// 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 /// <https://specifications.freedesktop.org/mpris-spec/2.2/#Bus-Name-Policy>
pub fn instance_bus_name() -> String { pub fn instance_bus_name() -> String {
format!( format!(
"org.mpris.MediaPlayer2.ncspot.instance{}", "org.mpris.MediaPlayer2.ncspot.instance{}",

View File

@@ -33,9 +33,8 @@ pub enum QueueEvent {
PreloadTrackRequest, PreloadTrackRequest,
} }
/// The queue determines the playback order of /// The queue determines the playback order of [Playable] items, and is also used to control
/// [Playable](crate::model::playable::Playable) items, and is also used to /// playback itself.
/// control playback itself.
pub struct Queue { pub struct Queue {
/// The internal data, which doesn't change with shuffle or repeat. This is /// The internal data, which doesn't change with shuffle or repeat. This is
/// the raw data only. /// the raw data only.

View File

@@ -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::authentication::Credentials;
use librespot_core::cache::Cache; use librespot_core::cache::Cache;
use librespot_core::config::SessionConfig; use librespot_core::config::SessionConfig;
use librespot_core::session::Session; use librespot_core::session::Session;
use librespot_core::session::SessionError; use librespot_core::session::SessionError;
use librespot_playback::audio_backend;
use librespot_playback::audio_backend::SinkBuilder; use librespot_playback::audio_backend::SinkBuilder;
use librespot_playback::config::Bitrate;
use librespot_playback::config::PlayerConfig; use librespot_playback::config::PlayerConfig;
use librespot_playback::mixer::softmixer::SoftMixer; use librespot_playback::mixer::softmixer::SoftMixer;
use librespot_playback::mixer::MixerConfig; 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 librespot_playback::player::Player;
use log::{debug, error, info};
use futures::channel::oneshot;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use url::Url; 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::application::ASYNC_RUNTIME;
use crate::config; use crate::config;
use crate::events::{Event, EventManager}; use crate::events::{Event, EventManager};
@@ -30,8 +27,11 @@ use crate::model::playable::Playable;
use crate::spotify_api::WebApi; use crate::spotify_api::WebApi;
use crate::spotify_worker::{Worker, WorkerCommand}; 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; 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)] #[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub enum PlayerEvent { pub enum PlayerEvent {
Playing(SystemTime), Playing(SystemTime),
@@ -40,18 +40,23 @@ pub enum PlayerEvent {
FinishedTrack, FinishedTrack,
} }
// TODO: Rename or document this as it isn't immediately clear what it represents/does from the /// Wrapper around a worker thread that exposes methods to safely control it.
// name.
#[derive(Clone)] #[derive(Clone)]
pub struct Spotify { pub struct Spotify {
events: EventManager, events: EventManager,
/// The credentials for the currently logged in user, used to authenticate to the Spotify API.
credentials: Credentials, credentials: Credentials,
cfg: Arc<config::Config>, cfg: Arc<config::Config>,
/// Playback status of the [Player] owned by the worker thread.
status: Arc<RwLock<PlayerEvent>>, status: Arc<RwLock<PlayerEvent>>,
pub api: WebApi, pub api: WebApi,
/// The amount of the current [Playable] that had elapsed when last paused.
elapsed: Arc<RwLock<Option<Duration>>>, elapsed: Arc<RwLock<Option<Duration>>>,
/// The amount of the current [Playable] that has been played in total.
since: Arc<RwLock<Option<SystemTime>>>, since: Arc<RwLock<Option<SystemTime>>>,
/// Channel to send commands to the worker thread.
channel: Arc<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>, channel: Arc<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>,
/// The username of the logged in user.
user: Option<String>, user: Option<String>,
} }
@@ -83,6 +88,8 @@ impl Spotify {
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<oneshot::Sender<String>>) { pub fn start_worker(&self, user_tx: Option<oneshot::Sender<String>>) {
let (tx, rx) = mpsc::unbounded_channel(); let (tx, rx) = mpsc::unbounded_channel();
*self *self
@@ -107,6 +114,7 @@ impl Spotify {
} }
} }
/// Generate the librespot [SessionConfig] used when creating a [Session].
pub fn session_config() -> SessionConfig { pub fn session_config() -> SessionConfig {
let mut session_config = SessionConfig::default(); let mut session_config = SessionConfig::default();
match env::var("http_proxy") { match env::var("http_proxy") {
@@ -119,6 +127,7 @@ impl Spotify {
session_config session_config
} }
/// Test whether `credentials` are valid Spotify credentials.
pub fn test_credentials(credentials: Credentials) -> Result<Session, SessionError> { pub fn test_credentials(credentials: Credentials) -> Result<Session, SessionError> {
let config = Self::session_config(); let config = Self::session_config();
ASYNC_RUNTIME ASYNC_RUNTIME
@@ -128,6 +137,8 @@ impl Spotify {
.map(|r| r.0) .map(|r| r.0)
} }
/// Create a [Session] that respects the user configuration in `cfg` and with the given
/// credentials.
async fn create_session( async fn create_session(
cfg: &config::Config, cfg: &config::Config,
credentials: Credentials, credentials: Credentials,
@@ -153,6 +164,7 @@ impl Spotify {
.map(|r| r.0) .map(|r| r.0)
} }
/// Create and initialize the requested audio backend.
fn init_backend(desired_backend: Option<String>) -> Option<SinkBuilder> { fn init_backend(desired_backend: Option<String>) -> Option<SinkBuilder> {
let backend = if let Some(name) = desired_backend { let backend = if let Some(name) = desired_backend {
audio_backend::BACKENDS audio_backend::BACKENDS
@@ -174,6 +186,7 @@ impl Spotify {
Some(backend.1) Some(backend.1)
} }
/// Create and run the worker thread.
async fn worker( async fn worker(
worker_channel: Arc<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>, worker_channel: Arc<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>,
events: EventManager, events: EventManager,
@@ -236,6 +249,7 @@ impl Spotify {
events.send(Event::SessionDied) events.send(Event::SessionDied)
} }
/// Get the current playback status of the [Player].
pub fn get_current_status(&self) -> PlayerEvent { pub fn get_current_status(&self) -> PlayerEvent {
let status = self let status = self
.status .status
@@ -244,6 +258,7 @@ impl Spotify {
(*status).clone() (*status).clone()
} }
/// Get the total amount of the current [Playable] that has been played.
pub fn get_current_progress(&self) -> Duration { pub fn get_current_progress(&self) -> Duration {
self.get_elapsed().unwrap_or_else(|| Duration::from_secs(0)) self.get_elapsed().unwrap_or_else(|| Duration::from_secs(0))
+ self + self
@@ -284,6 +299,8 @@ impl Spotify {
*since *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) { pub fn load(&self, track: &Playable, start_playing: bool, position_ms: u32) {
info!("loading track: {:?}", track); info!("loading track: {:?}", track);
self.send_worker(WorkerCommand::Load( 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) { pub fn update_status(&self, new_status: PlayerEvent) {
match new_status { match new_status {
PlayerEvent::Paused(position) => { PlayerEvent::Paused(position) => {
@@ -316,16 +336,20 @@ impl Spotify {
*status = new_status; *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) { pub fn update_track(&self) {
self.set_elapsed(None); self.set_elapsed(None);
self.set_since(None); self.set_since(None);
} }
/// Start playback of the [Player].
pub fn play(&self) { pub fn play(&self) {
info!("play()"); info!("play()");
self.send_worker(WorkerCommand::Play); self.send_worker(WorkerCommand::Play);
} }
/// Toggle playback (play/pause) of the [Player].
pub fn toggleplayback(&self) { pub fn toggleplayback(&self) {
match self.get_current_status() { match self.get_current_status() {
PlayerEvent::Playing(_) => self.pause(), PlayerEvent::Playing(_) => self.pause(),
@@ -334,6 +358,7 @@ impl Spotify {
} }
} }
/// Send a [WorkerCommand] to the worker thread.
fn send_worker(&self, cmd: WorkerCommand) { fn send_worker(&self, cmd: WorkerCommand) {
info!("sending command to worker: {:?}", cmd); info!("sending command to worker: {:?}", cmd);
let channel = self.channel.read().expect("can't readlock worker channel"); 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) { pub fn pause(&self) {
info!("pause()"); info!("pause()");
self.send_worker(WorkerCommand::Pause); self.send_worker(WorkerCommand::Pause);
} }
/// Stop playback of the [Player].
pub fn stop(&self) { pub fn stop(&self) {
info!("stop()"); info!("stop()");
self.send_worker(WorkerCommand::Stop); self.send_worker(WorkerCommand::Stop);
} }
/// Seek in the currently played [Playable] played by the [Player].
pub fn seek(&self, position_ms: u32) { pub fn seek(&self, position_ms: u32) {
self.send_worker(WorkerCommand::Seek(position_ms)); self.send_worker(WorkerCommand::Seek(position_ms));
} }
/// Seek relatively to the current playback position of the [Player].
pub fn seek_relative(&self, delta: i32) { pub fn seek_relative(&self, delta: i32) {
let progress = self.get_current_progress(); let progress = self.get_current_progress();
let new = (progress.as_secs() * 1000) as i32 + progress.subsec_millis() as i32 + delta; let new = (progress.as_secs() * 1000) as i32 + progress.subsec_millis() as i32 + delta;
self.seek(std::cmp::max(0, new) as u32); self.seek(std::cmp::max(0, new) as u32);
} }
/// Get the current volume of the [Player].
pub fn volume(&self) -> u16 { pub fn volume(&self) -> u16 {
self.cfg.state().volume self.cfg.state().volume
} }
/// Set the current volume of the [Player].
pub fn set_volume(&self, volume: u16) { pub fn set_volume(&self, volume: u16) {
info!("setting volume to {}", volume); info!("setting volume to {}", volume);
self.cfg.with_state_mut(|mut s| s.volume = volume); self.cfg.with_state_mut(|mut s| s.volume = volume);
self.send_worker(WorkerCommand::SetVolume(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) { pub fn preload(&self, track: &Playable) {
self.send_worker(WorkerCommand::Preload(track.clone())); self.send_worker(WorkerCommand::Preload(track.clone()));
} }
/// Shut down the worker thread.
pub fn shutdown(&self) { pub fn shutdown(&self) {
self.send_worker(WorkerCommand::Shutdown); self.send_worker(WorkerCommand::Shutdown);
} }
} }
/// A type of Spotify URI.
#[derive(Copy, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] #[derive(Copy, Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
pub enum UriType { pub enum UriType {
Album, Album,
@@ -400,6 +435,7 @@ pub enum UriType {
} }
impl UriType { impl UriType {
/// Try to create a [UriType] from the given string.
pub fn from_uri(s: &str) -> Option<Self> { pub fn from_uri(s: &str) -> Option<Self> {
if s.starts_with("spotify:album:") { if s.starts_with("spotify:album:") {
Some(Self::Album) Some(Self::Album)

View File

@@ -1,3 +1,21 @@
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,
PlaylistResult, PrivateUser, Recommendations, SavedAlbum, SavedTrack, SearchResult, SearchType,
Show, ShowId, SimplifiedTrack, TrackId, UserId,
};
use rspotify::{prelude::*, AuthCodeSpotify, ClientError, ClientResult, Config, Token};
use tokio::sync::mpsc;
use crate::model::album::Album; use crate::model::album::Album;
use crate::model::artist::Artist; use crate::model::artist::Artist;
use crate::model::category::Category; use crate::model::category::Category;
@@ -7,29 +25,17 @@ use crate::model::playlist::Playlist;
use crate::model::track::Track; use crate::model::track::Track;
use crate::spotify_worker::WorkerCommand; use crate::spotify_worker::WorkerCommand;
use crate::ui::pagination::{ApiPage, ApiResult}; use crate::ui::pagination::{ApiPage, ApiResult};
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,
};
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;
/// Convenient wrapper around the rspotify web API functionality.
#[derive(Clone)] #[derive(Clone)]
pub struct WebApi { pub struct WebApi {
/// Rspotify web API.
api: AuthCodeSpotify, api: AuthCodeSpotify,
/// The username of the logged in user.
user: Option<String>, user: Option<String>,
/// Sender of the mpsc channel to the [Spotify](crate::spotify::Spotify) worker thread.
worker_channel: Arc<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>, worker_channel: Arc<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>,
/// Time at which the token expires.
token_expiration: Arc<RwLock<DateTime<Utc>>>, token_expiration: Arc<RwLock<DateTime<Utc>>>,
} }
@@ -58,10 +64,13 @@ impl WebApi {
Self::default() Self::default()
} }
/// Set the username for use with the API.
pub fn set_user(&mut self, user: Option<String>) { pub fn set_user(&mut self, user: Option<String>) {
self.user = user; 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( pub(crate) fn set_worker_channel(
&mut self, &mut self,
channel: Arc<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>, channel: Arc<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>,
@@ -115,12 +124,12 @@ impl WebApi {
} }
} }
/// retries once when rate limits are hit /// Execute `api_call` and retry once if a rate limit occurs.
fn api_with_retry<F, R>(&self, cb: F) -> Option<R> fn api_with_retry<F, R>(&self, api_call: F) -> Option<R>
where where
F: Fn(&AuthCodeSpotify) -> ClientResult<R>, F: Fn(&AuthCodeSpotify) -> ClientResult<R>,
{ {
let result = { cb(&self.api) }; let result = { api_call(&self.api) };
match result { match result {
Ok(v) => Some(v), Ok(v) => Some(v),
Err(ClientError::Http(error)) => { Err(ClientError::Http(error)) => {
@@ -133,12 +142,12 @@ impl WebApi {
.and_then(|v| v.parse::<u64>().ok()); .and_then(|v| v.parse::<u64>().ok());
debug!("rate limit hit. waiting {:?} seconds", waiting_duration); debug!("rate limit hit. waiting {:?} seconds", waiting_duration);
thread::sleep(Duration::from_secs(waiting_duration.unwrap_or(0))); thread::sleep(Duration::from_secs(waiting_duration.unwrap_or(0)));
cb(&self.api).ok() api_call(&self.api).ok()
} }
401 => { 401 => {
debug!("token unauthorized. trying refresh.."); debug!("token unauthorized. trying refresh..");
self.update_token(); self.update_token();
cb(&self.api).ok() api_call(&self.api).ok()
} }
_ => { _ => {
error!("unhandled api error: {:?}", response); 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( pub fn append_tracks(
&self, &self,
playlist_id: &str, playlist_id: &str,
tracks: &[Playable], tracks: &[Playable],
position: Option<u32>, position: Option<u32>,
) -> bool { ) -> Result<PlaylistResult, ()> {
self.api_with_retry(|api| { self.api_with_retry(|api| {
let trackids: Vec<PlayableId> = tracks let trackids: Vec<PlayableId> = tracks
.iter() .iter()
@@ -173,7 +183,7 @@ impl WebApi {
position, position,
) )
}) })
.is_some() .ok_or(())
} }
pub fn delete_tracks( pub fn delete_tracks(
@@ -181,7 +191,7 @@ impl WebApi {
playlist_id: &str, playlist_id: &str,
snapshot_id: &str, snapshot_id: &str,
playables: &[Playable], playables: &[Playable],
) -> bool { ) -> Result<PlaylistResult, ()> {
self.api_with_retry(move |api| { self.api_with_retry(move |api| {
let playable_ids: Vec<PlayableId> = playables let playable_ids: Vec<PlayableId> = playables
.iter() .iter()
@@ -205,9 +215,11 @@ impl WebApi {
Some(snapshot_id), 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]) { pub fn overwrite_playlist(&self, id: &str, tracks: &[Playable]) {
// create mutable copy for chunking // create mutable copy for chunking
let mut tracks: Vec<Playable> = tracks.to_vec(); let mut tracks: Vec<Playable> = tracks.to_vec();
@@ -239,7 +251,7 @@ impl WebApi {
}; };
debug!("adding another {} tracks to playlist", tracks.len()); 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()); debug!("{} tracks successfully added", tracks.len());
} else { } else {
error!("error saving tracks to playlists {}", id); 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())) 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( pub fn create_playlist(
&self, &self,
name: &str, name: &str,
public: Option<bool>, public: Option<bool>,
description: Option<&str>, description: Option<&str>,
) -> Option<String> { ) -> Result<String, ()> {
let result = self.api_with_retry(|api| { let result = self.api_with_retry(|api| {
api.user_playlist_create( api.user_playlist_create(
UserId::from_id(self.user.as_ref().unwrap()).unwrap(), UserId::from_id(self.user.as_ref().unwrap()).unwrap(),
@@ -271,46 +286,59 @@ impl WebApi {
description, 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<FullAlbum> { /// Fetch the album with the given `album_id`.
pub fn album(&self, album_id: &str) -> Result<FullAlbum, ()> {
debug!("fetching album {}", album_id); 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))) self.api_with_retry(|api| api.album(aid.clone(), Some(Market::FromToken)))
.ok_or(())
} }
pub fn artist(&self, artist_id: &str) -> Option<FullArtist> { /// Fetch the artist with the given `artist_id`.
let aid = ArtistId::from_id(artist_id).ok()?; pub fn artist(&self, artist_id: &str) -> Result<FullArtist, ()> {
self.api_with_retry(|api| api.artist(aid.clone())) 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<FullPlaylist> { /// Fetch the playlist with the given `playlist_id`.
let pid = PlaylistId::from_id(playlist_id).ok()?; pub fn playlist(&self, playlist_id: &str) -> Result<FullPlaylist, ()> {
let pid = PlaylistId::from_id(playlist_id).map_err(|_| ())?;
self.api_with_retry(|api| api.playlist(pid.clone(), None, Some(Market::FromToken))) self.api_with_retry(|api| api.playlist(pid.clone(), None, Some(Market::FromToken)))
.ok_or(())
} }
pub fn track(&self, track_id: &str) -> Option<FullTrack> { /// Fetch the track with the given `track_id`.
let tid = TrackId::from_id(track_id).ok()?; pub fn track(&self, track_id: &str) -> Result<FullTrack, ()> {
let tid = TrackId::from_id(track_id).map_err(|_| ())?;
self.api_with_retry(|api| api.track(tid.clone(), Some(Market::FromToken))) self.api_with_retry(|api| api.track(tid.clone(), Some(Market::FromToken)))
.ok_or(())
} }
pub fn get_show(&self, show_id: &str) -> Option<FullShow> { /// Fetch the show with the given `show_id`.
let sid = ShowId::from_id(show_id).ok()?; pub fn show(&self, show_id: &str) -> Result<FullShow, ()> {
let sid = ShowId::from_id(show_id).map_err(|_| ())?;
self.api_with_retry(|api| api.get_a_show(sid.clone(), Some(Market::FromToken))) self.api_with_retry(|api| api.get_a_show(sid.clone(), Some(Market::FromToken)))
.ok_or(())
} }
pub fn episode(&self, episode_id: &str) -> Option<FullEpisode> { /// Fetch the episode with the given `episode_id`.
let eid = EpisodeId::from_id(episode_id).ok()?; pub fn episode(&self, episode_id: &str) -> Result<FullEpisode, ()> {
let eid = EpisodeId::from_id(episode_id).map_err(|_| ())?;
self.api_with_retry(|api| api.get_an_episode(eid.clone(), Some(Market::FromToken))) 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( pub fn recommendations(
&self, &self,
seed_artists: Option<Vec<&str>>, seed_artists: Option<Vec<&str>>,
seed_genres: Option<Vec<&str>>, seed_genres: Option<Vec<&str>>,
seed_tracks: Option<Vec<&str>>, seed_tracks: Option<Vec<&str>>,
) -> Option<Recommendations> { ) -> Result<Recommendations, ()> {
self.api_with_retry(|api| { self.api_with_retry(|api| {
let seed_artistids = seed_artists.as_ref().map(|artistids| { let seed_artistids = seed_artists.as_ref().map(|artistids| {
artistids artistids
@@ -333,15 +361,18 @@ impl WebApi {
Some(100), 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( pub fn search(
&self, &self,
searchtype: SearchType, searchtype: SearchType,
query: &str, query: &str,
limit: u32, limit: u32,
offset: u32, offset: u32,
) -> Option<SearchResult> { ) -> Result<SearchResult, ()> {
self.api_with_retry(|api| { self.api_with_retry(|api| {
api.search( api.search(
query, query,
@@ -353,8 +384,10 @@ impl WebApi {
) )
}) })
.take() .take()
.ok_or(())
} }
/// Fetch all the current user's playlists.
pub fn current_user_playlist(&self) -> ApiResult<Playlist> { pub fn current_user_playlist(&self) -> ApiResult<Playlist> {
const MAX_LIMIT: u32 = 50; const MAX_LIMIT: u32 = 50;
let spotify = self.clone(); let spotify = self.clone();
@@ -374,6 +407,7 @@ impl WebApi {
ApiResult::new(MAX_LIMIT, Arc::new(fetch_page)) 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<Playable> { pub fn user_playlist_tracks(&self, playlist_id: &str) -> ApiResult<Playable> {
const MAX_LIMIT: u32 = 100; const MAX_LIMIT: u32 = 100;
let spotify = self.clone(); let spotify = self.clone();
@@ -416,12 +450,14 @@ impl WebApi {
ApiResult::new(MAX_LIMIT, Arc::new(fetch_page)) 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( pub fn album_tracks(
&self, &self,
album_id: &str, album_id: &str,
limit: u32, limit: u32,
offset: u32, offset: u32,
) -> Option<Page<SimplifiedTrack>> { ) -> Result<Page<SimplifiedTrack>, ()> {
debug!("fetching album tracks {}", album_id); debug!("fetching album tracks {}", album_id);
self.api_with_retry(|api| { self.api_with_retry(|api| {
api.album_track_manual( api.album_track_manual(
@@ -431,8 +467,11 @@ impl WebApi {
Some(offset), 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( pub fn artist_albums(
&self, &self,
artist_id: &str, artist_id: &str,
@@ -469,6 +508,7 @@ impl WebApi {
ApiResult::new(MAX_SIZE, Arc::new(fetch_page)) 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<Episode> { pub fn show_episodes(&self, show_id: &str) -> ApiResult<Episode> {
const MAX_SIZE: u32 = 50; const MAX_SIZE: u32 = 50;
let spotify = self.clone(); let spotify = self.clone();
@@ -495,11 +535,14 @@ impl WebApi {
ApiResult::new(MAX_SIZE, Arc::new(fetch_page)) ApiResult::new(MAX_SIZE, Arc::new(fetch_page))
} }
pub fn get_saved_shows(&self, offset: u32) -> Option<Page<Show>> { /// Get the user's saved shows.
pub fn get_saved_shows(&self, offset: u32) -> Result<Page<Show>, ()> {
self.api_with_retry(|api| api.get_saved_show_manual(Some(50), Some(offset))) 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| { self.api_with_retry(|api| {
api.save_shows( api.save_shows(
ids.iter() ids.iter()
@@ -507,10 +550,11 @@ impl WebApi {
.collect::<Vec<ShowId>>(), .collect::<Vec<ShowId>>(),
) )
}) })
.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| { self.api_with_retry(|api| {
api.remove_users_saved_shows( api.remove_users_saved_shows(
ids.iter() ids.iter()
@@ -519,17 +563,21 @@ impl WebApi {
Some(Market::FromToken), 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( pub fn current_user_followed_artists(
&self, &self,
last: Option<&str>, last: Option<&str>,
) -> Option<CursorBasedPage<FullArtist>> { ) -> Result<CursorBasedPage<FullArtist>, ()> {
self.api_with_retry(|api| api.current_user_followed_artists(last, Some(50))) 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| { self.api_with_retry(|api| {
api.user_follow_artists( api.user_follow_artists(
ids.iter() ids.iter()
@@ -537,9 +585,11 @@ impl WebApi {
.collect::<Vec<ArtistId>>(), .collect::<Vec<ArtistId>>(),
) )
}) })
.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| { self.api_with_retry(|api| {
api.user_unfollow_artists( api.user_unfollow_artists(
ids.iter() ids.iter()
@@ -547,15 +597,19 @@ impl WebApi {
.collect::<Vec<ArtistId>>(), .collect::<Vec<ArtistId>>(),
) )
}) })
.ok_or(())
} }
pub fn current_user_saved_albums(&self, offset: u32) -> Option<Page<SavedAlbum>> { /// 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<Page<SavedAlbum>, ()> {
self.api_with_retry(|api| { self.api_with_retry(|api| {
api.current_user_saved_albums_manual(Some(Market::FromToken), Some(50), Some(offset)) 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| { self.api_with_retry(|api| {
api.current_user_saved_albums_add( api.current_user_saved_albums_add(
ids.iter() ids.iter()
@@ -563,9 +617,11 @@ impl WebApi {
.collect::<Vec<AlbumId>>(), .collect::<Vec<AlbumId>>(),
) )
}) })
.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| { self.api_with_retry(|api| {
api.current_user_saved_albums_delete( api.current_user_saved_albums_delete(
ids.iter() ids.iter()
@@ -573,15 +629,19 @@ impl WebApi {
.collect::<Vec<AlbumId>>(), .collect::<Vec<AlbumId>>(),
) )
}) })
.ok_or(())
} }
pub fn current_user_saved_tracks(&self, offset: u32) -> Option<Page<SavedTrack>> { /// 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<Page<SavedTrack>, ()> {
self.api_with_retry(|api| { self.api_with_retry(|api| {
api.current_user_saved_tracks_manual(Some(Market::FromToken), Some(50), Some(offset)) 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| { self.api_with_retry(|api| {
api.current_user_saved_tracks_add( api.current_user_saved_tracks_add(
ids.iter() ids.iter()
@@ -589,9 +649,11 @@ impl WebApi {
.collect::<Vec<TrackId>>(), .collect::<Vec<TrackId>>(),
) )
}) })
.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| { self.api_with_retry(|api| {
api.current_user_saved_tracks_delete( api.current_user_saved_tracks_delete(
ids.iter() ids.iter()
@@ -599,24 +661,32 @@ impl WebApi {
.collect::<Vec<TrackId>>(), .collect::<Vec<TrackId>>(),
) )
}) })
.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)) 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<Vec<Track>> { /// Get the top tracks of the artist with the given `id`.
pub fn artist_top_tracks(&self, id: &str) -> Result<Vec<Track>, ()> {
self.api_with_retry(|api| { self.api_with_retry(|api| {
api.artist_top_tracks(ArtistId::from_id(id).unwrap(), Some(Market::FromToken)) api.artist_top_tracks(ArtistId::from_id(id).unwrap(), Some(Market::FromToken))
}) })
.map(|ft| ft.iter().map(|t| t.into()).collect()) .map(|ft| ft.iter().map(|t| t.into()).collect())
.ok_or(())
} }
pub fn artist_related_artists(&self, id: &str) -> Option<Vec<Artist>> { /// Get artists related to the artist with the given `id`.
pub fn artist_related_artists(&self, id: &str) -> Result<Vec<Artist>, ()> {
self.api_with_retry(|api| api.artist_related_artists(ArtistId::from_id(id).unwrap())) self.api_with_retry(|api| api.artist_related_artists(ArtistId::from_id(id).unwrap()))
.map(|fa| fa.iter().map(|a| a.into()).collect()) .map(|fa| fa.iter().map(|a| a.into()).collect())
.ok_or(())
} }
/// Get the available categories.
pub fn categories(&self) -> ApiResult<Category> { pub fn categories(&self) -> ApiResult<Category> {
const MAX_LIMIT: u32 = 50; const MAX_LIMIT: u32 = 50;
let spotify = self.clone(); let spotify = self.clone();
@@ -641,6 +711,7 @@ impl WebApi {
ApiResult::new(MAX_LIMIT, Arc::new(fetch_page)) 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<Playlist> { pub fn category_playlists(&self, category_id: &str) -> ApiResult<Playlist> {
const MAX_LIMIT: u32 = 50; const MAX_LIMIT: u32 = 50;
let spotify = self.clone(); let spotify = self.clone();
@@ -666,7 +737,8 @@ impl WebApi {
ApiResult::new(MAX_LIMIT, Arc::new(fetch_page)) ApiResult::new(MAX_LIMIT, Arc::new(fetch_page))
} }
pub fn current_user(&self) -> Option<PrivateUser> { /// Get details about the logged in user.
self.api_with_retry(|api| api.current_user()) pub fn current_user(&self) -> Result<PrivateUser, ()> {
self.api_with_retry(|api| api.current_user()).ok_or(())
} }
} }

View File

@@ -6,6 +6,17 @@ use log::warn;
use crate::config::ConfigTheme; 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 { macro_rules! load_color {
( $theme: expr, $member: ident, $default: expr ) => { ( $theme: expr, $member: ident, $default: expr ) => {
$theme $theme
@@ -22,6 +33,7 @@ macro_rules! load_color {
}; };
} }
/// Create a [cursive::theme::Theme] from `theme_cfg`.
pub fn load(theme_cfg: &Option<ConfigTheme>) -> Theme { pub fn load(theme_cfg: &Option<ConfigTheme>) -> Theme {
let mut palette = Palette::default(); let mut palette = Palette::default();
let borders = BorderStyle::Simple; let borders = BorderStyle::Simple;

View File

@@ -35,6 +35,7 @@ pub trait ListItem: Sync + Send + 'static {
} }
fn share_url(&self) -> Option<String>; fn share_url(&self) -> Option<String>;
/// Get the album that contains this [ListItem].
fn album(&self, _queue: &Queue) -> Option<Album> { fn album(&self, _queue: &Queue) -> Option<Album> {
None None
} }

View File

@@ -38,7 +38,7 @@ impl ArtistView {
let library = library.clone(); let library = library.clone();
thread::spawn(move || { thread::spawn(move || {
if let Some(id) = id { 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); top_tracks.write().unwrap().extend(tracks);
library.trigger_redraw(); library.trigger_redraw();
} }
@@ -53,7 +53,7 @@ impl ArtistView {
let library = library.clone(); let library = library.clone();
thread::spawn(move || { thread::spawn(move || {
if let Some(id) = id { 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); related.write().unwrap().extend(artists);
library.trigger_redraw(); library.trigger_redraw();
} }

View File

@@ -720,27 +720,33 @@ impl<I: ListItem + Clone> ViewExt for ListView<I> {
UriType::Track => spotify UriType::Track => spotify
.api .api
.track(&url.id) .track(&url.id)
.map(|track| Track::from(&track).as_listitem()), .map(|track| Track::from(&track).as_listitem())
.ok(),
UriType::Album => spotify UriType::Album => spotify
.api .api
.album(&url.id) .album(&url.id)
.map(|album| Album::from(&album).as_listitem()), .map(|album| Album::from(&album).as_listitem())
.ok(),
UriType::Playlist => spotify UriType::Playlist => spotify
.api .api
.playlist(&url.id) .playlist(&url.id)
.map(|playlist| Playlist::from(&playlist).as_listitem()), .map(|playlist| Playlist::from(&playlist).as_listitem())
.ok(),
UriType::Artist => spotify UriType::Artist => spotify
.api .api
.artist(&url.id) .artist(&url.id)
.map(|artist| Artist::from(&artist).as_listitem()), .map(|artist| Artist::from(&artist).as_listitem())
.ok(),
UriType::Episode => spotify UriType::Episode => spotify
.api .api
.episode(&url.id) .episode(&url.id)
.map(|episode| Episode::from(&episode).as_listitem()), .map(|episode| Episode::from(&episode).as_listitem())
.ok(),
UriType::Show => spotify UriType::Show => spotify
.api .api
.get_show(&url.id) .show(&url.id)
.map(|show| Show::from(&show).as_listitem()), .map(|show| Show::from(&show).as_listitem())
.ok(),
}; };
let queue = self.queue.clone(); let queue = self.queue.clone();

View File

@@ -109,7 +109,7 @@ impl SearchResultsView {
_offset: usize, _offset: usize,
_append: bool, _append: bool,
) -> u32 { ) -> u32 {
if let Some(results) = spotify.api.track(query) { if let Ok(results) = spotify.api.track(query) {
let t = vec![(&results).into()]; let t = vec![(&results).into()];
let mut r = tracks.write().unwrap(); let mut r = tracks.write().unwrap();
*r = t; *r = t;
@@ -125,7 +125,7 @@ impl SearchResultsView {
offset: usize, offset: usize,
append: bool, append: bool,
) -> u32 { ) -> u32 {
if let Some(SearchResult::Tracks(results)) = if let Ok(SearchResult::Tracks(results)) =
spotify spotify
.api .api
.search(SearchType::Track, query, 50, offset as u32) .search(SearchType::Track, query, 50, offset as u32)
@@ -150,7 +150,7 @@ impl SearchResultsView {
_offset: usize, _offset: usize,
_append: bool, _append: bool,
) -> u32 { ) -> u32 {
if let Some(results) = spotify.api.album(query) { if let Ok(results) = spotify.api.album(query) {
let a = vec![(&results).into()]; let a = vec![(&results).into()];
let mut r = albums.write().unwrap(); let mut r = albums.write().unwrap();
*r = a; *r = a;
@@ -166,7 +166,7 @@ impl SearchResultsView {
offset: usize, offset: usize,
append: bool, append: bool,
) -> u32 { ) -> u32 {
if let Some(SearchResult::Albums(results)) = if let Ok(SearchResult::Albums(results)) =
spotify spotify
.api .api
.search(SearchType::Album, query, 50, offset as u32) .search(SearchType::Album, query, 50, offset as u32)
@@ -191,7 +191,7 @@ impl SearchResultsView {
_offset: usize, _offset: usize,
_append: bool, _append: bool,
) -> u32 { ) -> u32 {
if let Some(results) = spotify.api.artist(query) { if let Ok(results) = spotify.api.artist(query) {
let a = vec![(&results).into()]; let a = vec![(&results).into()];
let mut r = artists.write().unwrap(); let mut r = artists.write().unwrap();
*r = a; *r = a;
@@ -207,7 +207,7 @@ impl SearchResultsView {
offset: usize, offset: usize,
append: bool, append: bool,
) -> u32 { ) -> u32 {
if let Some(SearchResult::Artists(results)) = if let Ok(SearchResult::Artists(results)) =
spotify spotify
.api .api
.search(SearchType::Artist, query, 50, offset as u32) .search(SearchType::Artist, query, 50, offset as u32)
@@ -232,7 +232,7 @@ impl SearchResultsView {
_offset: usize, _offset: usize,
_append: bool, _append: bool,
) -> u32 { ) -> 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 pls = vec![result.into()];
let mut r = playlists.write().unwrap(); let mut r = playlists.write().unwrap();
*r = pls; *r = pls;
@@ -248,7 +248,7 @@ impl SearchResultsView {
offset: usize, offset: usize,
append: bool, append: bool,
) -> u32 { ) -> u32 {
if let Some(SearchResult::Playlists(results)) = if let Ok(SearchResult::Playlists(results)) =
spotify spotify
.api .api
.search(SearchType::Playlist, query, 50, offset as u32) .search(SearchType::Playlist, query, 50, offset as u32)
@@ -273,7 +273,7 @@ impl SearchResultsView {
_offset: usize, _offset: usize,
_append: bool, _append: bool,
) -> u32 { ) -> 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 pls = vec![result.into()];
let mut r = shows.write().unwrap(); let mut r = shows.write().unwrap();
*r = pls; *r = pls;
@@ -289,7 +289,7 @@ impl SearchResultsView {
offset: usize, offset: usize,
append: bool, append: bool,
) -> u32 { ) -> u32 {
if let Some(SearchResult::Shows(results)) = if let Ok(SearchResult::Shows(results)) =
spotify spotify
.api .api
.search(SearchType::Show, query, 50, offset as u32) .search(SearchType::Show, query, 50, offset as u32)
@@ -314,7 +314,7 @@ impl SearchResultsView {
_offset: usize, _offset: usize,
_append: bool, _append: bool,
) -> u32 { ) -> 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 e = vec![result.into()];
let mut r = episodes.write().unwrap(); let mut r = episodes.write().unwrap();
*r = e; *r = e;
@@ -330,7 +330,7 @@ impl SearchResultsView {
offset: usize, offset: usize,
append: bool, append: bool,
) -> u32 { ) -> u32 {
if let Some(SearchResult::Episodes(results)) = if let Ok(SearchResult::Episodes(results)) =
spotify spotify
.api .api
.search(SearchType::Episode, query, 50, offset as u32) .search(SearchType::Episode, query, 50, offset as u32)

View File

@@ -32,7 +32,7 @@ impl TabbedView {
Default::default() 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<String>, view: impl IntoBoxedViewExt) { pub fn add_tab(&mut self, title: impl Into<String>, view: impl IntoBoxedViewExt) {
let tab = BoxedViewExt::new(view.into_boxed_view_ext()).with_name(title); let tab = BoxedViewExt::new(view.into_boxed_view_ext()).with_name(title);
self.tabs.push(tab); self.tabs.push(tab);
@@ -54,7 +54,7 @@ impl TabbedView {
self.tabs.len() 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 { pub fn is_empty(&self) -> bool {
self.len() == 0 self.len() == 0
} }