diff --git a/psst-gui/src/controller/nav.rs b/psst-gui/src/controller/nav.rs index 5a332ca9..170fcb1e 100644 --- a/psst-gui/src/controller/nav.rs +++ b/psst-gui/src/controller/nav.rs @@ -47,7 +47,7 @@ impl NavController { } } Nav::ArtistDetail(link) => { - if !data.artist_detail.top_tracks.contains(link) { + if !data.artist_detail.artist.contains(link) { ctx.submit_command(artist::LOAD_DETAIL.with(link.to_owned())); } } diff --git a/psst-gui/src/data/artist.rs b/psst-gui/src/data/artist.rs index 286b13c1..ae2f9566 100644 --- a/psst-gui/src/data/artist.rs +++ b/psst-gui/src/data/artist.rs @@ -3,13 +3,12 @@ use std::sync::Arc; use druid::{im::Vector, Data, Lens}; use serde::{Deserialize, Serialize}; -use crate::data::{Album, Cached, Image, Promise, Track}; +use crate::data::{Album, Cached, Image, Promise}; #[derive(Clone, Data, Lens)] pub struct ArtistDetail { pub artist: Promise, pub albums: Promise, - pub top_tracks: Promise, pub related_artists: Promise>, ArtistLink>, pub artist_info: Promise, } @@ -56,22 +55,6 @@ pub struct ArtistStats { pub world_rank: i64, } -#[derive(Clone, Data, Lens)] -pub struct ArtistTracks { - pub id: Arc, - pub name: Arc, - pub tracks: Vector>, -} - -impl ArtistTracks { - pub fn link(&self) -> ArtistLink { - ArtistLink { - id: self.id.clone(), - name: self.name.clone(), - } - } -} - #[derive(Clone, Debug, Data, Lens, Eq, PartialEq, Hash, Deserialize, Serialize)] pub struct ArtistLink { pub id: Arc, diff --git a/psst-gui/src/data/mod.rs b/psst-gui/src/data/mod.rs index 357bea38..b2c44d3a 100644 --- a/psst-gui/src/data/mod.rs +++ b/psst-gui/src/data/mod.rs @@ -35,7 +35,7 @@ use psst_core::{item_id::ItemId, session::SessionService}; pub use crate::data::{ album::{Album, AlbumDetail, AlbumLink, AlbumType}, artist::{ - Artist, ArtistAlbums, ArtistDetail, ArtistInfo, ArtistLink, ArtistStats, ArtistTracks, + Artist, ArtistAlbums, ArtistDetail, ArtistInfo, ArtistLink, ArtistStats, }, config::{AudioQuality, Authentication, Config, Preferences, PreferencesTab, Theme}, ctx::Ctx, @@ -153,7 +153,6 @@ impl AppState { artist_detail: ArtistDetail { artist: Promise::Empty, albums: Promise::Empty, - top_tracks: Promise::Empty, related_artists: Promise::Empty, artist_info: Promise::Empty, }, @@ -209,7 +208,6 @@ impl AppState { self.artist_detail.albums = Promise::Empty; self.artist_detail.artist = Promise::Empty; self.artist_detail.related_artists = Promise::Empty; - self.artist_detail.top_tracks = Promise::Empty; self.playlist_detail.playlist = Promise::Empty; self.playlist_detail.tracks = Promise::Empty; self.show_detail.episodes = Promise::Empty; diff --git a/psst-gui/src/data/playback.rs b/psst-gui/src/data/playback.rs index f9001b55..b0d8f127 100644 --- a/psst-gui/src/data/playback.rs +++ b/psst-gui/src/data/playback.rs @@ -6,8 +6,7 @@ use psst_core::item_id::ItemId; use serde::{Deserialize, Serialize}; use super::{ - AlbumLink, ArtistLink, Episode, Library, Nav, PlaylistLink, RecommendationsRequest, ShowLink, - Track, + AlbumLink, Episode, Library, Nav, PlaylistLink, RecommendationsRequest, ShowLink, Track, }; #[derive(Clone, Data, Lens)] @@ -145,7 +144,6 @@ pub enum PlaybackOrigin { Home, Library, Album(AlbumLink), - Artist(ArtistLink), Playlist(PlaylistLink), Show(ShowLink), Search(Arc), @@ -158,7 +156,6 @@ impl PlaybackOrigin { PlaybackOrigin::Home => Nav::Home, PlaybackOrigin::Library => Nav::SavedTracks, PlaybackOrigin::Album(link) => Nav::AlbumDetail(link.clone(), None), - PlaybackOrigin::Artist(link) => Nav::ArtistDetail(link.clone()), PlaybackOrigin::Playlist(link) => Nav::PlaylistDetail(link.clone()), PlaybackOrigin::Show(link) => Nav::ShowDetail(link.clone()), PlaybackOrigin::Search(query) => Nav::SearchResults(query.clone()), @@ -173,7 +170,6 @@ impl fmt::Display for PlaybackOrigin { PlaybackOrigin::Home => f.write_str("Home"), PlaybackOrigin::Library => f.write_str("Saved Tracks"), PlaybackOrigin::Album(link) => link.name.fmt(f), - PlaybackOrigin::Artist(link) => link.name.fmt(f), PlaybackOrigin::Playlist(link) => link.name.fmt(f), PlaybackOrigin::Show(link) => link.name.fmt(f), PlaybackOrigin::Search(query) => query.fmt(f), diff --git a/psst-gui/src/data/playlist.rs b/psst-gui/src/data/playlist.rs index faf8f5b4..01ad927f 100644 --- a/psst-gui/src/data/playlist.rs +++ b/psst-gui/src/data/playlist.rs @@ -21,7 +21,7 @@ pub struct PlaylistAddTrack { #[derive(Clone, Debug, Data, Lens, Deserialize)] pub struct PlaylistRemoveTrack { pub link: PlaylistLink, - pub track_pos: usize, + pub track_uri: Arc, } #[derive(Clone, Debug, Data, Lens, Deserialize)] @@ -32,8 +32,10 @@ pub struct Playlist { pub images: Option>, #[serde(deserialize_with = "deserialize_description")] pub description: Arc, - #[serde(rename = "tracks")] + #[serde(rename = "items")] + #[serde(alias = "tracks")] #[serde(deserialize_with = "deserialize_track_count")] + #[serde(default)] pub track_count: Option, pub owner: PublicUser, pub collaborative: bool, diff --git a/psst-gui/src/data/show.rs b/psst-gui/src/data/show.rs index 7ed4d686..071f2274 100644 --- a/psst-gui/src/data/show.rs +++ b/psst-gui/src/data/show.rs @@ -20,6 +20,7 @@ pub struct Show { pub id: Arc, pub name: Arc, pub images: Vector, + #[serde(default)] pub publisher: Arc, pub description: Arc, pub total_episodes: Option, diff --git a/psst-gui/src/data/track.rs b/psst-gui/src/data/track.rs index f1660e8d..2925b91b 100644 --- a/psst-gui/src/data/track.rs +++ b/psst-gui/src/data/track.rs @@ -24,6 +24,7 @@ pub struct Track { #[serde(skip_deserializing)] pub local_path: Option>, pub is_playable: Option, + #[serde(default)] pub popularity: Option, #[serde(skip)] pub track_pos: usize, diff --git a/psst-gui/src/data/user.rs b/psst-gui/src/data/user.rs index e9dfc34b..d2c4e1f9 100644 --- a/psst-gui/src/data/user.rs +++ b/psst-gui/src/data/user.rs @@ -6,6 +6,7 @@ use serde::Deserialize; #[derive(Clone, Data, Lens, Deserialize)] pub struct UserProfile { pub display_name: Arc, + #[serde(default)] pub email: Arc, pub id: Arc, } diff --git a/psst-gui/src/ui/artist.rs b/psst-gui/src/ui/artist.rs index bed3aee7..23bdc165 100644 --- a/psst-gui/src/ui/artist.rs +++ b/psst-gui/src/ui/artist.rs @@ -9,8 +9,8 @@ use druid::{ use crate::{ cmd, data::{ - AppState, Artist, ArtistAlbums, ArtistDetail, ArtistInfo, ArtistLink, ArtistTracks, Cached, - Ctx, Nav, WithCtx, + AppState, Artist, ArtistAlbums, ArtistDetail, ArtistInfo, ArtistLink, Cached, Ctx, Nav, + WithCtx, }, ui::utils::{stat_row, InfoLayout}, webapi::WebApi, @@ -18,7 +18,7 @@ use crate::{ }; use super::{ - album, playable, theme, track, + album, theme, utils::{self}, }; @@ -27,39 +27,10 @@ pub const LOAD_DETAIL: Selector = Selector::new("app.artist.load-det pub fn detail_widget() -> impl Widget { Flex::column() .with_child(async_artist_info().padding((theme::grid(1.0), 0.0))) - .with_child(async_top_tracks_widget()) .with_child(async_albums_widget().padding((theme::grid(1.0), 0.0))) .with_child(async_related_widget().padding((theme::grid(1.0), 0.0))) } -fn async_top_tracks_widget() -> impl Widget { - Async::new( - utils::spinner_widget, - top_tracks_widget, - utils::error_widget, - ) - .lens( - Ctx::make( - AppState::common_ctx, - AppState::artist_detail.then(ArtistDetail::top_tracks), - ) - .then(Ctx::in_promise()), - ) - .on_command_async( - LOAD_DETAIL, - |d| WebApi::global().get_artist_top_tracks(&d.id), - |_, data, d| data.artist_detail.top_tracks.defer(d), - |_, data, (d, r)| { - let r = r.map(|tracks| ArtistTracks { - id: d.id.clone(), - name: d.name.clone(), - tracks, - }); - data.artist_detail.top_tracks.update((d, r)) - }, - ) -} - fn async_albums_widget() -> impl Widget { Async::new(utils::spinner_widget, albums_widget, utils::error_widget) .lens( @@ -217,18 +188,6 @@ fn artist_info_widget() -> impl Widget> { .padding((0.0, theme::grid(1.0))) // Keep overall vertical padding } -fn top_tracks_widget() -> impl Widget> { - playable::list_widget(playable::Display { - track: track::Display { - title: true, - album: true, - popularity: true, - cover: true, - ..track::Display::empty() - }, - }) -} - fn albums_widget() -> impl Widget> { Flex::column() .cross_axis_alignment(CrossAxisAlignment::Start) diff --git a/psst-gui/src/ui/playable.rs b/psst-gui/src/ui/playable.rs index a45fa991..0f674bf8 100644 --- a/psst-gui/src/ui/playable.rs +++ b/psst-gui/src/ui/playable.rs @@ -12,7 +12,7 @@ use druid::{ use crate::{ cmd, data::{ - ArtistTracks, CommonCtx, FindQuery, MatchFindQuery, Playable, PlaybackOrigin, + CommonCtx, FindQuery, MatchFindQuery, Playable, PlaybackOrigin, PlaybackPayload, PlaylistTracks, Recommendations, SavedTracks, SearchResults, ShowEpisodes, Track, WithCtx, }, @@ -170,22 +170,6 @@ impl PlayableIter for PlaylistTracks { } } -impl PlayableIter for ArtistTracks { - fn origin(&self) -> PlaybackOrigin { - PlaybackOrigin::Artist(self.link()) - } - - fn for_each(&self, mut cb: impl FnMut(Playable, usize)) { - for (position, track) in self.tracks.iter().enumerate() { - cb(Playable::Track(track.to_owned()), position); - } - } - - fn count(&self) -> usize { - self.tracks.len() - } -} - impl PlayableIter for SavedTracks { fn origin(&self) -> PlaybackOrigin { PlaybackOrigin::Library diff --git a/psst-gui/src/ui/playback.rs b/psst-gui/src/ui/playback.rs index 4190e806..72ab4038 100644 --- a/psst-gui/src/ui/playback.rs +++ b/psst-gui/src/ui/playback.rs @@ -108,12 +108,9 @@ fn playing_item_widget() -> impl Widget { ctx.submit_command(cmd::NAVIGATE.with(now_playing.origin.to_nav())); }) .context_menu(|now_playing| match &now_playing.item { - Playable::Track(track) => track::track_menu( - track, - &now_playing.library, - &now_playing.origin, - usize::MAX, - ), + Playable::Track(track) => { + track::track_menu(track, &now_playing.library, &now_playing.origin) + } Playable::Episode(episode) => { episode::episode_menu(episode, &now_playing.library) } @@ -178,7 +175,6 @@ fn playback_origin_icon(origin: &PlaybackOrigin) -> &'static SvgIcon { PlaybackOrigin::Home => &icons::HOME, PlaybackOrigin::Library => &icons::HEART, PlaybackOrigin::Album { .. } => &icons::ALBUM, - PlaybackOrigin::Artist { .. } => &icons::ARTIST, PlaybackOrigin::Playlist { .. } => &icons::PLAYLIST, PlaybackOrigin::Show { .. } => &icons::PODCAST, PlaybackOrigin::Search { .. } => &icons::SEARCH, diff --git a/psst-gui/src/ui/playlist.rs b/psst-gui/src/ui/playlist.rs index c285c0bd..21354377 100644 --- a/psst-gui/src/ui/playlist.rs +++ b/psst-gui/src/ui/playlist.rs @@ -146,7 +146,7 @@ pub fn list_widget() -> impl Widget { }) .on_command_async( REMOVE_TRACK, - |d| WebApi::global().remove_track_from_playlist(&d.link.id, d.track_pos), + |d| WebApi::global().remove_track_from_playlist(&d.link.id, &d.track_uri), |_, data, d| { data.with_library_mut(|library| library.decrement_playlist_track_count(&d.link)) }, diff --git a/psst-gui/src/ui/search.rs b/psst-gui/src/ui/search.rs index 9ae44f4d..d44ee4d2 100644 --- a/psst-gui/src/ui/search.rs +++ b/psst-gui/src/ui/search.rs @@ -20,7 +20,7 @@ use crate::{ use super::{album, artist, playable, playlist, theme, track, utils}; const NUMBER_OF_RESULTS_PER_TOPIC: usize = 5; -const INDIVIDUAL_TOPIC_RESULTS_LIMIT: usize = 50; +const INDIVIDUAL_TOPIC_RESULTS_LIMIT: usize = 10; pub const LOAD_RESULTS: Selector<(Arc, Option)> = Selector::new("app.search.load-results"); diff --git a/psst-gui/src/ui/track.rs b/psst-gui/src/ui/track.rs index e4404ec2..bdea365d 100644 --- a/psst-gui/src/ui/track.rs +++ b/psst-gui/src/ui/track.rs @@ -232,14 +232,13 @@ fn popularity_stars(popularity: u32) -> String { } fn track_row_menu(row: &PlayRow>) -> Menu { - track_menu(&row.item, &row.ctx.library, &row.origin, row.item.track_pos) + track_menu(&row.item, &row.ctx.library, &row.origin) } pub fn track_menu( track: &Arc, library: &Library, origin: &PlaybackOrigin, - track_pos: usize, ) -> Menu { let mut menu = Menu::empty(); @@ -347,7 +346,7 @@ pub fn track_menu( ) .command(playlist::REMOVE_TRACK.with(PlaylistRemoveTrack { link: playlist.to_owned(), - track_pos, + track_uri: format!("spotify:track:{}", track.id.0.to_base62()).into(), })), ); } diff --git a/psst-gui/src/webapi/client.rs b/psst-gui/src/webapi/client.rs index 04f9364e..bb7b34f5 100644 --- a/psst-gui/src/webapi/client.rs +++ b/psst-gui/src/webapi/client.rs @@ -775,19 +775,6 @@ impl WebApi { Ok(artist_albums) } - // https://developer.spotify.com/documentation/web-api/reference/get-an-artists-top-tracks - pub fn get_artist_top_tracks(&self, id: &str) -> Result>, Error> { - #[derive(Deserialize)] - struct Tracks { - tracks: Vector>, - } - let request = - &RequestBuilder::new(format!("v1/artists/{id}/top-tracks"), Method::Get, None) - .query("market", "from_token"); - let result: Tracks = self.load(request)?; - Ok(result.tracks) - } - // https://developer.spotify.com/documentation/web-api/reference/get-an-artists-related-artists pub fn get_related_artists(&self, id: &str) -> Result>, Error> { #[derive(Clone, Data, Deserialize)] @@ -1050,15 +1037,17 @@ impl WebApi { .collect()) } - // https://developer.spotify.com/documentation/web-api/reference/save-albums-user/ + // https://developer.spotify.com/documentation/web-api/reference/save-to-library/ pub fn save_album(&self, id: &str) -> Result<(), Error> { - let request = &RequestBuilder::new("v1/me/albums", Method::Put, None).query("ids", id); + let request = &RequestBuilder::new("v1/me/library", Method::Put, None) + .set_body(Some(json!({"uris": [format!("spotify:album:{id}")]}))); self.send_empty_json(request) } - // https://developer.spotify.com/documentation/web-api/reference/remove-albums-user/ + // https://developer.spotify.com/documentation/web-api/reference/remove-from-library/ pub fn unsave_album(&self, id: &str) -> Result<(), Error> { - let request = &RequestBuilder::new("v1/me/albums", Method::Delete, None).query("ids", id); + let request = &RequestBuilder::new("v1/me/library", Method::Delete, None) + .set_body(Some(json!({"uris": [format!("spotify:album:{id}")]}))); self.send_empty_json(request) } @@ -1094,27 +1083,31 @@ impl WebApi { .collect()) } - // https://developer.spotify.com/documentation/web-api/reference/save-tracks-user/ + // https://developer.spotify.com/documentation/web-api/reference/save-to-library/ pub fn save_track(&self, id: &str) -> Result<(), Error> { - let request = &RequestBuilder::new("v1/me/tracks", Method::Put, None).query("ids", id); + let request = &RequestBuilder::new("v1/me/library", Method::Put, None) + .set_body(Some(json!({"uris": [format!("spotify:track:{id}")]}))); self.send_empty_json(request) } - // https://developer.spotify.com/documentation/web-api/reference/remove-tracks-user/ + // https://developer.spotify.com/documentation/web-api/reference/remove-from-library/ pub fn unsave_track(&self, id: &str) -> Result<(), Error> { - let request = &RequestBuilder::new("v1/me/tracks", Method::Delete, None).query("ids", id); + let request = &RequestBuilder::new("v1/me/library", Method::Delete, None) + .set_body(Some(json!({"uris": [format!("spotify:track:{id}")]}))); self.send_empty_json(request) } - // https://developer.spotify.com/documentation/web-api/reference/save-shows-user + // https://developer.spotify.com/documentation/web-api/reference/save-to-library/ pub fn save_show(&self, id: &str) -> Result<(), Error> { - let request = &RequestBuilder::new("v1/me/shows", Method::Put, None).query("ids", id); + let request = &RequestBuilder::new("v1/me/library", Method::Put, None) + .set_body(Some(json!({"uris": [format!("spotify:show:{id}")]}))); self.send_empty_json(request) } - // https://developer.spotify.com/documentation/web-api/reference/remove-shows-user + // https://developer.spotify.com/documentation/web-api/reference/remove-from-library/ pub fn unsave_show(&self, id: &str) -> Result<(), Error> { - let request = &RequestBuilder::new("v1/me/shows", Method::Delete, None).query("ids", id); + let request = &RequestBuilder::new("v1/me/library", Method::Delete, None) + .set_body(Some(json!({"uris": [format!("spotify:show:{id}")]}))); self.send_empty_json(request) } } @@ -1224,17 +1217,16 @@ impl WebApi { } pub fn follow_playlist(&self, id: &str) -> Result<(), Error> { - let request = - &RequestBuilder::new(format!("v1/playlists/{id}/followers"), Method::Put, None) - .set_body(Some(json!({"public": false}))); - self.request(request)?; + let request = &RequestBuilder::new("v1/me/library", Method::Put, None) + .set_body(Some(json!({"uris": [format!("spotify:playlist:{id}")]}))); + self.send_empty_json(request)?; Ok(()) } pub fn unfollow_playlist(&self, id: &str) -> Result<(), Error> { - let request = - &RequestBuilder::new(format!("v1/playlists/{id}/followers"), Method::Delete, None); - self.request(request)?; + let request = &RequestBuilder::new("v1/me/library", Method::Delete, None) + .set_body(Some(json!({"uris": [format!("spotify:playlist:{id}")]}))); + self.send_empty_json(request)?; Ok(()) } @@ -1245,11 +1237,14 @@ impl WebApi { Ok(result) } - // https://developer.spotify.com/documentation/web-api/reference/get-playlists-tracks + // https://developer.spotify.com/documentation/web-api/reference/get-playlist-items pub fn get_playlist_tracks(&self, id: &str) -> Result>, Error> { #[derive(Clone, Deserialize)] struct PlaylistItem { - track: OptionalTrack, + #[serde(default)] + item: Option, + #[serde(default)] + track: Option, } // Spotify API likes to return _really_ bogus data for local tracks. Much better @@ -1262,7 +1257,7 @@ impl WebApi { Json(serde_json::Value), } - let request = &RequestBuilder::new(format!("v1/playlists/{id}/tracks"), Method::Get, None) + let request = &RequestBuilder::new(format!("v1/playlists/{id}/items"), Method::Get, None) .query("marker", "from_token") .query("additional_types", "track"); @@ -1274,9 +1269,13 @@ impl WebApi { .into_iter() .enumerate() .filter_map(|(index, item)| { - let mut track = match item.track { - OptionalTrack::Track(track) => track, - OptionalTrack::Json(json) => local_track_manager.find_local_track(json)?, + let track_source = item.item.or(item.track); + let mut track = match track_source { + Some(OptionalTrack::Track(track)) => track, + Some(OptionalTrack::Json(json)) => { + local_track_manager.find_local_track(json)? + } + None => return None, }; Arc::make_mut(&mut track).track_pos = index; Some(track) @@ -1284,36 +1283,35 @@ impl WebApi { .collect()) } + // https://developer.spotify.com/documentation/web-api/reference/change-playlist-details pub fn change_playlist_details(&self, id: &str, name: &str) -> Result<(), Error> { - let request = &RequestBuilder::new(format!("v1/playlists/{id}/tracks"), Method::Get, None) + let request = &RequestBuilder::new(format!("v1/playlists/{id}"), Method::Put, None) .set_body(Some(json!({ "name": name }))); - self.request(request)?; + self.send_empty_json(request)?; Ok(()) } - // https://developer.spotify.com/documentation/web-api/reference/add-tracks-to-playlist + // https://developer.spotify.com/documentation/web-api/reference/add-items-to-playlist pub fn add_track_to_playlist(&self, playlist_id: &str, track_uri: &str) -> Result<(), Error> { let request = &RequestBuilder::new( - format!("v1/playlists/{playlist_id}/tracks"), + format!("v1/playlists/{playlist_id}/items"), Method::Post, - None, - ) - .query("uris", track_uri); + Some(json!({"uris": [track_uri]})), + ); self.request(request).map(|_| ()) } - // https://developer.spotify.com/documentation/web-api/reference/remove-tracks-playlist + // https://developer.spotify.com/documentation/web-api/reference/remove-playlist-items pub fn remove_track_from_playlist( &self, playlist_id: &str, - track_pos: usize, + track_uri: &str, ) -> Result<(), Error> { let request = &RequestBuilder::new( - format!("v1/playlists/{playlist_id}/tracks"), + format!("v1/playlists/{playlist_id}/items"), Method::Delete, - None, - ) - .set_body(Some(json!({ "positions": [track_pos] }))); + Some(json!({ "items": [{ "uri": track_uri }] })), + ); self.request(request).map(|_| ()) } }