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:
committed by
GitHub
parent
a8c8a1761a
commit
96f2d88696
1485
Cargo.lock
generated
1485
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
35
src/album.rs
35
src/album.rs
@@ -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| {
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
12
src/mpris.rs
12
src/mpris.rs
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
{
|
||||
|
||||
11
src/show.rs
11
src/show.rs
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
39
src/track.rs
39
src/track.rs
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, {}",
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user