Update to rspotify 0.11.2 (#640)

* Update to rspotify 0.11.x

Many breaking changes

* Minor cleanups via Clippy
This commit is contained in:
Henrik Friedrichsen
2021-11-07 17:19:56 +01:00
committed by GitHub
parent a8c8a1761a
commit 96f2d88696
20 changed files with 1025 additions and 1187 deletions

1485
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -22,14 +22,12 @@ platform-dirs = "0.3.0"
failure = "0.1" failure = "0.1"
fern = "0.6" fern = "0.6"
futures = { version = "0.3", features = ["compat"] } futures = { version = "0.3", features = ["compat"] }
futures_01 = { version = "0.1", package = "futures" }
lazy_static = "1.3.0" lazy_static = "1.3.0"
librespot-core = "0.3.1" librespot-core = "0.3.1"
librespot-playback = "0.3.1" librespot-playback = "0.3.1"
librespot-protocol = "0.3.1" librespot-protocol = "0.3.1"
log = "0.4.13" log = "0.4.13"
notify-rust = { version = "4", optional = true } notify-rust = { version = "4", optional = true }
rspotify = { version = "0.10.0", features = ["blocking"] }
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] } tokio = { version = "1", features = ["rt-multi-thread", "sync", "time"] }
@@ -49,6 +47,11 @@ ioctl-rs = { version = "0.2", optional = true }
serde_cbor = "0.11.2" serde_cbor = "0.11.2"
pancurses = { version = "0.17.0", features = ["win32"] } pancurses = { version = "0.17.0", features = ["win32"] }
[dependencies.rspotify]
version = "0.11.2"
default-features = false
features = ["client-ureq", "ureq-rustls-tls"]
[dependencies.cursive] [dependencies.cursive]
version = "0.16.3" version = "0.16.3"
default-features = false default-features = false

View File

