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"
fern = "0.6"
futures = { version = "0.3", features = ["compat"] }
futures_01 = { version = "0.1", package = "futures" }
lazy_static = "1.3.0"
librespot-core = "0.3.1"
librespot-playback = "0.3.1"
librespot-protocol = "0.3.1"
log = "0.4.13"
notify-rust = { version = "4", optional = true }
rspotify = { version = "0.10.0", features = ["blocking"] }
serde = "1.0"
serde_json = "1.0"
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"
pancurses = { version = "0.17.0", features = ["win32"] }
[dependencies.rspotify]
version = "0.11.2"
default-features = false
features = ["client-ureq", "ureq-rustls-tls"]
[dependencies.cursive]
version = "0.16.3"
default-features = false

View File

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

View File

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

View File

@@ -2,7 +2,9 @@ use crate::library::Library;
use crate::playable::Playable;
use crate::queue::Queue;
use crate::traits::{ListItem, ViewExt};
use chrono::{DateTime, Utc};
use rspotify::model::show::{FullEpisode, SimplifiedEpisode};
use rspotify::model::Id;
use std::fmt;
use std::sync::Arc;
@@ -15,6 +17,8 @@ pub struct Episode {
pub description: String,
pub release_date: String,
pub cover_url: Option<String>,
pub added_at: Option<DateTime<Utc>>,
pub list_index: usize,
}
impl Episode {
@@ -28,13 +32,15 @@ impl Episode {
impl From<&SimplifiedEpisode> for Episode {
fn from(episode: &SimplifiedEpisode) -> Self {
Self {
id: episode.id.clone(),
uri: episode.uri.clone(),
duration: episode.duration_ms,
id: episode.id.id().to_string(),
uri: episode.id.uri(),
duration: episode.duration.as_millis() as u32,
name: episode.name.clone(),
description: episode.description.clone(),
release_date: episode.release_date.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 {
fn from(episode: &FullEpisode) -> Self {
Self {
id: episode.id.clone(),
uri: episode.uri.clone(),
duration: episode.duration_ms,
id: episode.id.id().to_string(),
uri: episode.id.uri(),
duration: episode.duration.as_millis() as u32,
name: episode.name.clone(),
description: episode.description.clone(),
release_date: episode.release_date.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>) {
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);
}

View File

@@ -6,6 +6,7 @@ use std::sync::{Arc, RwLock, RwLockReadGuard};
use std::thread;
use log::{debug, error, info};
use rspotify::model::Id;
use serde::de::DeserializeOwned;
use serde::Serialize;
@@ -43,7 +44,7 @@ pub struct Library {
impl Library {
pub fn new(ev: &EventManager, spotify: Spotify, cfg: Arc<Config>) -> Self {
let current_user = spotify.api.current_user();
let 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 library = Self {
@@ -307,7 +308,7 @@ impl Library {
fn fetch_artists(&self) {
let mut artists: Vec<Artist> = Vec::new();
let mut last: Option<String> = None;
let mut last: Option<&str> = None;
let mut i: u32 = 0;
@@ -324,7 +325,7 @@ impl Library {
artists.extend(page.items.iter().map(|fa| fa.into()));
if page.next.is_some() {
last = artists.last().unwrap().id.clone();
last = artists.last().unwrap().id.as_deref();
} else {
break;
}
@@ -387,7 +388,7 @@ impl Library {
.items
.iter()
.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;
}
@@ -438,7 +439,7 @@ impl Library {
.items
.iter()
.enumerate()
.any(|(i, t)| t.track.id != store[i].id)
.any(|(i, t)| Some(t.track.id.id().to_string()) != store[i].id)
{
return;
}
@@ -547,7 +548,9 @@ impl Library {
&& self
.spotify
.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()
{
return;
@@ -582,7 +585,7 @@ impl Library {
.spotify
.api
.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()
{
@@ -622,7 +625,7 @@ impl Library {
if self
.spotify
.api
.current_user_saved_albums_add(vec![album_id.clone()])
.current_user_saved_albums_add(vec![album_id.as_str()])
.is_none()
{
return;
@@ -651,7 +654,7 @@ impl Library {
if self
.spotify
.api
.current_user_saved_albums_delete(vec![album_id.clone()])
.current_user_saved_albums_delete(vec![album_id.as_str()])
.is_none()
{
return;
@@ -684,7 +687,7 @@ impl Library {
if self
.spotify
.api
.user_follow_artists(vec![artist_id.clone()])
.user_follow_artists(vec![artist_id.as_str()])
.is_none()
{
return;
@@ -716,7 +719,7 @@ impl Library {
if self
.spotify
.api
.user_unfollow_artists(vec![artist_id.clone()])
.user_unfollow_artists(vec![artist_id.as_str()])
.is_none()
{
return;
@@ -759,7 +762,7 @@ impl Library {
if self
.spotify
.api
.user_playlist_follow_playlist(playlist.owner_id.clone(), playlist.id.clone())
.user_playlist_follow_playlist(playlist.id.as_str())
.is_none()
{
return;
@@ -792,7 +795,7 @@ impl Library {
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();
if !store.iter().any(|s| s.id == show.id) {
@@ -807,7 +810,7 @@ impl Library {
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();
*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 {
queue.clear();
let index = queue.append_next(
t.iter()
&t.iter()
.map(|track| Playable::Track(track.clone()))
.collect(),
);
@@ -606,11 +606,7 @@ fn run_dbus_server(
playlist.load_tracks(spotify);
if let Some(t) = &playlist.tracks {
queue.clear();
let index = queue.append_next(
t.iter()
.map(|track| Playable::Track(track.clone()))
.collect(),
);
let index = queue.append_next(&t.iter().cloned().collect());
queue.play(index, false, false)
}
}
@@ -625,7 +621,7 @@ fn run_dbus_server(
let mut ep = e.clone();
ep.reverse();
let index = queue.append_next(
ep.iter()
&ep.iter()
.map(|episode| Playable::Episode(episode.clone()))
.collect(),
);
@@ -643,7 +639,7 @@ fn run_dbus_server(
Some(UriType::Artist) => {
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.append_next(&a.iter().map(|track| Playable::Track(track.clone())).collect());
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::artist::Artist;
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 {
let duration = self.duration();
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 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {

View File

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

View File

@@ -146,7 +146,7 @@ impl Queue {
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();
{

View File

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

View File

@@ -12,8 +12,6 @@ use librespot_playback::audio_backend;
use librespot_playback::config::Bitrate;
use librespot_playback::player::Player;
use rspotify::senum::Country;
use futures::channel::oneshot;
use tokio::sync::mpsc;
@@ -82,14 +80,7 @@ impl 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
}

View File

@@ -7,22 +7,19 @@ 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 rspotify::http::HttpError;
use rspotify::model::{
AlbumId, AlbumType, ArtistId, CursorBasedPage, EpisodeId, FullAlbum, FullArtist, FullEpisode,
FullPlaylist, FullShow, FullTrack, ItemPositions, Market, Page, PlayableId, PlaylistId,
PrivateUser, Recommendations, SavedAlbum, SavedTrack, SearchResult, SearchType, Show, ShowId,
SimplifiedTrack, TrackId, UserId,
};
use rspotify::{prelude::*, AuthCodeSpotify, ClientError, ClientResult, Token};
use std::collections::HashSet;
use std::iter::FromIterator;
use std::sync::{Arc, RwLock};
use std::thread;
use std::time::Duration;
@@ -30,9 +27,8 @@ use tokio::sync::mpsc;
#[derive(Clone)]
pub struct WebApi {
api: Arc<RwLock<SpotifyAPI>>,
api: AuthCodeSpotify,
user: Option<String>,
country: Option<Country>,
worker_channel: Arc<RwLock<Option<mpsc::UnboundedSender<WorkerCommand>>>>,
token_expiration: Arc<RwLock<DateTime<Utc>>>,
}
@@ -40,9 +36,8 @@ pub struct WebApi {
impl WebApi {
pub fn new() -> WebApi {
WebApi {
api: Arc::new(RwLock::new(SpotifyAPI::default())),
api: AuthCodeSpotify::default(),
user: None,
country: None,
worker_channel: Arc::new(RwLock::new(None)),
token_expiration: Arc::new(RwLock::new(Utc::now())),
}
@@ -52,10 +47,6 @@ impl WebApi {
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>>>>,
@@ -87,8 +78,13 @@ impl WebApi {
{
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.api.token.lock().expect("can't writelock api token") = Some(Token {
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
.token_expiration
.write()
@@ -102,32 +98,32 @@ impl WebApi {
/// 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>,
F: Fn(&AuthCodeSpotify) -> ClientResult<R>,
{
let result = {
let api = self.api.read().expect("can't read api");
cb(&api)
};
let result = { cb(&self.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()
Err(ClientError::Http(error)) => {
debug!("http error: {:?}", error);
if let HttpError::StatusCode(response) = error.as_ref() {
match response.status() {
429 => {
let waiting_duration = response
.header("Retry-After")
.and_then(|v| v.parse::<u64>().ok());
debug!("rate limit hit. waiting {:?} seconds", waiting_duration);
thread::sleep(
Duration::from_secs(waiting_duration.unwrap_or(0) as u64),
);
cb(&self.api).ok()
}
ApiError::Unauthorized => {
401 => {
debug!("token unauthorized. trying refresh..");
self.update_token();
let api = self.api.read().expect("can't read api");
cb(&api).ok()
cb(&self.api).ok()
}
e => {
error!("unhandled api error: {}", e);
_ => {
error!("unhandled api error: {:?}", response);
None
}
}
@@ -135,17 +131,34 @@ impl WebApi {
None
}
}
Err(e) => {
error!("unhandled api error: {}", e);
None
}
}
}
pub fn append_tracks(
&self,
playlist_id: &str,
tracks: &[String],
tracks: &[Playable],
position: Option<i32>,
) -> bool {
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()
}
@@ -154,31 +167,45 @@ impl WebApi {
&self,
playlist_id: &str,
snapshot_id: &str,
track_pos_pairs: &[(&Track, usize)],
playables: &[Playable],
) -> 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()),
self.api_with_retry(move |api| {
let playable_ids: Vec<Box<dyn PlayableId>> = playables
.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();
let positions = playables
.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()
}
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();
// create mutable copy for chunking
let mut tracks: Vec<Playable> = tracks.to_vec();
// we can only send 100 tracks per request
let mut remainder = if tracks.len() > 100 {
@@ -188,7 +215,22 @@ impl WebApi {
};
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);
while let Some(ref mut tracks) = remainder.clone() {
@@ -213,7 +255,7 @@ impl WebApi {
}
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()
}
@@ -221,57 +263,83 @@ impl WebApi {
&self,
name: &str,
public: Option<bool>,
description: Option<String>,
description: Option<&str>,
) -> Option<String> {
let result = self.api_with_retry(|api| {
api.user_playlist_create(
self.user.as_ref().unwrap(),
&UserId::from_id(self.user.as_ref().unwrap()).unwrap(),
name,
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> {
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> {
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> {
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> {
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> {
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> {
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(
&self,
seed_artists: Option<Vec<String>>,
seed_genres: Option<Vec<String>>,
seed_tracks: Option<Vec<String>>,
seed_artists: Option<Vec<&str>>,
seed_genres: Option<Vec<&str>>,
seed_tracks: Option<Vec<&str>>,
) -> Option<Recommendations> {
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(
seed_artists.clone(),
std::iter::empty(),
seed_artistids.as_ref(),
seed_genres.clone(),
seed_tracks.clone(),
100,
self.country,
&Map::new(),
seed_trackids.as_ref(),
Some(&Market::FromToken),
Some(100),
)
})
}
@@ -283,8 +351,17 @@ impl WebApi {
limit: u32,
offset: u32,
) -> Option<SearchResult> {
self.api_with_retry(|api| api.search(query, searchtype, limit, offset, self.country, None))
.take()
self.api_with_retry(|api| {
api.search(
query,
&searchtype,
Some(&Market::FromToken),
None,
Some(limit),
Some(offset),
)
})
.take()
}
pub fn current_user_playlist(&self) -> ApiResult<Playlist> {
@@ -292,19 +369,21 @@ impl WebApi {
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),
spotify.api_with_retry(|api| {
match api.current_user_playlists_manual(Some(MAX_LIMIT), Some(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> {
pub fn user_playlist_tracks(&self, playlist_id: &str) -> ApiResult<Playable> {
const MAX_LIMIT: u32 = 100;
let spotify = self.clone();
let playlist_id = playlist_id.to_string();
@@ -314,13 +393,12 @@ impl WebApi {
playlist_id, offset
);
spotify.api_with_retry(|api| {
match api.user_playlist_tracks(
spotify.user.as_ref().unwrap(),
&playlist_id,
match api.playlist_items_manual(
&PlaylistId::from_id(&playlist_id).unwrap(),
None,
MAX_LIMIT,
offset,
spotify.country,
Some(&Market::FromToken),
Some(MAX_LIMIT),
Some(offset),
) {
Ok(page) => Ok(ApiPage {
offset: page.offset,
@@ -331,10 +409,11 @@ impl WebApi {
.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
let mut playable: Playable = t.into();
// TODO: set these
playable.set_added_at(pt.added_at);
playable.set_list_index(page.offset as usize + index);
playable
})
})
.collect(),
@@ -347,7 +426,7 @@ impl WebApi {
}
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(
@@ -356,7 +435,13 @@ impl WebApi {
limit: u32,
offset: u32,
) -> 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(
@@ -370,10 +455,10 @@ impl WebApi {
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,
match api.artist_albums_manual(
&ArtistId::from_id(&artist_id).unwrap(),
album_type.as_ref(),
Some(&Market::FromToken),
Some(MAX_SIZE),
Some(offset),
) {
@@ -402,7 +487,12 @@ impl WebApi {
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) {
match api.get_shows_episodes_manual(
&ShowId::from_id(&show_id).unwrap(),
Some(&Market::FromToken),
Some(50),
Some(offset),
) {
Ok(page) => Ok(ApiPage {
offset: page.offset,
total: page.total,
@@ -417,71 +507,125 @@ impl WebApi {
}
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 {
self.api_with_retry(|api| api.save_shows(ids.clone()))
.is_some()
pub fn save_shows(&self, ids: Vec<&str>) -> bool {
self.api_with_retry(|api| {
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 {
self.api_with_retry(|api| api.remove_users_saved_shows(ids.clone(), self.country))
.is_some()
pub fn unsave_shows(&self, ids: Vec<&str>) -> bool {
self.api_with_retry(|api| {
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(
&self,
last: Option<String>,
last: Option<&str>,
) -> Option<CursorBasedPage<FullArtist>> {
self.api_with_retry(|api| api.current_user_followed_artists(50, last.clone()))
.map(|cp| cp.artists)
self.api_with_retry(|api| api.current_user_followed_artists(last, Some(50)))
}
pub fn user_follow_artists(&self, ids: Vec<String>) -> Option<()> {
self.api_with_retry(|api| api.user_follow_artists(&ids))
pub fn user_follow_artists(&self, ids: Vec<&str>) -> Option<()> {
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<()> {
self.api_with_retry(|api| api.user_unfollow_artists(&ids))
pub fn user_unfollow_artists(&self, ids: Vec<&str>) -> Option<()> {
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>> {
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<()> {
self.api_with_retry(|api| api.current_user_saved_albums_add(&ids))
pub fn current_user_saved_albums_add(&self, ids: Vec<&str>) -> Option<()> {
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<()> {
self.api_with_retry(|api| api.current_user_saved_albums_delete(&ids))
pub fn current_user_saved_albums_delete(&self, ids: Vec<&str>) -> Option<()> {
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>> {
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<()> {
self.api_with_retry(|api| api.current_user_saved_tracks_add(&ids))
pub fn current_user_saved_tracks_add(&self, ids: Vec<&str>) -> Option<()> {
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<()> {
self.api_with_retry(|api| api.current_user_saved_tracks_delete(&ids))
pub fn current_user_saved_tracks_delete(&self, ids: Vec<&str>) -> Option<()> {
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<()> {
self.api_with_retry(|api| api.user_playlist_follow_playlist(&owner_id, &id, true))
pub fn user_playlist_follow_playlist(&self, id: &str) -> Option<()> {
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>> {
self.api_with_retry(|api| api.artist_top_tracks(id, self.country))
.map(|ft| ft.tracks.iter().map(|t| t.into()).collect())
self.api_with_retry(|api| {
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>> {
self.api_with_retry(|api| api.artist_related_artists(&id))
.map(|fa| fa.artists.iter().map(|a| a.into()).collect())
pub fn artist_related_artists(&self, id: &str) -> Option<Vec<Artist>> {
self.api_with_retry(|api| api.artist_related_artists(&ArtistId::from_id(id).unwrap()))
.map(|fa| fa.iter().map(|a| a.into()).collect())
}
pub fn current_user(&self) -> Option<PrivateUser> {

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,16 +6,17 @@ use cursive::Cursive;
use crate::command::Command;
use crate::commands::CommandResult;
use crate::library::Library;
use crate::playable::Playable;
use crate::playlist::Playlist;
use crate::queue::Queue;
use crate::spotify::Spotify;
use crate::track::Track;
use crate::traits::ViewExt;
use crate::ui::listview::ListView;
pub struct PlaylistView {
playlist: Playlist,
list: ListView<Track>,
list: ListView<Playable>,
spotify: Spotify,
library: Arc<Library>,
queue: Arc<Queue>,
@@ -54,7 +55,7 @@ impl PlaylistView {
}
impl ViewWrapper for PlaylistView {
wrap_impl!(self.list: ListView<Track>);
wrap_impl!(self.list: ListView<Playable>);
}
impl ViewExt for PlaylistView {
@@ -64,7 +65,7 @@ impl ViewExt for PlaylistView {
fn title_sub(&self) -> String {
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);
format!(
"{} tracks, {}",

View File

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

View File

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