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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -233,14 +233,17 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the user configuration values.
|
||||
pub fn values(&self) -> RwLockReadGuard<ConfigValues> {
|
||||
self.values.read().expect("can't readlock config values")
|
||||
}
|
||||
|
||||
/// Get the runtime user state values.
|
||||
pub fn state(&self) -> RwLockReadGuard<UserState> {
|
||||
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)
|
||||
where
|
||||
F: Fn(RwLockWriteGuard<UserState>),
|
||||
@@ -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.
|
||||
|
||||
@@ -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<Event>;
|
||||
|
||||
/// Manager that can be used to send and receive messages across threads.
|
||||
#[derive(Clone)]
|
||||
pub struct EventManager {
|
||||
tx: EventSender,
|
||||
tx: Sender<Event>,
|
||||
rx: Receiver<Event>,
|
||||
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> {
|
||||
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");
|
||||
|
||||
@@ -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<RwLock<Vec<Track>>>,
|
||||
@@ -43,7 +52,7 @@ pub struct Library {
|
||||
|
||||
impl Library {
|
||||
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 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<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 {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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<Vec<Track>> = spotify
|
||||
.api
|
||||
.recommendations(Some(vec![&id]), None, None)
|
||||
.ok()
|
||||
.map(|r| r.tracks)
|
||||
.map(|tracks| tracks.iter().map(Track::from).collect());
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
15
src/mpris.rs
15
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
|
||||
/// <https://specifications.freedesktop.org/mpris-spec/2.2/#Bus-Name-Policy>
|
||||
pub fn instance_bus_name() -> String {
|
||||
format!(
|
||||
"org.mpris.MediaPlayer2.ncspot.instance{}",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<config::Config>,
|
||||
/// Playback status of the [Player] owned by the worker thread.
|
||||
status: Arc<RwLock<PlayerEvent>>,
|
||||
pub api: WebApi,
|
||||
/// The amount of the current [Playable] that had elapsed when last paused.
|
||||
elapsed: Arc<RwLock<Option<Duration>>>,
|
||||
/// The amount of the current [Playable] that has been played in total.
|
||||
since: Arc<RwLock<Option<SystemTime>>>,
|
||||
/// Channel to send commands to the worker thread.
|
||||
channel: Arc<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>,
|
||||
/// The username of the logged in user.
|
||||
user: Option<String>,
|
||||
}
|
||||
|
||||
@@ -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<oneshot::Sender<String>>) {
|
||||
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<Session, SessionError> {
|
||||
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<String>) -> Option<SinkBuilder> {
|
||||
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<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>,
|
||||
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<Self> {
|
||||
if s.starts_with("spotify:album:") {
|
||||
Some(Self::Album)
|
||||
|
||||
@@ -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::artist::Artist;
|
||||
use crate::model::category::Category;
|
||||
@@ -7,29 +25,17 @@ use crate::model::playlist::Playlist;
|
||||
use crate::model::track::Track;
|
||||
use crate::spotify_worker::WorkerCommand;
|
||||
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)]
|
||||
pub struct WebApi {
|
||||
/// Rspotify web API.
|
||||
api: AuthCodeSpotify,
|
||||
/// The username of the logged in user.
|
||||
user: Option<String>,
|
||||
/// Sender of the mpsc channel to the [Spotify](crate::spotify::Spotify) worker thread.
|
||||
worker_channel: Arc<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>,
|
||||
/// Time at which the token expires.
|
||||
token_expiration: Arc<RwLock<DateTime<Utc>>>,
|
||||
}
|
||||
|
||||
@@ -58,10 +64,13 @@ impl WebApi {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Set the username for use with the API.
|
||||
pub fn set_user(&mut self, user: Option<String>) {
|
||||
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<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>,
|
||||
@@ -115,12 +124,12 @@ impl WebApi {
|
||||
}
|
||||
}
|
||||
|
||||
/// retries once when rate limits are hit
|
||||
fn api_with_retry<F, R>(&self, cb: F) -> Option<R>
|
||||
/// Execute `api_call` and retry once if a rate limit occurs.
|
||||
fn api_with_retry<F, R>(&self, api_call: F) -> Option<R>
|
||||
where
|
||||
F: Fn(&AuthCodeSpotify) -> ClientResult<R>,
|
||||
{
|
||||
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::<u64>().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<u32>,
|
||||
) -> bool {
|
||||
) -> Result<PlaylistResult, ()> {
|
||||
self.api_with_retry(|api| {
|
||||
let trackids: Vec<PlayableId> = 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<PlaylistResult, ()> {
|
||||
self.api_with_retry(move |api| {
|
||||
let playable_ids: Vec<PlayableId> = 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<Playable> = 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<bool>,
|
||||
description: Option<&str>,
|
||||
) -> Option<String> {
|
||||
) -> Result<String, ()> {
|
||||
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<FullAlbum> {
|
||||
/// Fetch the album with the given `album_id`.
|
||||
pub fn album(&self, album_id: &str) -> Result<FullAlbum, ()> {
|
||||
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<FullArtist> {
|
||||
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<FullArtist, ()> {
|
||||
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> {
|
||||
let pid = PlaylistId::from_id(playlist_id).ok()?;
|
||||
/// Fetch the playlist with the given `playlist_id`.
|
||||
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)))
|
||||
.ok_or(())
|
||||
}
|
||||
|
||||
pub fn track(&self, track_id: &str) -> Option<FullTrack> {
|
||||
let tid = TrackId::from_id(track_id).ok()?;
|
||||
/// Fetch the track with the given `track_id`.
|
||||
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)))
|
||||
.ok_or(())
|
||||
}
|
||||
|
||||
pub fn get_show(&self, show_id: &str) -> Option<FullShow> {
|
||||
let sid = ShowId::from_id(show_id).ok()?;
|
||||
/// Fetch the show with the given `show_id`.
|
||||
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)))
|
||||
.ok_or(())
|
||||
}
|
||||
|
||||
pub fn episode(&self, episode_id: &str) -> Option<FullEpisode> {
|
||||
let eid = EpisodeId::from_id(episode_id).ok()?;
|
||||
/// Fetch the episode with the given `episode_id`.
|
||||
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)))
|
||||
.ok_or(())
|
||||
}
|
||||
|
||||
/// Get recommendations based on the seeds provided with `seed_artists`, `seed_genres` and
|
||||
/// `seed_tracks`.
|
||||
pub fn recommendations(
|
||||
&self,
|
||||
seed_artists: Option<Vec<&str>>,
|
||||
seed_genres: Option<Vec<&str>>,
|
||||
seed_tracks: Option<Vec<&str>>,
|
||||
) -> Option<Recommendations> {
|
||||
) -> Result<Recommendations, ()> {
|
||||
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<SearchResult> {
|
||||
) -> Result<SearchResult, ()> {
|
||||
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<Playlist> {
|
||||
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<Playable> {
|
||||
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<Page<SimplifiedTrack>> {
|
||||
) -> Result<Page<SimplifiedTrack>, ()> {
|
||||
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<Episode> {
|
||||
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<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)))
|
||||
.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::<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| {
|
||||
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<CursorBasedPage<FullArtist>> {
|
||||
) -> Result<CursorBasedPage<FullArtist>, ()> {
|
||||
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::<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| {
|
||||
api.user_unfollow_artists(
|
||||
ids.iter()
|
||||
@@ -547,15 +597,19 @@ impl WebApi {
|
||||
.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| {
|
||||
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::<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| {
|
||||
api.current_user_saved_albums_delete(
|
||||
ids.iter()
|
||||
@@ -573,15 +629,19 @@ impl WebApi {
|
||||
.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| {
|
||||
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::<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| {
|
||||
api.current_user_saved_tracks_delete(
|
||||
ids.iter()
|
||||
@@ -599,24 +661,32 @@ impl WebApi {
|
||||
.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))
|
||||
.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| {
|
||||
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<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()))
|
||||
.map(|fa| fa.iter().map(|a| a.into()).collect())
|
||||
.ok_or(())
|
||||
}
|
||||
|
||||
/// Get the available categories.
|
||||
pub fn categories(&self) -> ApiResult<Category> {
|
||||
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<Playlist> {
|
||||
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<PrivateUser> {
|
||||
self.api_with_retry(|api| api.current_user())
|
||||
/// Get details about the logged in user.
|
||||
pub fn current_user(&self) -> Result<PrivateUser, ()> {
|
||||
self.api_with_retry(|api| api.current_user()).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
12
src/theme.rs
12
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<ConfigTheme>) -> Theme {
|
||||
let mut palette = Palette::default();
|
||||
let borders = BorderStyle::Simple;
|
||||
|
||||
@@ -35,6 +35,7 @@ pub trait ListItem: Sync + Send + 'static {
|
||||
}
|
||||
fn share_url(&self) -> Option<String>;
|
||||
|
||||
/// Get the album that contains this [ListItem].
|
||||
fn album(&self, _queue: &Queue) -> Option<Album> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -720,27 +720,33 @@ impl<I: ListItem + Clone> ViewExt for ListView<I> {
|
||||
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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<String>, 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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user