@@ -1,4 +1,5 @@
use rand::{seq::IteratorRandom, thread_rng}; use rand::{seq::IteratorRandom, thread_rng};
use rspotify::model::Id;
use std::fmt; use std::fmt;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
@@ -68,10 +69,14 @@ impl Album {
impl From<&SimplifiedAlbum> for Album { impl From<&SimplifiedAlbum> for Album {
fn from(sa: &SimplifiedAlbum) -> Self { fn from(sa: &SimplifiedAlbum) -> Self {
Self { Self {
id: sa.id.clone(), id: sa.id.as_ref().map(|id| id.id().to_string()),
title: sa.name.clone(), title: sa.name.clone(),
artists: sa.artists.iter().map(|sa| sa.name.clone()).collect(), artists: sa.artists.iter().map(|sa| sa.name.clone()).collect(),
artist_ids: sa.artists.iter().filter_map(|a| a.id.clone()).collect(), artist_ids: sa
.artists
.iter()
.filter_map(|a| a.id.as_ref().map(|id| id.id().to_string()))
.collect(),
year: sa year: sa
.release_date .release_date
.clone() .clone()
@@ -81,7 +86,7 @@ impl From<&SimplifiedAlbum> for Album {
.unwrap() .unwrap()
.into(), .into(),
cover_url: sa.images.get(0).map(|i| i.url.clone()), cover_url: sa.images.get(0).map(|i| i.url.clone()),
url: sa.uri.clone(), url: sa.id.as_ref().map(|id| id.url()),
tracks: None, tracks: None,
added_at: None, added_at: None,
} }
@@ -99,13 +104,17 @@ impl From<&FullAlbum> for Album {
); );
Self { Self {
id: Some(fa.id.clone()), id: Some(fa.id.id().to_string()),
title: fa.name.clone(), title: fa.name.clone(),
artists: fa.artists.iter().map(|sa| sa.name.clone()).collect(), artists: fa.artists.iter().map(|sa| sa.name.clone()).collect(),
artist_ids: fa.artists.iter().filter_map(|a| a.id.clone()).collect(), artist_ids: fa
.artists
.iter()
.filter_map(|a| a.id.as_ref().map(|id| id.id().to_string()))
.collect(),
year: fa.release_date.split('-').next().unwrap().into(), year: fa.release_date.split('-').next().unwrap().into(),
cover_url: fa.images.get(0).map(|i| i.url.clone()), cover_url: fa.images.get(0).map(|i| i.url.clone()),
url: Some(fa.uri.clone()), url: Some(fa.id.uri()),
tracks, tracks,
added_at: None, added_at: None,
} }
@@ -185,7 +194,7 @@ impl ListItem for Album {
.iter() .iter()
.map(|track| Playable::Track(track.clone())) .map(|track| Playable::Track(track.clone()))
.collect(); .collect();
let index = queue.append_next(tracks); let index = queue.append_next(&tracks);
queue.play(index, true, true); queue.play(index, true, true);
} }
} }
@@ -237,11 +246,11 @@ impl ListItem for Album {
) -> Option<Box<dyn ViewExt>> { ) -> Option<Box<dyn ViewExt>> {
self.load_all_tracks(queue.get_spotify()); self.load_all_tracks(queue.get_spotify());
const MAX_SEEDS: usize = 5; const MAX_SEEDS: usize = 5;
let track_ids: Vec<String> = self let track_ids: Vec<&str> = self
.tracks .tracks
.as_ref()? .as_ref()?
.iter() .iter()
.map(|t| t.id.clone()) .map(|t| t.id.as_deref())
.flatten() .flatten()
// spotify allows at max 5 seed items, so choose 4 random tracks... // spotify allows at max 5 seed items, so choose 4 random tracks...
.choose_multiple(&mut thread_rng(), MAX_SEEDS - 1); .choose_multiple(&mut thread_rng(), MAX_SEEDS - 1);
@@ -249,7 +258,7 @@ impl ListItem for Album {
let artist_id: Option<String> = self let artist_id: Option<String> = self
.artist_ids .artist_ids
.iter() .iter()
.map(|aid| aid.clone()) .cloned()
// ...and one artist // ...and one artist
.choose(&mut thread_rng()); .choose(&mut thread_rng());
@@ -260,7 +269,11 @@ impl ListItem for Album {
let spotify = queue.get_spotify(); let spotify = queue.get_spotify();
let recommendations: Option<Vec<Track>> = spotify let recommendations: Option<Vec<Track>> = spotify
.api .api
.recommendations(artist_id.map(|aid| vec![aid]), None, Some(track_ids)) .recommendations(
artist_id.as_ref().map(|aid| vec![aid.as_str()]),
None,
Some(track_ids),
)
.map(|r| r.tracks) .map(|r| r.tracks)
.map(|tracks| tracks.iter().map(Track::from).collect()); .map(|tracks| tracks.iter().map(Track::from).collect());
recommendations.map(|tracks| { recommendations.map(|tracks| {

View File

@@ -2,6 +2,7 @@ use std::fmt;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use rspotify::model::artist::{FullArtist, SimplifiedArtist}; use rspotify::model::artist::{FullArtist, SimplifiedArtist};
use rspotify::model::Id;
use crate::library::Library; use crate::library::Library;
use crate::playable::Playable; use crate::playable::Playable;
@@ -43,9 +44,9 @@ impl Artist {
impl From<&SimplifiedArtist> for Artist { impl From<&SimplifiedArtist> for Artist {
fn from(sa: &SimplifiedArtist) -> Self { fn from(sa: &SimplifiedArtist) -> Self {
Self { Self {
id: sa.id.clone(), id: sa.id.as_ref().map(|id| id.id().to_string()),
name: sa.name.clone(), name: sa.name.clone(),
url: sa.uri.clone(), url: sa.id.as_ref().map(|id| id.url()),
tracks: None, tracks: None,
is_followed: false, is_followed: false,
} }
@@ -55,9 +56,9 @@ impl From<&SimplifiedArtist> for Artist {
impl From<&FullArtist> for Artist { impl From<&FullArtist> for Artist {
fn from(fa: &FullArtist) -> Self { fn from(fa: &FullArtist) -> Self {
Self { Self {
id: Some(fa.id.clone()), id: Some(fa.id.id().to_string()),
name: fa.name.clone(), name: fa.name.clone(),
url: Some(fa.uri.clone()), url: Some(fa.id.url()),
tracks: None, tracks: None,
is_followed: false, is_followed: false,
} }
@@ -129,7 +130,7 @@ impl ListItem for Artist {
.iter() .iter()
.map(|track| Playable::Track(track.clone())) .map(|track| Playable::Track(track.clone()))
.collect(); .collect();
let index = queue.append_next(tracks); let index = queue.append_next(&tracks);
queue.play(index, true, true); queue.play(index, true, true);
} }
} }
@@ -184,7 +185,7 @@ impl ListItem for Artist {
let spotify = queue.get_spotify(); let spotify = queue.get_spotify();
let recommendations: Option<Vec<Track>> = spotify let recommendations: Option<Vec<Track>> = spotify
.api .api
.recommendations(Some(vec![id]), None, None) .recommendations(Some(vec![&id]), None, None)
.map(|r| r.tracks) .map(|r| r.tracks)
.map(|tracks| tracks.iter().map(Track::from).collect()); .map(|tracks| tracks.iter().map(Track::from).collect());

View File

@@ -2,7 +2,9 @@ use crate::library::Library;
use crate::playable::Playable; use crate::playable::Playable;
use crate::queue::Queue; use crate::queue::Queue;
use crate::traits::{ListItem, ViewExt}; use crate::traits::{ListItem, ViewExt};
use chrono::{DateTime, Utc};
use rspotify::model::show::{FullEpisode, SimplifiedEpisode}; use rspotify::model::show::{FullEpisode, SimplifiedEpisode};
use rspotify::model::Id;
use std::fmt; use std::fmt;
use std::sync::Arc; use std::sync::Arc;
@@ -15,6 +17,8 @@ pub struct Episode {
pub description: String, pub description: String,
pub release_date: String, pub release_date: String,
pub cover_url: Option<String>, pub cover_url: Option<String>,
pub added_at: Option<DateTime<Utc>>,
pub list_index: usize,
} }
impl Episode { impl Episode {
@@ -28,13 +32,15 @@ impl Episode {
impl From<&SimplifiedEpisode> for Episode { impl From<&SimplifiedEpisode> for Episode {
fn from(episode: &SimplifiedEpisode) -> Self { fn from(episode: &SimplifiedEpisode) -> Self {
Self { Self {
id: episode.id.clone(), id: episode.id.id().to_string(),
uri: episode.uri.clone(), uri: episode.id.uri(),
duration: episode.duration_ms, duration: episode.duration.as_millis() as u32,
name: episode.name.clone(), name: episode.name.clone(),
description: episode.description.clone(), description: episode.description.clone(),
release_date: episode.release_date.clone(), release_date: episode.release_date.clone(),
cover_url: episode.images.get(0).map(|img| img.url.clone()), cover_url: episode.images.get(0).map(|img| img.url.clone()),
added_at: None,
list_index: 0,
} }
} }
} }
@@ -42,13 +48,15 @@ impl From<&SimplifiedEpisode> for Episode {
impl From<&FullEpisode> for Episode { impl From<&FullEpisode> for Episode {
fn from(episode: &FullEpisode) -> Self { fn from(episode: &FullEpisode) -> Self {
Self { Self {
id: episode.id.clone(), id: episode.id.id().to_string(),
uri: episode.uri.clone(), uri: episode.id.uri(),
duration: episode.duration_ms, duration: episode.duration.as_millis() as u32,
name: episode.name.clone(), name: episode.name.clone(),
description: episode.description.clone(), description: episode.description.clone(),
release_date: episode.release_date.clone(), release_date: episode.release_date.clone(),
cover_url: episode.images.get(0).map(|img| img.url.clone()), cover_url: episode.images.get(0).map(|img| img.url.clone()),
added_at: None,
list_index: 0,
} }
} }
} }
@@ -76,7 +84,7 @@ impl ListItem for Episode {
} }
fn play(&mut self, queue: Arc<Queue>) { fn play(&mut self, queue: Arc<Queue>) {
let index = queue.append_next(vec![Playable::Episode(self.clone())]); let index = queue.append_next(&vec![Playable::Episode(self.clone())]);
queue.play(index, true, false); queue.play(index, true, false);
} }

View File

@@ -6,6 +6,7 @@ use std::sync::{Arc, RwLock, RwLockReadGuard};
use std::thread; use std::thread;
use log::{debug, error, info}; use log::{debug, error, info};
use rspotify::model::Id;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
@@ -43,7 +44,7 @@ pub struct Library {
impl Library { impl Library {
pub fn new(ev: &EventManager, spotify: Spotify, cfg: Arc<Config>) -> Self { pub fn new(ev: &EventManager, spotify: Spotify, cfg: Arc<Config>) -> Self {
let current_user = spotify.api.current_user(); let current_user = spotify.api.current_user();
let user_id = current_user.as_ref().map(|u| u.id.clone()); let user_id = current_user.as_ref().map(|u| u.id.id().to_string());
let display_name = current_user.as_ref().and_then(|u| u.display_name.clone()); let display_name = current_user.as_ref().and_then(|u| u.display_name.clone());
let library = Self { let library = Self {
@@ -307,7 +308,7 @@ impl Library {
fn fetch_artists(&self) { fn fetch_artists(&self) {
let mut artists: Vec<Artist> = Vec::new(); let mut artists: Vec<Artist> = Vec::new();
let mut last: Option<String> = None; let mut last: Option<&str> = None;
let mut i: u32 = 0; let mut i: u32 = 0;
@@ -324,7 +325,7 @@ impl Library {
artists.extend(page.items.iter().map(|fa| fa.into())); artists.extend(page.items.iter().map(|fa| fa.into()));
if page.next.is_some() { if page.next.is_some() {
last = artists.last().unwrap().id.clone(); last = artists.last().unwrap().id.as_deref();
} else { } else {
break; break;
} }
@@ -387,7 +388,7 @@ impl Library {
.items .items
.iter() .iter()
.enumerate() .enumerate()
.any(|(i, a)| a.album.id != store[i].id.clone().unwrap_or_default()) .any(|(i, a)| a.album.id.id() != store[i].id.clone().unwrap_or_default())
{ {
return; return;
} }
@@ -438,7 +439,7 @@ impl Library {
.items .items
.iter() .iter()
.enumerate() .enumerate()
.any(|(i, t)| t.track.id != store[i].id) .any(|(i, t)| Some(t.track.id.id().to_string()) != store[i].id)
{ {
return; return;
} }
@@ -547,7 +548,9 @@ impl Library {
&& self && self
.spotify .spotify
.api .api
.current_user_saved_tracks_add(tracks.iter().filter_map(|t| t.id.clone()).collect()) .current_user_saved_tracks_add(
tracks.iter().filter_map(|t| t.id.as_deref()).collect(),
)
.is_none() .is_none()
{ {
return; return;
@@ -582,7 +585,7 @@ impl Library {
.spotify .spotify
.api .api
.current_user_saved_tracks_delete( .current_user_saved_tracks_delete(
tracks.iter().filter_map(|t| t.id.clone()).collect(), tracks.iter().filter_map(|t| t.id.as_deref()).collect(),
) )
.is_none() .is_none()
{ {
@@ -622,7 +625,7 @@ impl Library {
if self if self
.spotify .spotify
.api .api
.current_user_saved_albums_add(vec![album_id.clone()]) .current_user_saved_albums_add(vec![album_id.as_str()])
.is_none() .is_none()
{ {
return; return;
@@ -651,7 +654,7 @@ impl Library {
if self if self
.spotify .spotify
.api .api
.current_user_saved_albums_delete(vec![album_id.clone()]) .current_user_saved_albums_delete(vec![album_id.as_str()])
.is_none() .is_none()
{ {
return; return;
@@ -684,7 +687,7 @@ impl Library {
if self if self
.spotify .spotify
.api .api
.user_follow_artists(vec![artist_id.clone()]) .user_follow_artists(vec![artist_id.as_str()])
.is_none() .is_none()
{ {
return; return;
@@ -716,7 +719,7 @@ impl Library {
if self if self
.spotify .spotify
.api .api
.user_unfollow_artists(vec![artist_id.clone()]) .user_unfollow_artists(vec![artist_id.as_str()])
.is_none() .is_none()
{ {
return; return;
@@ -759,7 +762,7 @@ impl Library {
if self if self
.spotify .spotify
.api .api
.user_playlist_follow_playlist(playlist.owner_id.clone(), playlist.id.clone()) .user_playlist_follow_playlist(playlist.id.as_str())
.is_none() .is_none()
{ {
return; return;
@@ -792,7 +795,7 @@ impl Library {
return; return;
} }
if self.spotify.api.save_shows(vec![show.id.clone()]) { if self.spotify.api.save_shows(vec![show.id.as_str()]) {
{ {
let mut store = self.shows.write().unwrap(); let mut store = self.shows.write().unwrap();
if !store.iter().any(|s| s.id == show.id) { if !store.iter().any(|s| s.id == show.id) {
@@ -807,7 +810,7 @@ impl Library {
return; return;
} }
if self.spotify.api.unsave_shows(vec![show.id.clone()]) { if self.spotify.api.unsave_shows(vec![show.id.as_str()]) {
{ {
let mut store = self.shows.write().unwrap(); let mut store = self.shows.write().unwrap();
*store = store.iter().filter(|s| s.id != show.id).cloned().collect(); *store = store.iter().filter(|s| s.id != show.id).cloned().collect();

View File

@@ -584,7 +584,7 @@ fn run_dbus_server(
if let Some(t) = &Album::from(&a).tracks { if let Some(t) = &Album::from(&a).tracks {
queue.clear(); queue.clear();
let index = queue.append_next( let index = queue.append_next(
t.iter() &t.iter()
.map(|track| Playable::Track(track.clone())) .map(|track| Playable::Track(track.clone()))
.collect(), .collect(),
); );
@@ -606,11 +606,7 @@ fn run_dbus_server(
playlist.load_tracks(spotify); playlist.load_tracks(spotify);
if let Some(t) = &playlist.tracks { if let Some(t) = &playlist.tracks {
queue.clear(); queue.clear();
let index = queue.append_next( let index = queue.append_next(&t.iter().cloned().collect());
t.iter()
.map(|track| Playable::Track(track.clone()))
.collect(),
);
queue.play(index, false, false) queue.play(index, false, false)
} }
} }
@@ -625,7 +621,7 @@ fn run_dbus_server(
let mut ep = e.clone(); let mut ep = e.clone();
ep.reverse(); ep.reverse();
let index = queue.append_next( let index = queue.append_next(
ep.iter() &ep.iter()
.map(|episode| Playable::Episode(episode.clone())) .map(|episode| Playable::Episode(episode.clone()))
.collect(), .collect(),
); );
@@ -643,7 +639,7 @@ fn run_dbus_server(
Some(UriType::Artist) => { Some(UriType::Artist) => {
if let Some(a) = spotify.api.artist_top_tracks(id) { if let Some(a) = spotify.api.artist_top_tracks(id) {
queue.clear(); queue.clear();
queue.append_next(a.iter().map(|track| Playable::Track(track.clone())).collect()); queue.append_next(&a.iter().map(|track| Playable::Track(track.clone())).collect());
queue.play(0, false, false) queue.play(0, false, false)
} }
} }

View File

@@ -1,3 +1,6 @@
use chrono::{DateTime, Utc};
use rspotify::model::PlayableItem;
use crate::album::Album; use crate::album::Album;
use crate::artist::Artist; use crate::artist::Artist;
use crate::episode::Episode; use crate::episode::Episode;
@@ -44,6 +47,34 @@ impl Playable {
} }
} }
pub fn list_index(&self) -> usize {
match self {
Playable::Track(track) => track.list_index,
Playable::Episode(episode) => episode.list_index,
}
}
pub fn set_list_index(&mut self, index: usize) {
match self {
Playable::Track(track) => track.list_index = index,
Playable::Episode(episode) => episode.list_index = index,
}
}
pub fn added_at(&self) -> Option<DateTime<Utc>> {
match self {
Playable::Track(track) => track.added_at,
Playable::Episode(episode) => episode.added_at,
}
}
pub fn set_added_at(&mut self, added_at: Option<DateTime<Utc>>) {
match self {
Playable::Track(track) => track.added_at = added_at,
Playable::Episode(episode) => episode.added_at = added_at,
}
}
pub fn duration_str(&self) -> String { pub fn duration_str(&self) -> String {
let duration = self.duration(); let duration = self.duration();
let minutes = duration / 60_000; let minutes = duration / 60_000;
@@ -59,6 +90,15 @@ impl Playable {
} }
} }
impl From<&PlayableItem> for Playable {
fn from(item: &PlayableItem) -> Self {
match item {
PlayableItem::Episode(episode) => Playable::Episode(episode.into()),
PlayableItem::Track(track) => Playable::Track(track.into()),
}
}
}
impl fmt::Display for Playable { impl fmt::Display for Playable {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self { match self {

View File

@@ -6,6 +6,7 @@ use rand::{seq::IteratorRandom, thread_rng};
use log::debug; use log::debug;
use rspotify::model::playlist::{FullPlaylist, SimplifiedPlaylist}; use rspotify::model::playlist::{FullPlaylist, SimplifiedPlaylist};
use rspotify::model::Id;
use crate::playable::Playable; use crate::playable::Playable;
use crate::queue::Queue; use crate::queue::Queue;
@@ -22,7 +23,7 @@ pub struct Playlist {
pub owner_id: String, pub owner_id: String,
pub snapshot_id: String, pub snapshot_id: String,
pub num_tracks: usize, pub num_tracks: usize,
pub tracks: Option<Vec<Track>>, pub tracks: Option<Vec<Playable>>,
pub collaborative: bool, pub collaborative: bool,
} }
@@ -35,7 +36,7 @@ impl Playlist {
self.tracks = Some(self.get_all_tracks(spotify)); self.tracks = Some(self.get_all_tracks(spotify));
} }
fn get_all_tracks(&self, spotify: Spotify) -> Vec<Track> { fn get_all_tracks(&self, spotify: Spotify) -> Vec<Playable> {
let tracks_result = spotify.api.user_playlist_tracks(&self.id); let tracks_result = spotify.api.user_playlist_tracks(&self.id);
while !tracks_result.at_end() { while !tracks_result.at_end() {
tracks_result.next(); tracks_result.next();
@@ -49,7 +50,7 @@ impl Playlist {
self.tracks.as_ref().map_or(false, |tracks| { self.tracks.as_ref().map_or(false, |tracks| {
tracks tracks
.iter() .iter()
.any(|track| track.id == Some(track_id.to_string())) .any(|track| track.id() == Some(track_id.to_string()))
}) })
} }
@@ -58,7 +59,7 @@ impl Playlist {
debug!("deleting track: {} {:?}", index, track); debug!("deleting track: {} {:?}", index, track);
match spotify match spotify
.api .api
.delete_tracks(&self.id, &self.snapshot_id, &[(&track, track.list_index)]) .delete_tracks(&self.id, &self.snapshot_id, &[track])
{ {
false => false, false => false,
true => { true => {
@@ -72,16 +73,15 @@ impl Playlist {
} }
} }
pub fn append_tracks(&mut self, new_tracks: &[Track], spotify: Spotify, library: Arc<Library>) { pub fn append_tracks(
let track_ids: Vec<String> = new_tracks &mut self,
.to_vec() new_tracks: &[Playable],
.iter() spotify: Spotify,
.filter_map(|t| t.id.clone()) library: Arc<Library>,
.collect(); ) {
let mut has_modified = false; let mut has_modified = false;
if spotify.api.append_tracks(&self.id, &track_ids, None) { if spotify.api.append_tracks(&self.id, new_tracks, None) {
if let Some(tracks) = &mut self.tracks { if let Some(tracks) = &mut self.tracks {
tracks.append(&mut new_tracks.to_vec()); tracks.append(&mut new_tracks.to_vec());
has_modified = true; has_modified = true;
@@ -148,18 +148,12 @@ impl Playlist {
impl From<&SimplifiedPlaylist> for Playlist { impl From<&SimplifiedPlaylist> for Playlist {
fn from(list: &SimplifiedPlaylist) -> Self { fn from(list: &SimplifiedPlaylist) -> Self {
let num_tracks = if let Some(number) = list.tracks.get("total") {
number.as_u64().unwrap() as usize
} else {
0
};
Playlist { Playlist {
id: list.id.clone(), id: list.id.id().to_string(),
name: list.name.clone(), name: list.name.clone(),
owner_id: list.owner.id.clone(), owner_id: list.owner.id.id().to_string(),
snapshot_id: list.snapshot_id.clone(), snapshot_id: list.snapshot_id.clone(),
num_tracks, num_tracks: list.tracks.total as usize,
tracks: None, tracks: None,
collaborative: list.collaborative, collaborative: list.collaborative,
} }
@@ -169,9 +163,9 @@ impl From<&SimplifiedPlaylist> for Playlist {
impl From<&FullPlaylist> for Playlist { impl From<&FullPlaylist> for Playlist {
fn from(list: &FullPlaylist) -> Self { fn from(list: &FullPlaylist) -> Self {
Playlist { Playlist {
id: list.id.clone(), id: list.id.id().to_string(),
name: list.name.clone(), name: list.name.clone(),
owner_id: list.owner.id.clone(), owner_id: list.owner.id.id().to_string(),
snapshot_id: list.snapshot_id.clone(), snapshot_id: list.snapshot_id.clone(),
num_tracks: list.tracks.total as usize, num_tracks: list.tracks.total as usize,
tracks: None, tracks: None,
@@ -190,7 +184,7 @@ impl ListItem for Playlist {
.iter() .iter()
.filter_map(|t| t.id()) .filter_map(|t| t.id())
.collect(); .collect();
let ids: Vec<String> = tracks.iter().filter_map(|t| t.id.clone()).collect(); let ids: Vec<String> = tracks.iter().filter_map(|t| t.id()).collect();
!ids.is_empty() && playing == ids !ids.is_empty() && playing == ids
} else { } else {
false false
@@ -229,10 +223,6 @@ impl ListItem for Playlist {
self.load_tracks(queue.get_spotify()); self.load_tracks(queue.get_spotify());
if let Some(tracks) = &self.tracks { if let Some(tracks) = &self.tracks {
let tracks: Vec<Playable> = tracks
.iter()
.map(|track| Playable::Track(track.clone()))
.collect();
let index = queue.append_next(tracks); let index = queue.append_next(tracks);
queue.play(index, true, true); queue.play(index, true, true);
} }
@@ -243,7 +233,7 @@ impl ListItem for Playlist {
if let Some(tracks) = self.tracks.as_ref() { if let Some(tracks) = self.tracks.as_ref() {
for track in tracks.iter().rev() { for track in tracks.iter().rev() {
queue.insert_after_current(Playable::Track(track.clone())); queue.insert_after_current(track.clone());
} }
} }
} }
@@ -253,7 +243,7 @@ impl ListItem for Playlist {
if let Some(tracks) = self.tracks.as_ref() { if let Some(tracks) = self.tracks.as_ref() {
for track in tracks.iter() { for track in tracks.iter() {
queue.append(Playable::Track(track.clone())); queue.append(track.clone());
} }
} }
} }
@@ -294,7 +284,7 @@ impl ListItem for Playlist {
.tracks .tracks
.as_ref()? .as_ref()?
.iter() .iter()
.map(|t| t.id.clone()) .map(|t| t.id())
.flatten() .flatten()
// only select unique tracks // only select unique tracks
.collect::<HashSet<_>>() .collect::<HashSet<_>>()
@@ -309,7 +299,11 @@ impl ListItem for Playlist {
let spotify = queue.get_spotify(); let spotify = queue.get_spotify();
let recommendations: Option<Vec<Track>> = spotify let recommendations: Option<Vec<Track>> = spotify
.api .api
.recommendations(None, None, Some(track_ids)) .recommendations(
None,
None,
Some(track_ids.iter().map(|t| t.as_ref()).collect()),
)
.map(|r| r.tracks) .map(|r| r.tracks)
.map(|tracks| tracks.iter().map(Track::from).collect()); .map(|tracks| tracks.iter().map(Track::from).collect());

View File

@@ -146,7 +146,7 @@ impl Queue {
q.push(track); q.push(track);
} }
pub fn append_next(&self, tracks: Vec<Playable>) -> usize { pub fn append_next(&self, tracks: &Vec<Playable>) -> usize {
let mut q = self.queue.write().unwrap(); let mut q = self.queue.write().unwrap();
{ {

View File

@@ -6,6 +6,7 @@ use crate::spotify::Spotify;
use crate::traits::{IntoBoxedViewExt, ListItem, ViewExt}; use crate::traits::{IntoBoxedViewExt, ListItem, ViewExt};
use crate::ui::show::ShowView; use crate::ui::show::ShowView;
use rspotify::model::show::{FullShow, SimplifiedShow}; use rspotify::model::show::{FullShow, SimplifiedShow};
use rspotify::model::Id;
use std::fmt; use std::fmt;
use std::sync::Arc; use std::sync::Arc;
@@ -39,8 +40,8 @@ impl Show {
impl From<&SimplifiedShow> for Show { impl From<&SimplifiedShow> for Show {
fn from(show: &SimplifiedShow) -> Self { fn from(show: &SimplifiedShow) -> Self {
Self { Self {
id: show.id.clone(), id: show.id.id().to_string(),
uri: show.uri.clone(), uri: show.id.uri(),
name: show.name.clone(), name: show.name.clone(),
publisher: show.publisher.clone(), publisher: show.publisher.clone(),
description: show.description.clone(), description: show.description.clone(),
@@ -53,8 +54,8 @@ impl From<&SimplifiedShow> for Show {
impl From<&FullShow> for Show { impl From<&FullShow> for Show {
fn from(show: &FullShow) -> Self { fn from(show: &FullShow) -> Self {
Self { Self {
id: show.id.clone(), id: show.id.id().to_string(),
uri: show.uri.clone(), uri: show.id.uri(),
name: show.name.clone(), name: show.name.clone(),
publisher: show.publisher.clone(), publisher: show.publisher.clone(),
description: show.description.clone(), description: show.description.clone(),
@@ -103,7 +104,7 @@ impl ListItem for Show {
.map(|ep| Playable::Episode(ep.clone())) .map(|ep| Playable::Episode(ep.clone()))
.collect(); .collect();
let index = queue.append_next(playables); let index = queue.append_next(&playables);
queue.play(index, true, true); queue.play(index, true, true);
} }

View File

@@ -12,8 +12,6 @@ use librespot_playback::audio_backend;
use librespot_playback::config::Bitrate; use librespot_playback::config::Bitrate;
use librespot_playback::player::Player; use librespot_playback::player::Player;
use rspotify::senum::Country;
use futures::channel::oneshot; use futures::channel::oneshot;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@@ -82,14 +80,7 @@ impl Spotify {
spotify.api.set_worker_channel(spotify.channel.clone()); spotify.api.set_worker_channel(spotify.channel.clone());
spotify.api.update_token(); 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_user(spotify.user.clone());
spotify.api.set_country(country);
spotify spotify
} }

View File

@@ -7,22 +7,19 @@ use crate::spotify_worker::WorkerCommand;
use crate::track::Track; use crate::track::Track;
use crate::ui::pagination::{ApiPage, ApiResult}; use crate::ui::pagination::{ApiPage, ApiResult};
use chrono::{DateTime, Duration as ChronoDuration, Utc}; use chrono::{DateTime, Duration as ChronoDuration, Utc};
use failure::Error;
use futures::channel::oneshot; use futures::channel::oneshot;
use log::{debug, error, info}; use log::{debug, error, info};
use rspotify::blocking::client::ApiError;
use rspotify::blocking::client::Spotify as SpotifyAPI; use rspotify::http::HttpError;
use rspotify::model::album::{FullAlbum, SavedAlbum}; use rspotify::model::{
use rspotify::model::artist::FullArtist; AlbumId, AlbumType, ArtistId, CursorBasedPage, EpisodeId, FullAlbum, FullArtist, FullEpisode,
use rspotify::model::page::{CursorBasedPage, Page}; FullPlaylist, FullShow, FullTrack, ItemPositions, Market, Page, PlayableId, PlaylistId,
use rspotify::model::playlist::FullPlaylist; PrivateUser, Recommendations, SavedAlbum, SavedTrack, SearchResult, SearchType, Show, ShowId,
use rspotify::model::recommend::Recommendations; SimplifiedTrack, TrackId, UserId,
use rspotify::model::search::SearchResult; };
use rspotify::model::show::{FullEpisode, FullShow, Show}; use rspotify::{prelude::*, AuthCodeSpotify, ClientError, ClientResult, Token};
use rspotify::model::track::{FullTrack, SavedTrack, SimplifiedTrack}; use std::collections::HashSet;
use rspotify::model::user::PrivateUser; use std::iter::FromIterator;
use rspotify::senum::{AlbumType, Country, SearchType};
use serde_json::{json, Map};
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
@@ -30,9 +27,8 @@ use tokio::sync::mpsc;
#[derive(Clone)] #[derive(Clone)]
pub struct WebApi { pub struct WebApi {
api: Arc<RwLock<SpotifyAPI>>, api: AuthCodeSpotify,
user: Option<String>, user: Option<String>,
country: Option<Country>,
worker_channel: Arc<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>, worker_channel: Arc<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>,
token_expiration: Arc<RwLock<DateTime<Utc>>>, token_expiration: Arc<RwLock<DateTime<Utc>>>,
} }
@@ -40,9 +36,8 @@ pub struct WebApi {
impl WebApi { impl WebApi {
pub fn new() -> WebApi { pub fn new() -> WebApi {
WebApi { WebApi {
api: Arc::new(RwLock::new(SpotifyAPI::default())), api: AuthCodeSpotify::default(),
user: None, user: None,
country: None,
worker_channel: Arc::new(RwLock::new(None)), worker_channel: Arc::new(RwLock::new(None)),
token_expiration: Arc::new(RwLock::new(Utc::now())), token_expiration: Arc::new(RwLock::new(Utc::now())),
} }
@@ -52,10 +47,6 @@ impl WebApi {
self.user = user; self.user = user;
} }
pub fn set_country(&mut self, country: Option<Country>) {
self.country = country;
}
pub(crate) fn set_worker_channel( pub(crate) fn set_worker_channel(
&mut self, &mut self,
channel: Arc<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>, channel: Arc<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>,
@@ -87,8 +78,13 @@ impl WebApi {
{ {
channel.send(cmd).expect("can't send message to worker"); channel.send(cmd).expect("can't send message to worker");
let token = futures::executor::block_on(token_rx).unwrap(); let token = futures::executor::block_on(token_rx).unwrap();
self.api.write().expect("can't writelock api").access_token = *self.api.token.lock().expect("can't writelock api token") = Some(Token {
Some(token.access_token.to_string()); access_token: token.access_token,
expires_in: chrono::Duration::seconds(token.expires_in.into()),
scopes: HashSet::from_iter(token.scope),
expires_at: None,
refresh_token: None,
});
*self *self
.token_expiration .token_expiration
.write() .write()
@@ -102,32 +98,32 @@ impl WebApi {
/// retries once when rate limits are hit /// retries once when rate limits are hit
fn api_with_retry<F, R>(&self, cb: F) -> Option<R> fn api_with_retry<F, R>(&self, cb: F) -> Option<R>
where where
F: Fn(&SpotifyAPI) -> Result<R, Error>, F: Fn(&AuthCodeSpotify) -> ClientResult<R>,
{ {
let result = { let result = { cb(&self.api) };
let api = self.api.read().expect("can't read api");
cb(&api)
};
match result { match result {
Ok(v) => Some(v), Ok(v) => Some(v),
Err(e) => { Err(ClientError::Http(error)) => {
debug!("api error: {:?}", e); debug!("http error: {:?}", error);
if let Ok(apierror) = e.downcast::<ApiError>() { if let HttpError::StatusCode(response) = error.as_ref() {
match apierror { match response.status() {
ApiError::RateLimited(d) => { 429 => {
debug!("rate limit hit. waiting {:?} seconds", d); let waiting_duration = response
thread::sleep(Duration::from_secs(d.unwrap_or(0) as u64)); .header("Retry-After")
let api = self.api.read().expect("can't read api"); .and_then(|v| v.parse::<u64>().ok());
cb(&api).ok() debug!("rate limit hit. waiting {:?} seconds", waiting_duration);
thread::sleep(
Duration::from_secs(waiting_duration.unwrap_or(0) as u64),
);
cb(&self.api).ok()
} }
ApiError::Unauthorized => { 401 => {
debug!("token unauthorized. trying refresh.."); debug!("token unauthorized. trying refresh..");
self.update_token(); self.update_token();
let api = self.api.read().expect("can't read api"); cb(&self.api).ok()
cb(&api).ok()
} }
e => { _ => {
error!("unhandled api error: {}", e); error!("unhandled api error: {:?}", response);
None None
} }
} }
@@ -135,17 +131,34 @@ impl WebApi {
None None
} }
} }
Err(e) => {
error!("unhandled api error: {}", e);
None
}
} }
} }
pub fn append_tracks( pub fn append_tracks(
&self, &self,
playlist_id: &str, playlist_id: &str,
tracks: &[String], tracks: &[Playable],
position: Option<i32>, position: Option<i32>,
) -> bool { ) -> bool {
self.api_with_retry(|api| { self.api_with_retry(|api| {
api.user_playlist_add_tracks(self.user.as_ref().unwrap(), playlist_id, tracks, position) let trackids: Vec<Box<dyn PlayableId>> = tracks
.iter()
.map(|playable| {
Box::new(
TrackId::from_id(playable.id().as_ref().unwrap_or(&"".to_string()))
.unwrap(),
) as Box<dyn PlayableId>
})
.collect();
api.playlist_add_items(
&PlaylistId::from_id(playlist_id).unwrap(),
trackids.iter().map(|id| id.as_ref()),
position,
)
}) })
.is_some() .is_some()
} }
@@ -154,31 +167,45 @@ impl WebApi {
&self, &self,
playlist_id: &str, playlist_id: &str,
snapshot_id: &str, snapshot_id: &str,
track_pos_pairs: &[(&Track, usize)], playables: &[Playable],
) -> bool { ) -> bool {
let mut tracks = Vec::new(); self.api_with_retry(move |api| {
for (track, pos) in track_pos_pairs { let playable_ids: Vec<Box<dyn PlayableId>> = playables
let track_occurrence = json!({ .iter()
"uri": format!("spotify:track:{}", track.id.clone().unwrap()), .map(|playable| match playable {
"positions": [pos] Playable::Track(track) => {
}); Box::new(TrackId::from_id(&track.id.clone().unwrap_or_default()).unwrap())
let track_occurrence_object = track_occurrence.as_object(); as Box<dyn PlayableId>
tracks.push(track_occurrence_object.unwrap().clone()); }
} Playable::Episode(episode) => {
self.api_with_retry(|api| { Box::new(EpisodeId::from_id(&episode.id).unwrap()) as Box<dyn PlayableId>
api.user_playlist_remove_specific_occurrenes_of_tracks( }
self.user.as_ref().unwrap(), })
playlist_id, .collect();
tracks.clone(), let positions = playables
Some(snapshot_id.to_string()), .iter()
.map(|playable| [playable.list_index() as u32])
.collect::<Vec<_>>();
let item_pos: Vec<ItemPositions> = playable_ids
.iter()
.zip(positions.iter())
.map(|(id, positions)| ItemPositions {
id: id.as_ref(),
positions,
})
.collect();
api.playlist_remove_specific_occurrences_of_items(
&PlaylistId::from_id(playlist_id).unwrap(),
item_pos,
Some(snapshot_id),
) )
}) })
.is_some() .is_some()
} }
pub fn overwrite_playlist(&self, id: &str, tracks: &[Playable]) { pub fn overwrite_playlist(&self, id: &str, tracks: &[Playable]) {
// extract only track IDs // create mutable copy for chunking
let mut tracks: Vec<String> = tracks.iter().filter_map(|track| track.id()).collect(); let mut tracks: Vec<Playable> = tracks.to_vec();
// we can only send 100 tracks per request // we can only send 100 tracks per request
let mut remainder = if tracks.len() > 100 { let mut remainder = if tracks.len() > 100 {
@@ -188,7 +215,22 @@ impl WebApi {
}; };
if let Some(()) = self.api_with_retry(|api| { if let Some(()) = self.api_with_retry(|api| {
api.user_playlist_replace_tracks(self.user.as_ref().unwrap(), id, &tracks) let playable_ids: Vec<Box<dyn PlayableId>> = tracks
.iter()
.map(|playable| match playable {
Playable::Track(track) => {
Box::new(TrackId::from_id(&track.id.clone().unwrap_or_default()).unwrap())
as Box<dyn PlayableId>
}
Playable::Episode(episode) => {
Box::new(EpisodeId::from_id(&episode.id).unwrap()) as Box<dyn PlayableId>
}
})
.collect();
api.playlist_replace_items(
&PlaylistId::from_id(id).unwrap(),
playable_ids.iter().map(|p| p.as_ref()),
)
}) { }) {
debug!("saved {} tracks to playlist {}", tracks.len(), id); debug!("saved {} tracks to playlist {}", tracks.len(), id);
while let Some(ref mut tracks) = remainder.clone() { while let Some(ref mut tracks) = remainder.clone() {
@@ -213,7 +255,7 @@ impl WebApi {
} }
pub fn delete_playlist(&self, id: &str) -> bool { pub fn delete_playlist(&self, id: &str) -> bool {
self.api_with_retry(|api| api.user_playlist_unfollow(self.user.as_ref().unwrap(), id)) self.api_with_retry(|api| api.playlist_unfollow(&PlaylistId::from_id(id).unwrap()))
.is_some() .is_some()
} }
@@ -221,57 +263,83 @@ impl WebApi {
&self, &self,
name: &str, name: &str,
public: Option<bool>, public: Option<bool>,
description: Option<String>, description: Option<&str>,
) -> Option<String> { ) -> Option<String> {
let result = self.api_with_retry(|api| { let result = self.api_with_retry(|api| {
api.user_playlist_create( api.user_playlist_create(
self.user.as_ref().unwrap(), &UserId::from_id(self.user.as_ref().unwrap()).unwrap(),
name, name,
public, public,
description.clone(), None,
description,
) )
}); });
result.map(|r| r.id) result.map(|r| r.id.id().to_string())
} }
pub fn album(&self, album_id: &str) -> Option<FullAlbum> { pub fn album(&self, album_id: &str) -> Option<FullAlbum> {
self.api_with_retry(|api| api.album(album_id)) self.api_with_retry(|api| api.album(&AlbumId::from_id(album_id).unwrap()))
} }
pub fn artist(&self, artist_id: &str) -> Option<FullArtist> { pub fn artist(&self, artist_id: &str) -> Option<FullArtist> {
self.api_with_retry(|api| api.artist(artist_id)) self.api_with_retry(|api| api.artist(&ArtistId::from_id(artist_id).unwrap()))
} }
pub fn playlist(&self, playlist_id: &str) -> Option<FullPlaylist> { pub fn playlist(&self, playlist_id: &str) -> Option<FullPlaylist> {
self.api_with_retry(|api| api.playlist(playlist_id, None, self.country)) self.api_with_retry(|api| {
api.playlist(
&PlaylistId::from_id(playlist_id).unwrap(),
None,
Some(&Market::FromToken),
)
})
} }
pub fn track(&self, track_id: &str) -> Option<FullTrack> { pub fn track(&self, track_id: &str) -> Option<FullTrack> {
self.api_with_retry(|api| api.track(track_id)) self.api_with_retry(|api| api.track(&TrackId::from_id(track_id).unwrap()))
} }
pub fn get_show(&self, show_id: &str) -> Option<FullShow> { 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)) self.api_with_retry(|api| {
api.get_a_show(&ShowId::from_id(show_id).unwrap(), Some(&Market::FromToken))
})
} }
pub fn episode(&self, episode_id: &str) -> Option<FullEpisode> { pub fn episode(&self, episode_id: &str) -> Option<FullEpisode> {
self.api_with_retry(|api| api.get_an_episode(episode_id.to_string(), self.country)) self.api_with_retry(|api| {
api.get_an_episode(
&EpisodeId::from_id(episode_id).unwrap(),
Some(&Market::FromToken),
)
})
} }
pub fn recommendations( pub fn recommendations(
&self, &self,
seed_artists: Option<Vec<String>>, seed_artists: Option<Vec<&str>>,
seed_genres: Option<Vec<String>>, seed_genres: Option<Vec<&str>>,
seed_tracks: Option<Vec<String>>, seed_tracks: Option<Vec<&str>>,
) -> Option<Recommendations> { ) -> Option<Recommendations> {
self.api_with_retry(|api| { self.api_with_retry(|api| {
let seed_artistids = seed_artists.as_ref().map(|artistids| {
artistids
.iter()
.map(|id| ArtistId::from_id(id).unwrap())
.collect::<Vec<ArtistId>>()
});
let seed_trackids = seed_tracks.as_ref().map(|trackids| {
trackids
.iter()
.map(|id| TrackId::from_id(id).unwrap())
.collect::<Vec<TrackId>>()
});
api.recommendations( api.recommendations(
seed_artists.clone(), std::iter::empty(),
seed_artistids.as_ref(),
seed_genres.clone(), seed_genres.clone(),
seed_tracks.clone(), seed_trackids.as_ref(),
100, Some(&Market::FromToken),
self.country, Some(100),
&Map::new(),
) )
}) })
} }
@@ -283,8 +351,17 @@ impl WebApi {
limit: u32, limit: u32,
offset: u32, offset: u32,
) -> Option<SearchResult> { ) -> Option<SearchResult> {
self.api_with_retry(|api| api.search(query, searchtype, limit, offset, self.country, None)) self.api_with_retry(|api| {
.take() api.search(
query,
&searchtype,
Some(&Market::FromToken),
None,
Some(limit),
Some(offset),
)
})
.take()
} }
pub fn current_user_playlist(&self) -> ApiResult<Playlist> { pub fn current_user_playlist(&self) -> ApiResult<Playlist> {
@@ -292,19 +369,21 @@ impl WebApi {
let spotify = self.clone(); let spotify = self.clone();
let fetch_page = move |offset: u32| { let fetch_page = move |offset: u32| {
debug!("fetching user playlists, offset: {}", offset); debug!("fetching user playlists, offset: {}", offset);
spotify.api_with_retry(|api| match api.current_user_playlists(MAX_LIMIT, offset) { spotify.api_with_retry(|api| {
Ok(page) => Ok(ApiPage { match api.current_user_playlists_manual(Some(MAX_LIMIT), Some(offset)) {
offset: page.offset, Ok(page) => Ok(ApiPage {
total: page.total, offset: page.offset,
items: page.items.iter().map(|sp| sp.into()).collect(), total: page.total,
}), items: page.items.iter().map(|sp| sp.into()).collect(),
Err(e) => Err(e), }),
Err(e) => Err(e),
}
}) })
}; };
ApiResult::new(MAX_LIMIT, Arc::new(fetch_page)) ApiResult::new(MAX_LIMIT, Arc::new(fetch_page))
} }
pub fn user_playlist_tracks(&self, playlist_id: &str) -> ApiResult<Track> { pub fn user_playlist_tracks(&self, playlist_id: &str) -> ApiResult<Playable> {
const MAX_LIMIT: u32 = 100; const MAX_LIMIT: u32 = 100;
let spotify = self.clone(); let spotify = self.clone();
let playlist_id = playlist_id.to_string(); let playlist_id = playlist_id.to_string();
@@ -314,13 +393,12 @@ impl WebApi {
playlist_id, offset playlist_id, offset
); );
spotify.api_with_retry(|api| { spotify.api_with_retry(|api| {
match api.user_playlist_tracks( match api.playlist_items_manual(
spotify.user.as_ref().unwrap(), &PlaylistId::from_id(&playlist_id).unwrap(),
&playlist_id,
None, None,
MAX_LIMIT, Some(&Market::FromToken),
offset, Some(MAX_LIMIT),
spotify.country, Some(offset),
) { ) {
Ok(page) => Ok(ApiPage { Ok(page) => Ok(ApiPage {
offset: page.offset, offset: page.offset,
@@ -331,10 +409,11 @@ impl WebApi {
.enumerate() .enumerate()
.flat_map(|(index, pt)| { .flat_map(|(index, pt)| {
pt.track.as_ref().map(|t| { pt.track.as_ref().map(|t| {
let mut track: Track = t.into(); let mut playable: Playable = t.into();
track.added_at = Some(pt.added_at); // TODO: set these
track.list_index = page.offset as usize + index; playable.set_added_at(pt.added_at);
track playable.set_list_index(page.offset as usize + index);
playable
}) })
}) })
.collect(), .collect(),
@@ -347,7 +426,7 @@ impl WebApi {
} }
pub fn full_album(&self, album_id: &str) -> Option<FullAlbum> { pub fn full_album(&self, album_id: &str) -> Option<FullAlbum> {
self.api_with_retry(|api| api.album(album_id)) self.api_with_retry(|api| api.album(&AlbumId::from_id(album_id).unwrap()))
} }
pub fn album_tracks( pub fn album_tracks(
@@ -356,7 +435,13 @@ impl WebApi {
limit: u32, limit: u32,
offset: u32, offset: u32,
) -> Option<Page<SimplifiedTrack>> { ) -> Option<Page<SimplifiedTrack>> {
self.api_with_retry(|api| api.album_track(album_id, limit, offset)) self.api_with_retry(|api| {
api.album_track_manual(
&AlbumId::from_id(album_id).unwrap(),
Some(limit),
Some(offset),
)
})
} }
pub fn artist_albums( pub fn artist_albums(
@@ -370,10 +455,10 @@ impl WebApi {
let fetch_page = move |offset: u32| { let fetch_page = move |offset: u32| {
debug!("fetching artist {} albums, offset: {}", artist_id, offset); debug!("fetching artist {} albums, offset: {}", artist_id, offset);
spotify.api_with_retry(|api| { spotify.api_with_retry(|api| {
match api.artist_albums( match api.artist_albums_manual(
&artist_id, &ArtistId::from_id(&artist_id).unwrap(),
album_type, album_type.as_ref(),
spotify.country, Some(&Market::FromToken),
Some(MAX_SIZE), Some(MAX_SIZE),
Some(offset), Some(offset),
) { ) {
@@ -402,7 +487,12 @@ impl WebApi {
let fetch_page = move |offset: u32| { let fetch_page = move |offset: u32| {
debug!("fetching show {} episodes, offset: {}", &show_id, offset); debug!("fetching show {} episodes, offset: {}", &show_id, offset);
spotify.api_with_retry(|api| { spotify.api_with_retry(|api| {
match api.get_shows_episodes(show_id.clone(), MAX_SIZE, offset, spotify.country) { match api.get_shows_episodes_manual(
&ShowId::from_id(&show_id).unwrap(),
Some(&Market::FromToken),
Some(50),
Some(offset),
) {
Ok(page) => Ok(ApiPage { Ok(page) => Ok(ApiPage {
offset: page.offset, offset: page.offset,
total: page.total, total: page.total,
@@ -417,71 +507,125 @@ impl WebApi {
} }
pub fn get_saved_shows(&self, offset: u32) -> Option<Page<Show>> { pub fn get_saved_shows(&self, offset: u32) -> Option<Page<Show>> {
self.api_with_retry(|api| api.get_saved_show(50, offset)) self.api_with_retry(|api| api.get_saved_show_manual(Some(50), Some(offset)))
} }
pub fn save_shows(&self, ids: Vec<String>) -> bool { pub fn save_shows(&self, ids: Vec<&str>) -> bool {
self.api_with_retry(|api| api.save_shows(ids.clone())) self.api_with_retry(|api| {
.is_some() api.save_shows(
&ids.iter()
.map(|id| ShowId::from_id(id).unwrap())
.collect::<Vec<ShowId>>(),
)
})
.is_some()
} }
pub fn unsave_shows(&self, ids: Vec<String>) -> bool { pub fn unsave_shows(&self, ids: Vec<&str>) -> bool {
self.api_with_retry(|api| api.remove_users_saved_shows(ids.clone(), self.country)) self.api_with_retry(|api| {
.is_some() api.remove_users_saved_shows(
&ids.iter()
.map(|id| ShowId::from_id(id).unwrap())
.collect::<Vec<ShowId>>(),
Some(&Market::FromToken),
)
})
.is_some()
} }
pub fn current_user_followed_artists( pub fn current_user_followed_artists(
&self, &self,
last: Option<String>, last: Option<&str>,
) -> Option<CursorBasedPage<FullArtist>> { ) -> Option<CursorBasedPage<FullArtist>> {
self.api_with_retry(|api| api.current_user_followed_artists(50, last.clone())) self.api_with_retry(|api| api.current_user_followed_artists(last, Some(50)))
.map(|cp| cp.artists)
} }
pub fn user_follow_artists(&self, ids: Vec<String>) -> Option<()> { pub fn user_follow_artists(&self, ids: Vec<&str>) -> Option<()> {
self.api_with_retry(|api| api.user_follow_artists(&ids)) self.api_with_retry(|api| {
api.user_follow_artists(
&ids.iter()
.map(|id| ArtistId::from_id(id).unwrap())
.collect::<Vec<ArtistId>>(),
)
})
} }
pub fn user_unfollow_artists(&self, ids: Vec<String>) -> Option<()> { pub fn user_unfollow_artists(&self, ids: Vec<&str>) -> Option<()> {
self.api_with_retry(|api| api.user_unfollow_artists(&ids)) self.api_with_retry(|api| {
api.user_unfollow_artists(
&ids.iter()
.map(|id| ArtistId::from_id(id).unwrap())
.collect::<Vec<ArtistId>>(),
)
})
} }
pub fn current_user_saved_albums(&self, offset: u32) -> Option<Page<SavedAlbum>> { pub fn current_user_saved_albums(&self, offset: u32) -> Option<Page<SavedAlbum>> {
self.api_with_retry(|api| api.current_user_saved_albums(50, offset)) self.api_with_retry(|api| {
api.current_user_saved_albums_manual(Some(&Market::FromToken), Some(50), Some(offset))
})
} }
pub fn current_user_saved_albums_add(&self, ids: Vec<String>) -> Option<()> { pub fn current_user_saved_albums_add(&self, ids: Vec<&str>) -> Option<()> {
self.api_with_retry(|api| api.current_user_saved_albums_add(&ids)) self.api_with_retry(|api| {
api.current_user_saved_albums_add(
&ids.iter()
.map(|id| AlbumId::from_id(id).unwrap())
.collect::<Vec<AlbumId>>(),
)
})
} }
pub fn current_user_saved_albums_delete(&self, ids: Vec<String>) -> Option<()> { pub fn current_user_saved_albums_delete(&self, ids: Vec<&str>) -> Option<()> {
self.api_with_retry(|api| api.current_user_saved_albums_delete(&ids)) self.api_with_retry(|api| {
api.current_user_saved_albums_delete(
&ids.iter()
.map(|id| AlbumId::from_id(id).unwrap())
.collect::<Vec<AlbumId>>(),
)
})
} }
pub fn current_user_saved_tracks(&self, offset: u32) -> Option<Page<SavedTrack>> { pub fn current_user_saved_tracks(&self, offset: u32) -> Option<Page<SavedTrack>> {
self.api_with_retry(|api| api.current_user_saved_tracks(50, offset)) self.api_with_retry(|api| {
api.current_user_saved_tracks_manual(Some(&Market::FromToken), Some(50), Some(offset))
})
} }
pub fn current_user_saved_tracks_add(&self, ids: Vec<String>) -> Option<()> { pub fn current_user_saved_tracks_add(&self, ids: Vec<&str>) -> Option<()> {
self.api_with_retry(|api| api.current_user_saved_tracks_add(&ids)) self.api_with_retry(|api| {
api.current_user_saved_tracks_add(
&ids.iter()
.map(|id| TrackId::from_id(id).unwrap())
.collect::<Vec<TrackId>>(),
)
})
} }
pub fn current_user_saved_tracks_delete(&self, ids: Vec<String>) -> Option<()> { pub fn current_user_saved_tracks_delete(&self, ids: Vec<&str>) -> Option<()> {
self.api_with_retry(|api| api.current_user_saved_tracks_delete(&ids)) self.api_with_retry(|api| {
api.current_user_saved_tracks_delete(
&ids.iter()
.map(|id| TrackId::from_id(id).unwrap())
.collect::<Vec<TrackId>>(),
)
})
} }
pub fn user_playlist_follow_playlist(&self, owner_id: String, id: String) -> Option<()> { pub fn user_playlist_follow_playlist(&self, id: &str) -> Option<()> {
self.api_with_retry(|api| api.user_playlist_follow_playlist(&owner_id, &id, true)) self.api_with_retry(|api| api.playlist_follow(&PlaylistId::from_id(id).unwrap(), None))
} }
pub fn artist_top_tracks(&self, id: &str) -> Option<Vec<Track>> { pub fn artist_top_tracks(&self, id: &str) -> Option<Vec<Track>> {
self.api_with_retry(|api| api.artist_top_tracks(id, self.country)) self.api_with_retry(|api| {
.map(|ft| ft.tracks.iter().map(|t| t.into()).collect()) api.artist_top_tracks(&ArtistId::from_id(id).unwrap(), &Market::FromToken)
})
.map(|ft| ft.iter().map(|t| t.into()).collect())
} }
pub fn artist_related_artists(&self, id: String) -> Option<Vec<Artist>> { pub fn artist_related_artists(&self, id: &str) -> Option<Vec<Artist>> {
self.api_with_retry(|api| api.artist_related_artists(&id)) self.api_with_retry(|api| api.artist_related_artists(&ArtistId::from_id(id).unwrap()))
.map(|fa| fa.artists.iter().map(|a| a.into()).collect()) .map(|fa| fa.iter().map(|a| a.into()).collect())
} }
pub fn current_user(&self) -> Option<PrivateUser> { pub fn current_user(&self) -> Option<PrivateUser> {

View File

@@ -4,6 +4,7 @@ use std::sync::{Arc, RwLock};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use rspotify::model::album::FullAlbum; use rspotify::model::album::FullAlbum;
use rspotify::model::track::{FullTrack, SavedTrack, SimplifiedTrack}; use rspotify::model::track::{FullTrack, SavedTrack, SimplifiedTrack};
use rspotify::model::Id;
use crate::album::Album; use crate::album::Album;
use crate::artist::Artist; use crate::artist::Artist;
@@ -42,7 +43,7 @@ impl Track {
let artist_ids = track let artist_ids = track
.artists .artists
.iter() .iter()
.filter_map(|a| a.id.clone()) .filter_map(|a| a.id.as_ref().map(|id| id.id().to_string()))
.collect::<Vec<String>>(); .collect::<Vec<String>>();
let album_artists = album let album_artists = album
.artists .artists
@@ -51,19 +52,19 @@ impl Track {
.collect::<Vec<String>>(); .collect::<Vec<String>>();
Self { Self {
id: track.id.clone(), id: track.id.as_ref().map(|id| id.id().to_string()),
uri: track.uri.clone(), uri: track.id.as_ref().map(|id| id.uri()).unwrap_or_default(),
title: track.name.clone(), title: track.name.clone(),
track_number: track.track_number, track_number: track.track_number,
disc_number: track.disc_number, disc_number: track.disc_number,
duration: track.duration_ms, duration: track.duration.as_millis() as u32,
artists, artists,
artist_ids, artist_ids,
album: Some(album.name.clone()), album: Some(album.name.clone()),
album_id: Some(album.id.clone()), album_id: Some(album.id.id().to_string()),
album_artists, album_artists,
cover_url: album.images.get(0).map(|img| img.url.clone()), cover_url: album.images.get(0).map(|img| img.url.clone()),
url: track.uri.clone(), url: track.id.as_ref().map(|id| id.url()).unwrap_or_default(),
added_at: None, added_at: None,
list_index: 0, list_index: 0,
} }
@@ -86,23 +87,23 @@ impl From<&SimplifiedTrack> for Track {
let artist_ids = track let artist_ids = track
.artists .artists
.iter() .iter()
.filter_map(|a| a.id.clone()) .filter_map(|a| a.id.as_ref().map(|a| a.id().to_string()))
.collect::<Vec<String>>(); .collect::<Vec<String>>();
Self { Self {
id: track.id.clone(), id: track.id.as_ref().map(|id| id.id().to_string()),
uri: track.uri.clone(), uri: track.id.as_ref().map(|id| id.uri()).unwrap_or_default(),
title: track.name.clone(), title: track.name.clone(),
track_number: track.track_number, track_number: track.track_number,
disc_number: track.disc_number, disc_number: track.disc_number,
duration: track.duration_ms, duration: track.duration.as_millis() as u32,
artists, artists,
artist_ids, artist_ids,
album: None, album: None,
album_id: None, album_id: None,
album_artists: Vec::new(), album_artists: Vec::new(),
cover_url: None, cover_url: None,
url: track.uri.clone(), url: track.id.as_ref().map(|id| id.url()).unwrap_or_default(),
added_at: None, added_at: None,
list_index: 0, list_index: 0,
} }
@@ -119,7 +120,7 @@ impl From<&FullTrack> for Track {
let artist_ids = track let artist_ids = track
.artists .artists
.iter() .iter()
.filter_map(|a| a.id.clone()) .filter_map(|a| a.id.as_ref().map(|a| a.id().to_string()))
.collect::<Vec<String>>(); .collect::<Vec<String>>();
let album_artists = track let album_artists = track
.album .album
@@ -129,19 +130,19 @@ impl From<&FullTrack> for Track {
.collect::<Vec<String>>(); .collect::<Vec<String>>();
Self { Self {
id: track.id.clone(), id: Some(track.id.id().to_string()),
uri: track.uri.clone(), uri: track.id.uri(),
title: track.name.clone(), title: track.name.clone(),
track_number: track.track_number, track_number: track.track_number,
disc_number: track.disc_number, disc_number: track.disc_number,
duration: track.duration_ms, duration: track.duration.as_millis() as u32,
artists, artists,
artist_ids, artist_ids,
album: Some(track.album.name.clone()), album: Some(track.album.name.clone()),
album_id: track.album.id.clone(), album_id: track.album.id.as_ref().map(|a| a.id().to_string()),
album_artists, album_artists,
cover_url: track.album.images.get(0).map(|img| img.url.clone()), cover_url: track.album.images.get(0).map(|img| img.url.clone()),
url: track.uri.clone(), url: track.id.url(),
added_at: None, added_at: None,
list_index: 0, list_index: 0,
} }
@@ -210,7 +211,7 @@ impl ListItem for Track {
} }
fn play(&mut self, queue: Arc<Queue>) { fn play(&mut self, queue: Arc<Queue>) {
let index = queue.append_next(vec![Playable::Track(self.clone())]); let index = queue.append_next(&vec![Playable::Track(self.clone())]);
queue.play(index, true, false); queue.play(index, true, false);
} }
@@ -252,7 +253,7 @@ impl ListItem for Track {
let recommendations: Option<Vec<Track>> = if let Some(id) = &self.id { let recommendations: Option<Vec<Track>> = if let Some(id) = &self.id {
spotify spotify
.api .api
.recommendations(None, None, Some(vec![id.clone()])) .recommendations(None, None, Some(vec![id]))
.map(|r| r.tracks) .map(|r| r.tracks)
.map(|tracks| tracks.iter().map(Track::from).collect()) .map(|tracks| tracks.iter().map(Track::from).collect())
} else { } else {

View File

@@ -3,6 +3,7 @@ use std::thread;
use cursive::view::ViewWrapper; use cursive::view::ViewWrapper;
use cursive::Cursive; use cursive::Cursive;
use rspotify::model::AlbumType;
use crate::album::Album; use crate::album::Album;
use crate::artist::Artist; use crate::artist::Artist;
@@ -14,7 +15,6 @@ use crate::track::Track;
use crate::traits::ViewExt; use crate::traits::ViewExt;
use crate::ui::listview::ListView; use crate::ui::listview::ListView;
use crate::ui::tabview::TabView; use crate::ui::tabview::TabView;
use rspotify::senum::AlbumType;
pub struct ArtistView { pub struct ArtistView {
artist: Artist, artist: Artist,
@@ -53,7 +53,7 @@ impl ArtistView {
let library = library.clone(); let library = library.clone();
thread::spawn(move || { thread::spawn(move || {
if let Some(id) = id { if let Some(id) = id {
if let Some(artists) = spotify.api.artist_related_artists(id) { if let Some(artists) = spotify.api.artist_related_artists(&id) {
related.write().unwrap().extend(artists); related.write().unwrap().extend(artists);
library.trigger_redraw(); library.trigger_redraw();
} }

View File

@@ -71,7 +71,7 @@ impl ContextMenu {
let spotify = spotify.clone(); let spotify = spotify.clone();
let library = library.clone(); let library = library.clone();
playlist.append_tracks(&[track.clone()], spotify, library); playlist.append_tracks(&[Playable::Track(track.clone())], spotify, library);
c.pop_layer(); c.pop_layer();
// Close add_track_dialog too // Close add_track_dialog too
@@ -81,7 +81,7 @@ impl ContextMenu {
let modal = Modal::new(already_added_dialog); let modal = Modal::new(already_added_dialog);
s.add_layer(modal); s.add_layer(modal);
} else { } else {
playlist.append_tracks(&[track], spotify, library); playlist.append_tracks(&[Playable::Track(track)], spotify, library);
s.pop_layer(); s.pop_layer();
} }
}); });

View File

@@ -117,7 +117,7 @@ impl<I: ListItem> ListView<I> {
.iter() .iter()
.map(|track| Playable::Track(track.clone())) .map(|track| Playable::Track(track.clone()))
.collect(); .collect();
let index = self.queue.append_next(tracks); let index = self.queue.append_next(&tracks);
self.queue.play(index + self.selected, true, false); self.queue.play(index + self.selected, true, false);
true true
} else { } else {

View File

@@ -6,16 +6,17 @@ use cursive::Cursive;
use crate::command::Command; use crate::command::Command;
use crate::commands::CommandResult; use crate::commands::CommandResult;
use crate::library::Library; use crate::library::Library;
use crate::playable::Playable;
use crate::playlist::Playlist; use crate::playlist::Playlist;
use crate::queue::Queue; use crate::queue::Queue;
use crate::spotify::Spotify; use crate::spotify::Spotify;
use crate::track::Track;
use crate::traits::ViewExt; use crate::traits::ViewExt;
use crate::ui::listview::ListView; use crate::ui::listview::ListView;
pub struct PlaylistView { pub struct PlaylistView {
playlist: Playlist, playlist: Playlist,
list: ListView<Track>, list: ListView<Playable>,
spotify: Spotify, spotify: Spotify,
library: Arc<Library>, library: Arc<Library>,
queue: Arc<Queue>, queue: Arc<Queue>,
@@ -54,7 +55,7 @@ impl PlaylistView {
} }
impl ViewWrapper for PlaylistView { impl ViewWrapper for PlaylistView {
wrap_impl!(self.list: ListView<Track>); wrap_impl!(self.list: ListView<Playable>);
} }
impl ViewExt for PlaylistView { impl ViewExt for PlaylistView {
@@ -64,7 +65,7 @@ impl ViewExt for PlaylistView {
fn title_sub(&self) -> String { fn title_sub(&self) -> String {
if let Some(tracks) = self.playlist.tracks.as_ref() { if let Some(tracks) = self.playlist.tracks.as_ref() {
let duration_secs = tracks.iter().map(|p| p.duration as u64 / 1000).sum(); let duration_secs = tracks.iter().map(|p| p.duration() as u64 / 1000).sum();
let duration = std::time::Duration::from_secs(duration_secs); let duration = std::time::Duration::from_secs(duration_secs);
format!( format!(
"{} tracks, {}", "{} tracks, {}",

View File

@@ -28,7 +28,6 @@ use crate::ui::pagination::Pagination;
use crate::ui::search_results::SearchResultsView; use crate::ui::search_results::SearchResultsView;
use crate::ui::tabview::TabView; use crate::ui::tabview::TabView;
use rspotify::model::search::SearchResult; use rspotify::model::search::SearchResult;
use rspotify::senum::SearchType;
pub struct SearchView { pub struct SearchView {
edit: NamedView<EditView>, edit: NamedView<EditView>,

View File

@@ -18,7 +18,7 @@ use crate::ui::tabview::TabView;
use cursive::view::ViewWrapper; use cursive::view::ViewWrapper;
use cursive::Cursive; use cursive::Cursive;
use rspotify::model::search::SearchResult; use rspotify::model::search::SearchResult;
use rspotify::senum::SearchType; use rspotify::model::SearchType;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
pub struct SearchResultsView { pub struct SearchResultsView {