Refactor: separate Spotify API from player logic
The separation is not perfect yet, but it's a start and makes the entire codebase much easier to read.
This commit is contained in:
@@ -35,7 +35,7 @@ impl Album {
|
||||
|
||||
if let Some(ref album_id) = self.id {
|
||||
let mut collected_tracks = Vec::new();
|
||||
if let Some(full_album) = spotify.full_album(album_id) {
|
||||
if let Some(full_album) = spotify.api.full_album(album_id) {
|
||||
let mut tracks_result = Some(full_album.tracks.clone());
|
||||
while let Some(ref tracks) = tracks_result {
|
||||
for t in &tracks.items {
|
||||
@@ -48,7 +48,7 @@ impl Album {
|
||||
tracks_result = match tracks.next {
|
||||
Some(_) => {
|
||||
debug!("requesting tracks again..");
|
||||
spotify.album_tracks(
|
||||
spotify.api.album_tracks(
|
||||
album_id,
|
||||
50,
|
||||
tracks.offset + tracks.items.len() as u32,
|
||||
|
||||
@@ -34,7 +34,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.artist_top_tracks(artist_id);
|
||||
self.tracks = spotify.api.artist_top_tracks(artist_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ impl CommandManager {
|
||||
Ok(None)
|
||||
}
|
||||
Command::NewPlaylist(name) => {
|
||||
match self.spotify.create_playlist(name, None, None) {
|
||||
match self.spotify.api.create_playlist(name, None, None) {
|
||||
Some(_) => self.library.update_library(),
|
||||
None => error!("could not create playlist {}", name),
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ pub struct Library {
|
||||
|
||||
impl Library {
|
||||
pub fn new(ev: &EventManager, spotify: Spotify, cfg: Arc<Config>) -> Self {
|
||||
let current_user = spotify.current_user();
|
||||
let current_user = spotify.api.current_user();
|
||||
let user_id = current_user.as_ref().map(|u| u.id.clone());
|
||||
let display_name = current_user.as_ref().and_then(|u| u.display_name.clone());
|
||||
|
||||
@@ -131,7 +131,7 @@ impl Library {
|
||||
};
|
||||
|
||||
if let Some(position) = pos {
|
||||
if self.spotify.delete_playlist(id) {
|
||||
if self.spotify.api.delete_playlist(id) {
|
||||
{
|
||||
let mut store = self.playlists.write().expect("can't writelock playlists");
|
||||
store.remove(position);
|
||||
@@ -143,7 +143,7 @@ impl Library {
|
||||
|
||||
pub fn overwrite_playlist(&self, id: &str, tracks: &[Playable]) {
|
||||
debug!("saving {} tracks to list {}", tracks.len(), id);
|
||||
self.spotify.overwrite_playlist(id, tracks);
|
||||
self.spotify.api.overwrite_playlist(id, tracks);
|
||||
|
||||
self.fetch_playlists();
|
||||
self.save_cache(config::cache_path(CACHE_PLAYLISTS), self.playlists.clone());
|
||||
@@ -151,7 +151,7 @@ impl Library {
|
||||
|
||||
pub fn save_playlist(&self, name: &str, tracks: &[Playable]) {
|
||||
debug!("saving {} tracks to new list {}", tracks.len(), name);
|
||||
match self.spotify.create_playlist(name, None, None) {
|
||||
match self.spotify.api.create_playlist(name, None, None) {
|
||||
Some(id) => self.overwrite_playlist(&id, tracks),
|
||||
None => error!("could not create new playlist.."),
|
||||
}
|
||||
@@ -231,7 +231,7 @@ impl Library {
|
||||
debug!("loading shows");
|
||||
|
||||
let mut saved_shows: Vec<Show> = Vec::new();
|
||||
let mut shows_result = self.spotify.get_saved_shows(0);
|
||||
let mut shows_result = self.spotify.api.get_saved_shows(0);
|
||||
|
||||
while let Some(shows) = shows_result.as_ref() {
|
||||
saved_shows.extend(shows.items.iter().map(|show| (&show.show).into()));
|
||||
@@ -241,6 +241,7 @@ impl Library {
|
||||
Some(_) => {
|
||||
debug!("requesting shows again..");
|
||||
self.spotify
|
||||
.api
|
||||
.get_saved_shows(shows.offset + shows.items.len() as u32)
|
||||
}
|
||||
None => None,
|
||||
@@ -255,7 +256,7 @@ impl Library {
|
||||
let mut stale_lists = self.playlists.read().unwrap().clone();
|
||||
let mut list_order = Vec::new();
|
||||
|
||||
let lists_page = self.spotify.current_user_playlist();
|
||||
let lists_page = self.spotify.api.current_user_playlist();
|
||||
let mut lists_batch = Some(lists_page.items.read().unwrap().clone());
|
||||
while let Some(lists) = &lists_batch {
|
||||
for (index, remote) in lists.iter().enumerate() {
|
||||
@@ -311,7 +312,7 @@ impl Library {
|
||||
let mut i: u32 = 0;
|
||||
|
||||
loop {
|
||||
let page = self.spotify.current_user_followed_artists(last);
|
||||
let page = self.spotify.api.current_user_followed_artists(last);
|
||||
debug!("artists page: {}", i);
|
||||
i += 1;
|
||||
if page.is_none() {
|
||||
@@ -363,7 +364,10 @@ impl Library {
|
||||
let mut i: u32 = 0;
|
||||
|
||||
loop {
|
||||
let page = self.spotify.current_user_saved_albums(albums.len() as u32);
|
||||
let page = self
|
||||
.spotify
|
||||
.api
|
||||
.current_user_saved_albums(albums.len() as u32);
|
||||
debug!("albums page: {}", i);
|
||||
i += 1;
|
||||
if page.is_none() {
|
||||
@@ -409,7 +413,10 @@ impl Library {
|
||||
let mut i: u32 = 0;
|
||||
|
||||
loop {
|
||||
let page = self.spotify.current_user_saved_tracks(tracks.len() as u32);
|
||||
let page = self
|
||||
.spotify
|
||||
.api
|
||||
.current_user_saved_tracks(tracks.len() as u32);
|
||||
|
||||
debug!("tracks page: {}", i);
|
||||
i += 1;
|
||||
@@ -539,6 +546,7 @@ impl Library {
|
||||
if api
|
||||
&& self
|
||||
.spotify
|
||||
.api
|
||||
.current_user_saved_tracks_add(tracks.iter().filter_map(|t| t.id.clone()).collect())
|
||||
.is_none()
|
||||
{
|
||||
@@ -572,6 +580,7 @@ impl Library {
|
||||
if api
|
||||
&& self
|
||||
.spotify
|
||||
.api
|
||||
.current_user_saved_tracks_delete(
|
||||
tracks.iter().filter_map(|t| t.id.clone()).collect(),
|
||||
)
|
||||
@@ -612,6 +621,7 @@ impl Library {
|
||||
if let Some(ref album_id) = album.id {
|
||||
if self
|
||||
.spotify
|
||||
.api
|
||||
.current_user_saved_albums_add(vec![album_id.clone()])
|
||||
.is_none()
|
||||
{
|
||||
@@ -637,6 +647,7 @@ impl Library {
|
||||
if let Some(ref album_id) = album.id {
|
||||
if self
|
||||
.spotify
|
||||
.api
|
||||
.current_user_saved_albums_delete(vec![album_id.clone()])
|
||||
.is_none()
|
||||
{
|
||||
@@ -669,6 +680,7 @@ impl Library {
|
||||
if let Some(ref artist_id) = artist.id {
|
||||
if self
|
||||
.spotify
|
||||
.api
|
||||
.user_follow_artists(vec![artist_id.clone()])
|
||||
.is_none()
|
||||
{
|
||||
@@ -700,6 +712,7 @@ impl Library {
|
||||
if let Some(ref artist_id) = artist.id {
|
||||
if self
|
||||
.spotify
|
||||
.api
|
||||
.user_unfollow_artists(vec![artist_id.clone()])
|
||||
.is_none()
|
||||
{
|
||||
@@ -742,6 +755,7 @@ impl Library {
|
||||
|
||||
if self
|
||||
.spotify
|
||||
.api
|
||||
.user_playlist_follow_playlist(playlist.owner_id.clone(), playlist.id.clone())
|
||||
.is_none()
|
||||
{
|
||||
@@ -775,7 +789,7 @@ impl Library {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.spotify.save_shows(vec![show.id.clone()]) {
|
||||
if self.spotify.api.save_shows(vec![show.id.clone()]) {
|
||||
{
|
||||
let mut store = self.shows.write().unwrap();
|
||||
if !store.iter().any(|s| s.id == show.id) {
|
||||
@@ -790,7 +804,7 @@ impl Library {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.spotify.unsave_shows(vec![show.id.clone()]) {
|
||||
if self.spotify.api.unsave_shows(vec![show.id.clone()]) {
|
||||
{
|
||||
let mut store = self.shows.write().unwrap();
|
||||
*store = store.iter().filter(|s| s.id != show.id).cloned().collect();
|
||||
|
||||
@@ -34,6 +34,7 @@ mod serialization;
|
||||
mod sharing;
|
||||
mod show;
|
||||
mod spotify;
|
||||
mod spotify_api;
|
||||
mod spotify_url;
|
||||
mod spotify_worker;
|
||||
mod theme;
|
||||
|
||||
13
src/mpris.rs
13
src/mpris.rs
@@ -47,6 +47,7 @@ fn get_metadata(playable: Option<Playable>, spotify: Spotify) -> Metadata {
|
||||
Some(Playable::Track(track))
|
||||
} else {
|
||||
spotify
|
||||
.api
|
||||
.track(&track.id.unwrap_or_default())
|
||||
.as_ref()
|
||||
.map(|t| Playable::Track(t.into()))
|
||||
@@ -558,7 +559,7 @@ fn run_dbus_server(
|
||||
let uri_type = UriType::from_uri(&uri);
|
||||
match uri_type {
|
||||
Some(UriType::Album) => {
|
||||
if let Some(a) = spotify.album(id) {
|
||||
if let Some(a) = spotify.api.album(id) {
|
||||
if let Some(t) = &Album::from(&a).tracks {
|
||||
queue.clear();
|
||||
let index = queue.append_next(
|
||||
@@ -571,14 +572,14 @@ fn run_dbus_server(
|
||||
}
|
||||
}
|
||||
Some(UriType::Track) => {
|
||||
if let Some(t) = spotify.track(id) {
|
||||
if let Some(t) = spotify.api.track(id) {
|
||||
queue.clear();
|
||||
queue.append(Playable::Track(Track::from(&t)));
|
||||
queue.play(0, false, false)
|
||||
}
|
||||
}
|
||||
Some(UriType::Playlist) => {
|
||||
if let Some(p) = spotify.playlist(id) {
|
||||
if let Some(p) = spotify.api.playlist(id) {
|
||||
let mut playlist = Playlist::from(&p);
|
||||
let spotify = spotify.clone();
|
||||
playlist.load_tracks(spotify);
|
||||
@@ -594,7 +595,7 @@ fn run_dbus_server(
|
||||
}
|
||||
}
|
||||
Some(UriType::Show) => {
|
||||
if let Some(s) = spotify.get_show(id) {
|
||||
if let Some(s) = spotify.api.get_show(id) {
|
||||
let mut show: Show = (&s).into();
|
||||
let spotify = spotify.clone();
|
||||
show.load_all_episodes(spotify);
|
||||
@@ -612,14 +613,14 @@ fn run_dbus_server(
|
||||
}
|
||||
}
|
||||
Some(UriType::Episode) => {
|
||||
if let Some(e) = spotify.episode(id) {
|
||||
if let Some(e) = spotify.api.episode(id) {
|
||||
queue.clear();
|
||||
queue.append(Playable::Episode(Episode::from(&e)));
|
||||
queue.play(0, false, false)
|
||||
}
|
||||
}
|
||||
Some(UriType::Artist) => {
|
||||
if let Some(a) = spotify.artist_top_tracks(id) {
|
||||
if let Some(a) = spotify.api.artist_top_tracks(id) {
|
||||
queue.clear();
|
||||
queue.append_next(a.iter().map(|track| Playable::Track(track.clone())).collect());
|
||||
queue.play(0, false, false)
|
||||
|
||||
@@ -33,7 +33,7 @@ impl Playlist {
|
||||
}
|
||||
|
||||
fn get_all_tracks(&self, spotify: Spotify) -> Vec<Track> {
|
||||
let tracks_result = spotify.user_playlist_tracks(&self.id);
|
||||
let tracks_result = spotify.api.user_playlist_tracks(&self.id);
|
||||
while !tracks_result.at_end() {
|
||||
tracks_result.next();
|
||||
}
|
||||
@@ -53,7 +53,10 @@ impl Playlist {
|
||||
pub fn delete_track(&mut self, index: usize, spotify: Spotify, library: Arc<Library>) -> bool {
|
||||
let track = self.tracks.as_ref().unwrap()[index].clone();
|
||||
debug!("deleting track: {} {:?}", index, track);
|
||||
match spotify.delete_tracks(&self.id, &self.snapshot_id, &[(&track, track.list_index)]) {
|
||||
match spotify
|
||||
.api
|
||||
.delete_tracks(&self.id, &self.snapshot_id, &[(&track, track.list_index)])
|
||||
{
|
||||
false => false,
|
||||
true => {
|
||||
if let Some(tracks) = &mut self.tracks {
|
||||
@@ -75,7 +78,7 @@ impl Playlist {
|
||||
|
||||
let mut has_modified = false;
|
||||
|
||||
if spotify.append_tracks(&self.id, &track_ids, None) {
|
||||
if spotify.api.append_tracks(&self.id, &track_ids, None) {
|
||||
if let Some(tracks) = &mut self.tracks {
|
||||
tracks.append(&mut new_tracks.to_vec());
|
||||
has_modified = true;
|
||||
|
||||
@@ -26,7 +26,7 @@ impl Show {
|
||||
return;
|
||||
}
|
||||
|
||||
let episodes_result = spotify.show_episodes(&self.id);
|
||||
let episodes_result = spotify.api.show_episodes(&self.id);
|
||||
while !episodes_result.at_end() {
|
||||
episodes_result.next();
|
||||
}
|
||||
|
||||
455
src/spotify.rs
455
src/spotify.rs
@@ -11,20 +11,7 @@ use librespot_playback::audio_backend;
|
||||
use librespot_playback::config::Bitrate;
|
||||
use librespot_playback::player::Player;
|
||||
|
||||
use rspotify::blocking::client::Spotify as SpotifyAPI;
|
||||
use rspotify::model::album::{FullAlbum, SavedAlbum};
|
||||
use rspotify::model::artist::FullArtist;
|
||||
use rspotify::model::page::{CursorBasedPage, Page};
|
||||
use rspotify::model::playlist::FullPlaylist;
|
||||
use rspotify::model::search::SearchResult;
|
||||
use rspotify::model::track::{FullTrack, SavedTrack, SimplifiedTrack};
|
||||
use rspotify::model::user::PrivateUser;
|
||||
use rspotify::senum::{AlbumType, SearchType};
|
||||
use rspotify::{blocking::client::ApiError, senum::Country};
|
||||
|
||||
use serde_json::{json, Map};
|
||||
|
||||
use failure::Error;
|
||||
use rspotify::senum::Country;
|
||||
|
||||
use futures::channel::oneshot;
|
||||
use tokio::sync::mpsc;
|
||||
@@ -34,22 +21,13 @@ use url::Url;
|
||||
use std::env;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use crate::artist::Artist;
|
||||
use crate::config;
|
||||
use crate::events::{Event, EventManager};
|
||||
use crate::playable::Playable;
|
||||
use crate::spotify_api::WebApi;
|
||||
use crate::spotify_worker::{Worker, WorkerCommand};
|
||||
use crate::track::Track;
|
||||
|
||||
use crate::album::Album;
|
||||
use crate::episode::Episode;
|
||||
use crate::playlist::Playlist;
|
||||
use crate::ui::pagination::{ApiPage, ApiResult};
|
||||
use rspotify::model::recommend::Recommendations;
|
||||
use rspotify::model::show::{FullEpisode, FullShow, Show};
|
||||
|
||||
pub const VOLUME_PERCENT: u16 = ((u16::max_value() as f64) * 1.0 / 100.0) as u16;
|
||||
|
||||
@@ -67,13 +45,12 @@ pub struct Spotify {
|
||||
credentials: Credentials,
|
||||
cfg: Arc<config::Config>,
|
||||
status: Arc<RwLock<PlayerEvent>>,
|
||||
api: Arc<RwLock<SpotifyAPI>>,
|
||||
pub api: WebApi,
|
||||
elapsed: Arc<RwLock<Option<Duration>>>,
|
||||
since: Arc<RwLock<Option<SystemTime>>>,
|
||||
token_issued: Arc<RwLock<Option<SystemTime>>>,
|
||||
channel: Arc<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>,
|
||||
user: Option<String>,
|
||||
country: Option<Country>,
|
||||
}
|
||||
|
||||
impl Spotify {
|
||||
@@ -87,13 +64,12 @@ impl Spotify {
|
||||
credentials,
|
||||
cfg: cfg.clone(),
|
||||
status: Arc::new(RwLock::new(PlayerEvent::Stopped)),
|
||||
api: Arc::new(RwLock::new(SpotifyAPI::default())),
|
||||
api: WebApi::new(),
|
||||
elapsed: Arc::new(RwLock::new(None)),
|
||||
since: Arc::new(RwLock::new(None)),
|
||||
token_issued: Arc::new(RwLock::new(None)),
|
||||
channel: Arc::new(RwLock::new(None)),
|
||||
user: None,
|
||||
country: None,
|
||||
};
|
||||
|
||||
let (user_tx, user_rx) = oneshot::channel();
|
||||
@@ -102,11 +78,18 @@ impl Spotify {
|
||||
let volume = cfg.state().volume;
|
||||
spotify.set_volume(volume);
|
||||
|
||||
spotify.country = spotify
|
||||
spotify.api.set_worker_channel(spotify.channel.clone());
|
||||
spotify.api.update_token();
|
||||
|
||||
let country: Option<Country> = spotify
|
||||
.api
|
||||
.current_user()
|
||||
.and_then(|u| u.country)
|
||||
.and_then(|c| c.parse().ok());
|
||||
|
||||
spotify.api.set_user(spotify.user.clone());
|
||||
spotify.api.set_country(country);
|
||||
|
||||
spotify
|
||||
}
|
||||
|
||||
@@ -136,9 +119,6 @@ impl Spotify {
|
||||
.await
|
||||
});
|
||||
}
|
||||
|
||||
// acquire token for web api usage
|
||||
self.refresh_token();
|
||||
}
|
||||
|
||||
pub fn session_config() -> SessionConfig {
|
||||
@@ -292,417 +272,6 @@ impl Spotify {
|
||||
*since
|
||||
}
|
||||
|
||||
pub fn refresh_token(&self) {
|
||||
{
|
||||
let expiry = self.token_issued.read().unwrap();
|
||||
if let Some(time) = *expiry {
|
||||
if time.elapsed().unwrap() < Duration::from_secs(3000) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (token_tx, token_rx) = oneshot::channel();
|
||||
self.send_worker(WorkerCommand::RequestToken(token_tx));
|
||||
let token = futures::executor::block_on(token_rx).unwrap();
|
||||
|
||||
// update token used by web api calls
|
||||
self.api.write().expect("can't writelock api").access_token = Some(token.access_token);
|
||||
self.token_issued
|
||||
.write()
|
||||
.unwrap()
|
||||
.replace(SystemTime::now());
|
||||
}
|
||||
|
||||
/// retries once when rate limits are hit
|
||||
fn api_with_retry<F, R>(&self, cb: F) -> Option<R>
|
||||
where
|
||||
F: Fn(&SpotifyAPI) -> Result<R, Error>,
|
||||
{
|
||||
let result = {
|
||||
let api = self.api.read().expect("can't read api");
|
||||
cb(&api)
|
||||
};
|
||||
match result {
|
||||
Ok(v) => Some(v),
|
||||
Err(e) => {
|
||||
debug!("api error: {:?}", e);
|
||||
if let Ok(apierror) = e.downcast::<ApiError>() {
|
||||
match apierror {
|
||||
ApiError::RateLimited(d) => {
|
||||
debug!("rate limit hit. waiting {:?} seconds", d);
|
||||
thread::sleep(Duration::from_secs(d.unwrap_or(0) as u64));
|
||||
let api = self.api.read().expect("can't read api");
|
||||
cb(&api).ok()
|
||||
}
|
||||
ApiError::Unauthorized => {
|
||||
debug!("token unauthorized. trying refresh..");
|
||||
self.refresh_token();
|
||||
let api = self.api.read().expect("can't read api");
|
||||
cb(&api).ok()
|
||||
}
|
||||
e => {
|
||||
error!("unhandled api error: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_tracks(
|
||||
&self,
|
||||
playlist_id: &str,
|
||||
tracks: &[String],
|
||||
position: Option<i32>,
|
||||
) -> bool {
|
||||
self.api_with_retry(|api| {
|
||||
api.user_playlist_add_tracks(self.user.as_ref().unwrap(), playlist_id, tracks, position)
|
||||
})
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn delete_tracks(
|
||||
&self,
|
||||
playlist_id: &str,
|
||||
snapshot_id: &str,
|
||||
track_pos_pairs: &[(&Track, usize)],
|
||||
) -> bool {
|
||||
let mut tracks = Vec::new();
|
||||
for (track, pos) in track_pos_pairs {
|
||||
let track_occurrence = json!({
|
||||
"uri": format!("spotify:track:{}", track.id.clone().unwrap()),
|
||||
"positions": [pos]
|
||||
});
|
||||
let track_occurrence_object = track_occurrence.as_object();
|
||||
tracks.push(track_occurrence_object.unwrap().clone());
|
||||
}
|
||||
self.api_with_retry(|api| {
|
||||
api.user_playlist_remove_specific_occurrenes_of_tracks(
|
||||
self.user.as_ref().unwrap(),
|
||||
playlist_id,
|
||||
tracks.clone(),
|
||||
Some(snapshot_id.to_string()),
|
||||
)
|
||||
})
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn overwrite_playlist(&self, id: &str, tracks: &[Playable]) {
|
||||
// extract only track IDs
|
||||
let mut tracks: Vec<String> = tracks.iter().filter_map(|track| track.id()).collect();
|
||||
|
||||
// we can only send 100 tracks per request
|
||||
let mut remainder = if tracks.len() > 100 {
|
||||
Some(tracks.split_off(100))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(()) = self.api_with_retry(|api| {
|
||||
api.user_playlist_replace_tracks(self.user.as_ref().unwrap(), id, &tracks)
|
||||
}) {
|
||||
debug!("saved {} tracks to playlist {}", tracks.len(), id);
|
||||
while let Some(ref mut tracks) = remainder.clone() {
|
||||
// grab the next set of 100 tracks
|
||||
remainder = if tracks.len() > 100 {
|
||||
Some(tracks.split_off(100))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
debug!("adding another {} tracks to playlist", tracks.len());
|
||||
if self.append_tracks(id, tracks, None) {
|
||||
debug!("{} tracks successfully added", tracks.len());
|
||||
} else {
|
||||
error!("error saving tracks to playlists {}", id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("error saving tracks to playlist {}", id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_playlist(&self, id: &str) -> bool {
|
||||
self.api_with_retry(|api| api.user_playlist_unfollow(self.user.as_ref().unwrap(), id))
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn create_playlist(
|
||||
&self,
|
||||
name: &str,
|
||||
public: Option<bool>,
|
||||
description: Option<String>,
|
||||
) -> Option<String> {
|
||||
let result = self.api_with_retry(|api| {
|
||||
api.user_playlist_create(
|
||||
self.user.as_ref().unwrap(),
|
||||
name,
|
||||
public,
|
||||
description.clone(),
|
||||
)
|
||||
});
|
||||
result.map(|r| r.id)
|
||||
}
|
||||
|
||||
pub fn album(&self, album_id: &str) -> Option<FullAlbum> {
|
||||
self.api_with_retry(|api| api.album(album_id))
|
||||
}
|
||||
|
||||
pub fn artist(&self, artist_id: &str) -> Option<FullArtist> {
|
||||
self.api_with_retry(|api| api.artist(artist_id))
|
||||
}
|
||||
|
||||
pub fn playlist(&self, playlist_id: &str) -> Option<FullPlaylist> {
|
||||
self.api_with_retry(|api| api.playlist(playlist_id, None, self.country))
|
||||
}
|
||||
|
||||
pub fn track(&self, track_id: &str) -> Option<FullTrack> {
|
||||
self.api_with_retry(|api| api.track(track_id))
|
||||
}
|
||||
|
||||
pub fn get_show(&self, show_id: &str) -> Option<FullShow> {
|
||||
self.api_with_retry(|api| api.get_a_show(show_id.to_string(), self.country))
|
||||
}
|
||||
|
||||
pub fn episode(&self, episode_id: &str) -> Option<FullEpisode> {
|
||||
self.api_with_retry(|api| api.get_an_episode(episode_id.to_string(), self.country))
|
||||
}
|
||||
|
||||
pub fn recommendations(
|
||||
&self,
|
||||
seed_artists: Option<Vec<String>>,
|
||||
seed_genres: Option<Vec<String>>,
|
||||
seed_tracks: Option<Vec<String>>,
|
||||
) -> Option<Recommendations> {
|
||||
self.api_with_retry(|api| {
|
||||
api.recommendations(
|
||||
seed_artists.clone(),
|
||||
seed_genres.clone(),
|
||||
seed_tracks.clone(),
|
||||
100,
|
||||
self.country,
|
||||
&Map::new(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn search(
|
||||
&self,
|
||||
searchtype: SearchType,
|
||||
query: &str,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Option<SearchResult> {
|
||||
self.api_with_retry(|api| api.search(query, searchtype, limit, offset, self.country, None))
|
||||
.take()
|
||||
}
|
||||
|
||||
pub fn current_user_playlist(&self) -> ApiResult<Playlist> {
|
||||
const MAX_LIMIT: u32 = 50;
|
||||
let spotify = self.clone();
|
||||
let fetch_page = move |offset: u32| {
|
||||
debug!("fetching user playlists, offset: {}", offset);
|
||||
spotify.api_with_retry(|api| match api.current_user_playlists(MAX_LIMIT, offset) {
|
||||
Ok(page) => Ok(ApiPage {
|
||||
offset: page.offset,
|
||||
total: page.total,
|
||||
items: page.items.iter().map(|sp| sp.into()).collect(),
|
||||
}),
|
||||
Err(e) => Err(e),
|
||||
})
|
||||
};
|
||||
ApiResult::new(MAX_LIMIT, Arc::new(fetch_page))
|
||||
}
|
||||
|
||||
pub fn user_playlist_tracks(&self, playlist_id: &str) -> ApiResult<Track> {
|
||||
const MAX_LIMIT: u32 = 100;
|
||||
let spotify = self.clone();
|
||||
let playlist_id = playlist_id.to_string();
|
||||
let fetch_page = move |offset: u32| {
|
||||
debug!(
|
||||
"fetching playlist {} tracks, offset: {}",
|
||||
playlist_id, offset
|
||||
);
|
||||
spotify.api_with_retry(|api| {
|
||||
match api.user_playlist_tracks(
|
||||
spotify.user.as_ref().unwrap(),
|
||||
&playlist_id,
|
||||
None,
|
||||
MAX_LIMIT,
|
||||
offset,
|
||||
spotify.country,
|
||||
) {
|
||||
Ok(page) => Ok(ApiPage {
|
||||
offset: page.offset,
|
||||
total: page.total,
|
||||
items: page
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(index, pt)| {
|
||||
pt.track.as_ref().map(|t| {
|
||||
let mut track: Track = t.into();
|
||||
track.added_at = Some(pt.added_at);
|
||||
track.list_index = page.offset as usize + index;
|
||||
track
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
})
|
||||
};
|
||||
ApiResult::new(MAX_LIMIT, Arc::new(fetch_page))
|
||||
}
|
||||
|
||||
pub fn full_album(&self, album_id: &str) -> Option<FullAlbum> {
|
||||
self.api_with_retry(|api| api.album(album_id))
|
||||
}
|
||||
|
||||
pub fn album_tracks(
|
||||
&self,
|
||||
album_id: &str,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Option<Page<SimplifiedTrack>> {
|
||||
self.api_with_retry(|api| api.album_track(album_id, limit, offset))
|
||||
}
|
||||
|
||||
pub fn artist_albums(
|
||||
&self,
|
||||
artist_id: &str,
|
||||
album_type: Option<AlbumType>,
|
||||
) -> ApiResult<Album> {
|
||||
const MAX_SIZE: u32 = 50;
|
||||
let spotify = self.clone();
|
||||
let artist_id = artist_id.to_string();
|
||||
let fetch_page = move |offset: u32| {
|
||||
debug!("fetching artist {} albums, offset: {}", artist_id, offset);
|
||||
spotify.api_with_retry(|api| {
|
||||
match api.artist_albums(
|
||||
&artist_id,
|
||||
album_type,
|
||||
spotify.country,
|
||||
Some(MAX_SIZE),
|
||||
Some(offset),
|
||||
) {
|
||||
Ok(page) => {
|
||||
let mut albums: Vec<Album> =
|
||||
page.items.iter().map(|sa| sa.into()).collect();
|
||||
albums.sort_by(|a, b| b.year.cmp(&a.year));
|
||||
Ok(ApiPage {
|
||||
offset: page.offset,
|
||||
total: page.total,
|
||||
items: albums,
|
||||
})
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
ApiResult::new(MAX_SIZE, Arc::new(fetch_page))
|
||||
}
|
||||
|
||||
pub fn show_episodes(&self, show_id: &str) -> ApiResult<Episode> {
|
||||
const MAX_SIZE: u32 = 50;
|
||||
let spotify = self.clone();
|
||||
let show_id = show_id.to_string();
|
||||
let fetch_page = move |offset: u32| {
|
||||
debug!("fetching show {} episodes, offset: {}", &show_id, offset);
|
||||
spotify.api_with_retry(|api| {
|
||||
match api.get_shows_episodes(show_id.clone(), MAX_SIZE, offset, spotify.country) {
|
||||
Ok(page) => Ok(ApiPage {
|
||||
offset: page.offset,
|
||||
total: page.total,
|
||||
items: page.items.iter().map(|se| se.into()).collect(),
|
||||
}),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
ApiResult::new(MAX_SIZE, Arc::new(fetch_page))
|
||||
}
|
||||
|
||||
pub fn get_saved_shows(&self, offset: u32) -> Option<Page<Show>> {
|
||||
self.api_with_retry(|api| api.get_saved_show(50, offset))
|
||||
}
|
||||
|
||||
pub fn save_shows(&self, ids: Vec<String>) -> bool {
|
||||
self.api_with_retry(|api| api.save_shows(ids.clone()))
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn unsave_shows(&self, ids: Vec<String>) -> bool {
|
||||
self.api_with_retry(|api| api.remove_users_saved_shows(ids.clone(), self.country))
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn current_user_followed_artists(
|
||||
&self,
|
||||
last: Option<String>,
|
||||
) -> Option<CursorBasedPage<FullArtist>> {
|
||||
self.api_with_retry(|api| api.current_user_followed_artists(50, last.clone()))
|
||||
.map(|cp| cp.artists)
|
||||
}
|
||||
|
||||
pub fn user_follow_artists(&self, ids: Vec<String>) -> Option<()> {
|
||||
self.api_with_retry(|api| api.user_follow_artists(&ids))
|
||||
}
|
||||
|
||||
pub fn user_unfollow_artists(&self, ids: Vec<String>) -> Option<()> {
|
||||
self.api_with_retry(|api| api.user_unfollow_artists(&ids))
|
||||
}
|
||||
|
||||
pub fn current_user_saved_albums(&self, offset: u32) -> Option<Page<SavedAlbum>> {
|
||||
self.api_with_retry(|api| api.current_user_saved_albums(50, offset))
|
||||
}
|
||||
|
||||
pub fn current_user_saved_albums_add(&self, ids: Vec<String>) -> Option<()> {
|
||||
self.api_with_retry(|api| api.current_user_saved_albums_add(&ids))
|
||||
}
|
||||
|
||||
pub fn current_user_saved_albums_delete(&self, ids: Vec<String>) -> Option<()> {
|
||||
self.api_with_retry(|api| api.current_user_saved_albums_delete(&ids))
|
||||
}
|
||||
|
||||
pub fn current_user_saved_tracks(&self, offset: u32) -> Option<Page<SavedTrack>> {
|
||||
self.api_with_retry(|api| api.current_user_saved_tracks(50, offset))
|
||||
}
|
||||
|
||||
pub fn current_user_saved_tracks_add(&self, ids: Vec<String>) -> Option<()> {
|
||||
self.api_with_retry(|api| api.current_user_saved_tracks_add(&ids))
|
||||
}
|
||||
|
||||
pub fn current_user_saved_tracks_delete(&self, ids: Vec<String>) -> Option<()> {
|
||||
self.api_with_retry(|api| api.current_user_saved_tracks_delete(&ids))
|
||||
}
|
||||
|
||||
pub fn user_playlist_follow_playlist(&self, owner_id: String, id: String) -> Option<()> {
|
||||
self.api_with_retry(|api| api.user_playlist_follow_playlist(&owner_id, &id, true))
|
||||
}
|
||||
|
||||
pub fn artist_top_tracks(&self, id: &str) -> Option<Vec<Track>> {
|
||||
self.api_with_retry(|api| api.artist_top_tracks(id, self.country))
|
||||
.map(|ft| ft.tracks.iter().map(|t| t.into()).collect())
|
||||
}
|
||||
|
||||
pub fn artist_related_artists(&self, id: String) -> Option<Vec<Artist>> {
|
||||
self.api_with_retry(|api| api.artist_related_artists(&id))
|
||||
.map(|fa| fa.artists.iter().map(|a| a.into()).collect())
|
||||
}
|
||||
|
||||
pub fn current_user(&self) -> Option<PrivateUser> {
|
||||
self.api_with_retry(|api| api.current_user())
|
||||
}
|
||||
|
||||
pub fn load(&self, track: &Playable, start_playing: bool, position_ms: u32) {
|
||||
info!("loading track: {:?}", track);
|
||||
self.send_worker(WorkerCommand::Load(
|
||||
|
||||
490
src/spotify_api.rs
Normal file
490
src/spotify_api.rs
Normal file
@@ -0,0 +1,490 @@
|
||||
use crate::album::Album;
|
||||
use crate::artist::Artist;
|
||||
use crate::episode::Episode;
|
||||
use crate::playable::Playable;
|
||||
use crate::playlist::Playlist;
|
||||
use crate::spotify_worker::WorkerCommand;
|
||||
use crate::track::Track;
|
||||
use crate::ui::pagination::{ApiPage, ApiResult};
|
||||
use chrono::{DateTime, Duration as ChronoDuration, Utc};
|
||||
use failure::Error;
|
||||
use futures::channel::oneshot;
|
||||
use log::{debug, error, info};
|
||||
use rspotify::blocking::client::ApiError;
|
||||
use rspotify::blocking::client::Spotify as SpotifyAPI;
|
||||
use rspotify::model::album::{FullAlbum, SavedAlbum};
|
||||
use rspotify::model::artist::FullArtist;
|
||||
use rspotify::model::page::{CursorBasedPage, Page};
|
||||
use rspotify::model::playlist::FullPlaylist;
|
||||
use rspotify::model::recommend::Recommendations;
|
||||
use rspotify::model::search::SearchResult;
|
||||
use rspotify::model::show::{FullEpisode, FullShow, Show};
|
||||
use rspotify::model::track::{FullTrack, SavedTrack, SimplifiedTrack};
|
||||
use rspotify::model::user::PrivateUser;
|
||||
use rspotify::senum::{AlbumType, Country, SearchType};
|
||||
use serde_json::{json, Map};
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WebApi {
|
||||
api: Arc<RwLock<SpotifyAPI>>,
|
||||
user: Option<String>,
|
||||
country: Option<Country>,
|
||||
worker_channel: Arc<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>,
|
||||
token_expiration: Arc<RwLock<DateTime<Utc>>>,
|
||||
}
|
||||
|
||||
impl WebApi {
|
||||
pub fn new() -> WebApi {
|
||||
WebApi {
|
||||
api: Arc::new(RwLock::new(SpotifyAPI::default())),
|
||||
user: None,
|
||||
country: None,
|
||||
worker_channel: Arc::new(RwLock::new(None)),
|
||||
token_expiration: Arc::new(RwLock::new(Utc::now())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_user(&mut self, user: Option<String>) {
|
||||
self.user = user;
|
||||
}
|
||||
|
||||
pub fn set_country(&mut self, country: Option<Country>) {
|
||||
self.country = country;
|
||||
}
|
||||
|
||||
pub(crate) fn set_worker_channel(
|
||||
&mut self,
|
||||
channel: Arc<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>,
|
||||
) {
|
||||
self.worker_channel = channel;
|
||||
}
|
||||
|
||||
pub fn update_token(&self) {
|
||||
{
|
||||
let token_expiration = self.token_expiration.read().unwrap();
|
||||
let now = Utc::now();
|
||||
let delta = *token_expiration - now;
|
||||
|
||||
// token is valid for 5 more minutes, renewal is not necessary yet
|
||||
if delta.num_seconds() > 60 * 5 {
|
||||
return;
|
||||
}
|
||||
|
||||
info!("Token will expire in {}, renewing", delta);
|
||||
}
|
||||
|
||||
let (token_tx, token_rx) = oneshot::channel();
|
||||
let cmd = WorkerCommand::RequestToken(token_tx);
|
||||
if let Some(channel) = self
|
||||
.worker_channel
|
||||
.read()
|
||||
.expect("can't readlock worker channel")
|
||||
.as_ref()
|
||||
{
|
||||
channel.send(cmd).expect("can't send message to worker");
|
||||
let token = futures::executor::block_on(token_rx).unwrap();
|
||||
self.api.write().expect("can't writelock api").access_token =
|
||||
Some(token.access_token.to_string());
|
||||
*self
|
||||
.token_expiration
|
||||
.write()
|
||||
.expect("could not writelock token") =
|
||||
Utc::now() + ChronoDuration::seconds(token.expires_in.into());
|
||||
} else {
|
||||
error!("worker channel is not set");
|
||||
}
|
||||
}
|
||||
|
||||
/// retries once when rate limits are hit
|
||||
fn api_with_retry<F, R>(&self, cb: F) -> Option<R>
|
||||
where
|
||||
F: Fn(&SpotifyAPI) -> Result<R, Error>,
|
||||
{
|
||||
let result = {
|
||||
let api = self.api.read().expect("can't read api");
|
||||
cb(&api)
|
||||
};
|
||||
match result {
|
||||
Ok(v) => Some(v),
|
||||
Err(e) => {
|
||||
debug!("api error: {:?}", e);
|
||||
if let Ok(apierror) = e.downcast::<ApiError>() {
|
||||
match apierror {
|
||||
ApiError::RateLimited(d) => {
|
||||
debug!("rate limit hit. waiting {:?} seconds", d);
|
||||
thread::sleep(Duration::from_secs(d.unwrap_or(0) as u64));
|
||||
let api = self.api.read().expect("can't read api");
|
||||
cb(&api).ok()
|
||||
}
|
||||
ApiError::Unauthorized => {
|
||||
debug!("token unauthorized. trying refresh..");
|
||||
self.update_token();
|
||||
let api = self.api.read().expect("can't read api");
|
||||
cb(&api).ok()
|
||||
}
|
||||
e => {
|
||||
error!("unhandled api error: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_tracks(
|
||||
&self,
|
||||
playlist_id: &str,
|
||||
tracks: &[String],
|
||||
position: Option<i32>,
|
||||
) -> bool {
|
||||
self.api_with_retry(|api| {
|
||||
api.user_playlist_add_tracks(self.user.as_ref().unwrap(), playlist_id, tracks, position)
|
||||
})
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn delete_tracks(
|
||||
&self,
|
||||
playlist_id: &str,
|
||||
snapshot_id: &str,
|
||||
track_pos_pairs: &[(&Track, usize)],
|
||||
) -> bool {
|
||||
let mut tracks = Vec::new();
|
||||
for (track, pos) in track_pos_pairs {
|
||||
let track_occurrence = json!({
|
||||
"uri": format!("spotify:track:{}", track.id.clone().unwrap()),
|
||||
"positions": [pos]
|
||||
});
|
||||
let track_occurrence_object = track_occurrence.as_object();
|
||||
tracks.push(track_occurrence_object.unwrap().clone());
|
||||
}
|
||||
self.api_with_retry(|api| {
|
||||
api.user_playlist_remove_specific_occurrenes_of_tracks(
|
||||
self.user.as_ref().unwrap(),
|
||||
playlist_id,
|
||||
tracks.clone(),
|
||||
Some(snapshot_id.to_string()),
|
||||
)
|
||||
})
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn overwrite_playlist(&self, id: &str, tracks: &[Playable]) {
|
||||
// extract only track IDs
|
||||
let mut tracks: Vec<String> = tracks.iter().filter_map(|track| track.id()).collect();
|
||||
|
||||
// we can only send 100 tracks per request
|
||||
let mut remainder = if tracks.len() > 100 {
|
||||
Some(tracks.split_off(100))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(()) = self.api_with_retry(|api| {
|
||||
api.user_playlist_replace_tracks(self.user.as_ref().unwrap(), id, &tracks)
|
||||
}) {
|
||||
debug!("saved {} tracks to playlist {}", tracks.len(), id);
|
||||
while let Some(ref mut tracks) = remainder.clone() {
|
||||
// grab the next set of 100 tracks
|
||||
remainder = if tracks.len() > 100 {
|
||||
Some(tracks.split_off(100))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
debug!("adding another {} tracks to playlist", tracks.len());
|
||||
if self.append_tracks(id, tracks, None) {
|
||||
debug!("{} tracks successfully added", tracks.len());
|
||||
} else {
|
||||
error!("error saving tracks to playlists {}", id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("error saving tracks to playlist {}", id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_playlist(&self, id: &str) -> bool {
|
||||
self.api_with_retry(|api| api.user_playlist_unfollow(self.user.as_ref().unwrap(), id))
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn create_playlist(
|
||||
&self,
|
||||
name: &str,
|
||||
public: Option<bool>,
|
||||
description: Option<String>,
|
||||
) -> Option<String> {
|
||||
let result = self.api_with_retry(|api| {
|
||||
api.user_playlist_create(
|
||||
self.user.as_ref().unwrap(),
|
||||
name,
|
||||
public,
|
||||
description.clone(),
|
||||
)
|
||||
});
|
||||
result.map(|r| r.id)
|
||||
}
|
||||
|
||||
pub fn album(&self, album_id: &str) -> Option<FullAlbum> {
|
||||
self.api_with_retry(|api| api.album(album_id))
|
||||
}
|
||||
|
||||
pub fn artist(&self, artist_id: &str) -> Option<FullArtist> {
|
||||
self.api_with_retry(|api| api.artist(artist_id))
|
||||
}
|
||||
|
||||
pub fn playlist(&self, playlist_id: &str) -> Option<FullPlaylist> {
|
||||
self.api_with_retry(|api| api.playlist(playlist_id, None, self.country))
|
||||
}
|
||||
|
||||
pub fn track(&self, track_id: &str) -> Option<FullTrack> {
|
||||
self.api_with_retry(|api| api.track(track_id))
|
||||
}
|
||||
|
||||
pub fn get_show(&self, show_id: &str) -> Option<FullShow> {
|
||||
self.api_with_retry(|api| api.get_a_show(show_id.to_string(), self.country))
|
||||
}
|
||||
|
||||
pub fn episode(&self, episode_id: &str) -> Option<FullEpisode> {
|
||||
self.api_with_retry(|api| api.get_an_episode(episode_id.to_string(), self.country))
|
||||
}
|
||||
|
||||
pub fn recommendations(
|
||||
&self,
|
||||
seed_artists: Option<Vec<String>>,
|
||||
seed_genres: Option<Vec<String>>,
|
||||
seed_tracks: Option<Vec<String>>,
|
||||
) -> Option<Recommendations> {
|
||||
self.api_with_retry(|api| {
|
||||
api.recommendations(
|
||||
seed_artists.clone(),
|
||||
seed_genres.clone(),
|
||||
seed_tracks.clone(),
|
||||
100,
|
||||
self.country,
|
||||
&Map::new(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn search(
|
||||
&self,
|
||||
searchtype: SearchType,
|
||||
query: &str,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Option<SearchResult> {
|
||||
self.api_with_retry(|api| api.search(query, searchtype, limit, offset, self.country, None))
|
||||
.take()
|
||||
}
|
||||
|
||||
pub fn current_user_playlist(&self) -> ApiResult<Playlist> {
|
||||
const MAX_LIMIT: u32 = 50;
|
||||
let spotify = self.clone();
|
||||
let fetch_page = move |offset: u32| {
|
||||
debug!("fetching user playlists, offset: {}", offset);
|
||||
spotify.api_with_retry(|api| match api.current_user_playlists(MAX_LIMIT, offset) {
|
||||
Ok(page) => Ok(ApiPage {
|
||||
offset: page.offset,
|
||||
total: page.total,
|
||||
items: page.items.iter().map(|sp| sp.into()).collect(),
|
||||
}),
|
||||
Err(e) => Err(e),
|
||||
})
|
||||
};
|
||||
ApiResult::new(MAX_LIMIT, Arc::new(fetch_page))
|
||||
}
|
||||
|
||||
pub fn user_playlist_tracks(&self, playlist_id: &str) -> ApiResult<Track> {
|
||||
const MAX_LIMIT: u32 = 100;
|
||||
let spotify = self.clone();
|
||||
let playlist_id = playlist_id.to_string();
|
||||
let fetch_page = move |offset: u32| {
|
||||
debug!(
|
||||
"fetching playlist {} tracks, offset: {}",
|
||||
playlist_id, offset
|
||||
);
|
||||
spotify.api_with_retry(|api| {
|
||||
match api.user_playlist_tracks(
|
||||
spotify.user.as_ref().unwrap(),
|
||||
&playlist_id,
|
||||
None,
|
||||
MAX_LIMIT,
|
||||
offset,
|
||||
spotify.country,
|
||||
) {
|
||||
Ok(page) => Ok(ApiPage {
|
||||
offset: page.offset,
|
||||
total: page.total,
|
||||
items: page
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(index, pt)| {
|
||||
pt.track.as_ref().map(|t| {
|
||||
let mut track: Track = t.into();
|
||||
track.added_at = Some(pt.added_at);
|
||||
track.list_index = page.offset as usize + index;
|
||||
track
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
})
|
||||
};
|
||||
ApiResult::new(MAX_LIMIT, Arc::new(fetch_page))
|
||||
}
|
||||
|
||||
pub fn full_album(&self, album_id: &str) -> Option<FullAlbum> {
|
||||
self.api_with_retry(|api| api.album(album_id))
|
||||
}
|
||||
|
||||
pub fn album_tracks(
|
||||
&self,
|
||||
album_id: &str,
|
||||
limit: u32,
|
||||
offset: u32,
|
||||
) -> Option<Page<SimplifiedTrack>> {
|
||||
self.api_with_retry(|api| api.album_track(album_id, limit, offset))
|
||||
}
|
||||
|
||||
pub fn artist_albums(
|
||||
&self,
|
||||
artist_id: &str,
|
||||
album_type: Option<AlbumType>,
|
||||
) -> ApiResult<Album> {
|
||||
const MAX_SIZE: u32 = 50;
|
||||
let spotify = self.clone();
|
||||
let artist_id = artist_id.to_string();
|
||||
let fetch_page = move |offset: u32| {
|
||||
debug!("fetching artist {} albums, offset: {}", artist_id, offset);
|
||||
spotify.api_with_retry(|api| {
|
||||
match api.artist_albums(
|
||||
&artist_id,
|
||||
album_type,
|
||||
spotify.country,
|
||||
Some(MAX_SIZE),
|
||||
Some(offset),
|
||||
) {
|
||||
Ok(page) => {
|
||||
let mut albums: Vec<Album> =
|
||||
page.items.iter().map(|sa| sa.into()).collect();
|
||||
albums.sort_by(|a, b| b.year.cmp(&a.year));
|
||||
Ok(ApiPage {
|
||||
offset: page.offset,
|
||||
total: page.total,
|
||||
items: albums,
|
||||
})
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
ApiResult::new(MAX_SIZE, Arc::new(fetch_page))
|
||||
}
|
||||
|
||||
pub fn show_episodes(&self, show_id: &str) -> ApiResult<Episode> {
|
||||
const MAX_SIZE: u32 = 50;
|
||||
let spotify = self.clone();
|
||||
let show_id = show_id.to_string();
|
||||
let fetch_page = move |offset: u32| {
|
||||
debug!("fetching show {} episodes, offset: {}", &show_id, offset);
|
||||
spotify.api_with_retry(|api| {
|
||||
match api.get_shows_episodes(show_id.clone(), MAX_SIZE, offset, spotify.country) {
|
||||
Ok(page) => Ok(ApiPage {
|
||||
offset: page.offset,
|
||||
total: page.total,
|
||||
items: page.items.iter().map(|se| se.into()).collect(),
|
||||
}),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
ApiResult::new(MAX_SIZE, Arc::new(fetch_page))
|
||||
}
|
||||
|
||||
pub fn get_saved_shows(&self, offset: u32) -> Option<Page<Show>> {
|
||||
self.api_with_retry(|api| api.get_saved_show(50, offset))
|
||||
}
|
||||
|
||||
pub fn save_shows(&self, ids: Vec<String>) -> bool {
|
||||
self.api_with_retry(|api| api.save_shows(ids.clone()))
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn unsave_shows(&self, ids: Vec<String>) -> bool {
|
||||
self.api_with_retry(|api| api.remove_users_saved_shows(ids.clone(), self.country))
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn current_user_followed_artists(
|
||||
&self,
|
||||
last: Option<String>,
|
||||
) -> Option<CursorBasedPage<FullArtist>> {
|
||||
self.api_with_retry(|api| api.current_user_followed_artists(50, last.clone()))
|
||||
.map(|cp| cp.artists)
|
||||
}
|
||||
|
||||
pub fn user_follow_artists(&self, ids: Vec<String>) -> Option<()> {
|
||||
self.api_with_retry(|api| api.user_follow_artists(&ids))
|
||||
}
|
||||
|
||||
pub fn user_unfollow_artists(&self, ids: Vec<String>) -> Option<()> {
|
||||
self.api_with_retry(|api| api.user_unfollow_artists(&ids))
|
||||
}
|
||||
|
||||
pub fn current_user_saved_albums(&self, offset: u32) -> Option<Page<SavedAlbum>> {
|
||||
self.api_with_retry(|api| api.current_user_saved_albums(50, offset))
|
||||
}
|
||||
|
||||
pub fn current_user_saved_albums_add(&self, ids: Vec<String>) -> Option<()> {
|
||||
self.api_with_retry(|api| api.current_user_saved_albums_add(&ids))
|
||||
}
|
||||
|
||||
pub fn current_user_saved_albums_delete(&self, ids: Vec<String>) -> Option<()> {
|
||||
self.api_with_retry(|api| api.current_user_saved_albums_delete(&ids))
|
||||
}
|
||||
|
||||
pub fn current_user_saved_tracks(&self, offset: u32) -> Option<Page<SavedTrack>> {
|
||||
self.api_with_retry(|api| api.current_user_saved_tracks(50, offset))
|
||||
}
|
||||
|
||||
pub fn current_user_saved_tracks_add(&self, ids: Vec<String>) -> Option<()> {
|
||||
self.api_with_retry(|api| api.current_user_saved_tracks_add(&ids))
|
||||
}
|
||||
|
||||
pub fn current_user_saved_tracks_delete(&self, ids: Vec<String>) -> Option<()> {
|
||||
self.api_with_retry(|api| api.current_user_saved_tracks_delete(&ids))
|
||||
}
|
||||
|
||||
pub fn user_playlist_follow_playlist(&self, owner_id: String, id: String) -> Option<()> {
|
||||
self.api_with_retry(|api| api.user_playlist_follow_playlist(&owner_id, &id, true))
|
||||
}
|
||||
|
||||
pub fn artist_top_tracks(&self, id: &str) -> Option<Vec<Track>> {
|
||||
self.api_with_retry(|api| api.artist_top_tracks(id, self.country))
|
||||
.map(|ft| ft.tracks.iter().map(|t| t.into()).collect())
|
||||
}
|
||||
|
||||
pub fn artist_related_artists(&self, id: String) -> Option<Vec<Artist>> {
|
||||
self.api_with_retry(|api| api.artist_related_artists(&id))
|
||||
.map(|fa| fa.artists.iter().map(|a| a.into()).collect())
|
||||
}
|
||||
|
||||
pub fn current_user(&self) -> Option<PrivateUser> {
|
||||
self.api_with_retry(|api| api.current_user())
|
||||
}
|
||||
}
|
||||
@@ -251,6 +251,7 @@ impl ListItem for Track {
|
||||
|
||||
let recommendations: Option<Vec<Track>> = if let Some(id) = &self.id {
|
||||
spotify
|
||||
.api
|
||||
.recommendations(None, None, Some(vec![id.clone()]))
|
||||
.map(|r| r.tracks)
|
||||
.map(|tracks| tracks.iter().map(Track::from).collect())
|
||||
@@ -283,7 +284,7 @@ impl ListItem for Track {
|
||||
let spotify = queue.get_spotify();
|
||||
|
||||
match self.album_id {
|
||||
Some(ref album_id) => spotify.album(album_id).map(|ref fa| fa.into()),
|
||||
Some(ref album_id) => spotify.api.album(album_id).map(|ref fa| fa.into()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ impl ArtistView {
|
||||
let library = library.clone();
|
||||
thread::spawn(move || {
|
||||
if let Some(id) = id {
|
||||
if let Some(tracks) = spotify.artist_top_tracks(&id) {
|
||||
if let Some(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.artist_related_artists(id) {
|
||||
if let Some(artists) = spotify.api.artist_related_artists(id) {
|
||||
related.write().unwrap().extend(artists);
|
||||
library.trigger_redraw();
|
||||
}
|
||||
@@ -106,7 +106,7 @@ impl ArtistView {
|
||||
) -> ListView<Album> {
|
||||
if let Some(artist_id) = &artist.id {
|
||||
let spotify = queue.get_spotify();
|
||||
let albums_page = spotify.artist_albums(artist_id, Some(album_type));
|
||||
let albums_page = spotify.api.artist_albums(artist_id, Some(album_type));
|
||||
let view = ListView::new(albums_page.items.clone(), queue, library);
|
||||
albums_page.apply_pagination(view.get_pagination());
|
||||
|
||||
|
||||
@@ -534,21 +534,27 @@ impl<I: ListItem + Clone> ViewExt for ListView<I> {
|
||||
if let Some(url) = url {
|
||||
let target: Option<Box<dyn ListItem>> = match url.uri_type {
|
||||
UriType::Track => spotify
|
||||
.api
|
||||
.track(&url.id)
|
||||
.map(|track| Track::from(&track).as_listitem()),
|
||||
UriType::Album => spotify
|
||||
.api
|
||||
.album(&url.id)
|
||||
.map(|album| Album::from(&album).as_listitem()),
|
||||
UriType::Playlist => spotify
|
||||
.api
|
||||
.playlist(&url.id)
|
||||
.map(|playlist| Playlist::from(&playlist).as_listitem()),
|
||||
UriType::Artist => spotify
|
||||
.api
|
||||
.artist(&url.id)
|
||||
.map(|artist| Artist::from(&artist).as_listitem()),
|
||||
UriType::Episode => spotify
|
||||
.api
|
||||
.episode(&url.id)
|
||||
.map(|episode| Episode::from(&episode).as_listitem()),
|
||||
UriType::Show => spotify
|
||||
.api
|
||||
.get_show(&url.id)
|
||||
.map(|show| Show::from(&show).as_listitem()),
|
||||
};
|
||||
|
||||
@@ -109,7 +109,7 @@ impl SearchResultsView {
|
||||
_offset: usize,
|
||||
_append: bool,
|
||||
) -> u32 {
|
||||
if let Some(results) = spotify.track(query) {
|
||||
if let Some(results) = spotify.api.track(query) {
|
||||
let t = vec![(&results).into()];
|
||||
let mut r = tracks.write().unwrap();
|
||||
*r = t;
|
||||
@@ -126,7 +126,9 @@ impl SearchResultsView {
|
||||
append: bool,
|
||||
) -> u32 {
|
||||
if let Some(SearchResult::Tracks(results)) =
|
||||
spotify.search(SearchType::Track, query, 50, offset as u32)
|
||||
spotify
|
||||
.api
|
||||
.search(SearchType::Track, query, 50, offset as u32)
|
||||
{
|
||||
let mut t = results.items.iter().map(|ft| ft.into()).collect();
|
||||
let mut r = tracks.write().unwrap();
|
||||
@@ -148,7 +150,7 @@ impl SearchResultsView {
|
||||
_offset: usize,
|
||||
_append: bool,
|
||||
) -> u32 {
|
||||
if let Some(results) = spotify.album(query) {
|
||||
if let Some(results) = spotify.api.album(query) {
|
||||
let a = vec![(&results).into()];
|
||||
let mut r = albums.write().unwrap();
|
||||
*r = a;
|
||||
@@ -165,7 +167,9 @@ impl SearchResultsView {
|
||||
append: bool,
|
||||
) -> u32 {
|
||||
if let Some(SearchResult::Albums(results)) =
|
||||
spotify.search(SearchType::Album, query, 50, offset as u32)
|
||||
spotify
|
||||
.api
|
||||
.search(SearchType::Album, query, 50, offset as u32)
|
||||
{
|
||||
let mut a = results.items.iter().map(|sa| sa.into()).collect();
|
||||
let mut r = albums.write().unwrap();
|
||||
@@ -187,7 +191,7 @@ impl SearchResultsView {
|
||||
_offset: usize,
|
||||
_append: bool,
|
||||
) -> u32 {
|
||||
if let Some(results) = spotify.artist(query) {
|
||||
if let Some(results) = spotify.api.artist(query) {
|
||||
let a = vec![(&results).into()];
|
||||
let mut r = artists.write().unwrap();
|
||||
*r = a;
|
||||
@@ -204,7 +208,9 @@ impl SearchResultsView {
|
||||
append: bool,
|
||||
) -> u32 {
|
||||
if let Some(SearchResult::Artists(results)) =
|
||||
spotify.search(SearchType::Artist, query, 50, offset as u32)
|
||||
spotify
|
||||
.api
|
||||
.search(SearchType::Artist, query, 50, offset as u32)
|
||||
{
|
||||
let mut a = results.items.iter().map(|fa| fa.into()).collect();
|
||||
let mut r = artists.write().unwrap();
|
||||
@@ -226,7 +232,7 @@ impl SearchResultsView {
|
||||
_offset: usize,
|
||||
_append: bool,
|
||||
) -> u32 {
|
||||
if let Some(result) = spotify.playlist(query).as_ref() {
|
||||
if let Some(result) = spotify.api.playlist(query).as_ref() {
|
||||
let pls = vec![result.into()];
|
||||
let mut r = playlists.write().unwrap();
|
||||
*r = pls;
|
||||
@@ -243,7 +249,9 @@ impl SearchResultsView {
|
||||
append: bool,
|
||||
) -> u32 {
|
||||
if let Some(SearchResult::Playlists(results)) =
|
||||
spotify.search(SearchType::Playlist, query, 50, offset as u32)
|
||||
spotify
|
||||
.api
|
||||
.search(SearchType::Playlist, query, 50, offset as u32)
|
||||
{
|
||||
let mut pls = results.items.iter().map(|sp| sp.into()).collect();
|
||||
let mut r = playlists.write().unwrap();
|
||||
@@ -265,7 +273,7 @@ impl SearchResultsView {
|
||||
_offset: usize,
|
||||
_append: bool,
|
||||
) -> u32 {
|
||||
if let Some(result) = spotify.get_show(query).as_ref() {
|
||||
if let Some(result) = spotify.api.get_show(query).as_ref() {
|
||||
let pls = vec![result.into()];
|
||||
let mut r = shows.write().unwrap();
|
||||
*r = pls;
|
||||
@@ -282,7 +290,9 @@ impl SearchResultsView {
|
||||
append: bool,
|
||||
) -> u32 {
|
||||
if let Some(SearchResult::Shows(results)) =
|
||||
spotify.search(SearchType::Show, query, 50, offset as u32)
|
||||
spotify
|
||||
.api
|
||||
.search(SearchType::Show, query, 50, offset as u32)
|
||||
{
|
||||
let mut pls = results.items.iter().map(|sp| sp.into()).collect();
|
||||
let mut r = shows.write().unwrap();
|
||||
@@ -304,7 +314,7 @@ impl SearchResultsView {
|
||||
_offset: usize,
|
||||
_append: bool,
|
||||
) -> u32 {
|
||||
if let Some(result) = spotify.episode(query).as_ref() {
|
||||
if let Some(result) = spotify.api.episode(query).as_ref() {
|
||||
let e = vec![result.into()];
|
||||
let mut r = episodes.write().unwrap();
|
||||
*r = e;
|
||||
@@ -321,7 +331,9 @@ impl SearchResultsView {
|
||||
append: bool,
|
||||
) -> u32 {
|
||||
if let Some(SearchResult::Episodes(results)) =
|
||||
spotify.search(SearchType::Episode, query, 50, offset as u32)
|
||||
spotify
|
||||
.api
|
||||
.search(SearchType::Episode, query, 50, offset as u32)
|
||||
{
|
||||
let mut e = results.items.iter().map(|se| se.into()).collect();
|
||||
let mut r = episodes.write().unwrap();
|
||||
@@ -378,7 +390,7 @@ impl SearchResultsView {
|
||||
// check if API token refresh is necessary before commencing multiple
|
||||
// requests to avoid deadlock, as the parallel requests might
|
||||
// simultaneously try to refresh the token
|
||||
self.spotify.refresh_token();
|
||||
self.spotify.api.update_token();
|
||||
|
||||
// is the query a Spotify URI?
|
||||
if let Some(uritype) = UriType::from_uri(&query) {
|
||||
|
||||
@@ -23,7 +23,7 @@ impl ShowView {
|
||||
let show = show.clone();
|
||||
|
||||
let list = {
|
||||
let results = spotify.show_episodes(&show.id);
|
||||
let results = spotify.api.show_episodes(&show.id);
|
||||
let view = ListView::new(results.items.clone(), queue, library);
|
||||
results.apply_pagination(view.get_pagination());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user