Repository: aome510/spotify-player (language: rust)

Issue:
Unable to search or lookup artist songs
**Describe the bug**
Whenever I try to search a song or look up an artist's playlist the player gets stuck, from the logs I can see that the fetch request seems to be failing.

**To Reproduce**
Install spotify-player
Authenticate
Try to search a song title or artist

**Expected behaviour**
View the searched songs

**Log and backtrace**
2024-11-27T21:43:17.059424Z  INFO client_request{request=GetContext(Artist(ArtistId("2d0hyoQ5ynDBnkvAbJKORj")))}: spotify_player::client: Get artist context: spotify:artist:2d0hyoQ5ynDBnkvAbJKORj
2024-11-27T21:43:17.326311Z ERROR client_request{request=GetContext(Artist(ArtistId("2d0hyoQ5ynDBnkvAbJKORj")))}: spotify_player::client::handlers: Failed to handle client request: get related artists: http error: status code 404 Not Found

**Screenshots**
N/A

**Environment**

- OS: Arch Linux
- Application version: 0.20.1
- Application features: pulseaudio-backend,streaming,image,notify

**Additional context**
N/A



Discussion / hints:
Something similar also happens when I start the application, nothing shows in the Playback and from the log I can see this:

2024-11-27T21:53:10.385492Z ERROR librespot_connect::spirc: ContextError: Error { kind: NotFound, error: StatusCode(404) }    
2024-11-27T21:53:10.385518Z  INFO librespot_connect::spirc: No more tracks left in queue    
2024-11-27T21:53:10.453908Z ERROR librespot_connect::spirc: ContextError: Error { kind: NotFound, error: StatusCode(404) }    

Same issues, this happens when searching
```
2024-11-27T23:31:48.533617Z ERROR client_request{request=Search("blah blah")}: spotify_player::client::handlers: Failed to handle client request: json parse error: invalid type: null, expected struct SimplifiedPlaylist at line 1 column 8664: invalid type: null, expected struct SimplifiedPlaylist at line 1 column 8664

``` 
User playlists still work and appear but new playlists that were made recently aren't appended due to 
```
2024-11-27T23:31:35.293265Z ERROR client_request{request=GetUserPlaylists}: spotify_player::client::handlers: Failed to handle client request: invalid type: null, expected struct SimplifiedPlaylist at line 1 column 4901
```
I have the exact same issue...

Might this be related to recent the changes in the web API?

https://developer.spotify.com/blog/2024-11-27-changes-to-the-web-api
I also gave the exact same issue

Arch, spotify-player-pipewire from aur
Can confirm that I also got many 404 errors. Might be related to the new Spotify API changes. Spotify always breaks their APIs 🤦 . I'll take a look over the weekend to see how to fix/mitigate this issue.
For me, it works fine if I use the default client_id. 
If I use my own one, it's broken. 
Seems consistent with the [changes introduced in the api](https://developer.spotify.com/blog/2024-11-27-changes-to-the-web-api), as my own app is still "in development" and as such is considered a "new" app that has no access to the relevant web API access points.
So I have to choose now: spotify connect or searching...
one thing I just found out is that this 404 error only happens if you use your own `client_id`, i.e. to set up [Spotify Connect](https://github.com/aome510/spotify-player?tab=readme-ov-file#spotify-connect). In other words, to work-around this issue, you can unset `client_id` in your `app.toml` to use the default Spotify `client_id`

I feel like this can be worked around further by implementing a separate client just for Spotify Connect, which I will play around over the weekend
I can confirm that using the default Spotify client_id has fixed the issue for me.

Fix patch:
diff --git a/spotify_player/src/client/mod.rs b/spotify_player/src/client/mod.rs
index d40c0f14..810652b4 100644
--- a/spotify_player/src/client/mod.rs
+++ b/spotify_player/src/client/mod.rs
@@ -1,9 +1,6 @@
 use std::ops::Deref;
 use std::{borrow::Cow, collections::HashMap, sync::Arc};
 
-#[cfg(feature = "streaming")]
-use crate::state::Mutex;
-use crate::state::Show;
 use crate::{auth, config};
 use crate::{
     auth::AuthConfig,
@@ -11,7 +8,7 @@ use crate::{
         rspotify_model, store_data_into_file_cache, Album, AlbumId, Artist, ArtistId, Category,
         Context, ContextId, Device, FileCacheKey, Item, ItemId, MemoryCaches, Playback,
         PlaybackMetadata, Playlist, PlaylistFolderItem, PlaylistId, SearchResults, SharedState,
-        ShowId, Track, TrackId, UserId, TTL_CACHE_DURATION, USER_LIKED_TRACKS_ID,
+        Show, ShowId, Track, TrackId, UserId, TTL_CACHE_DURATION, USER_LIKED_TRACKS_ID,
         USER_RECENTLY_PLAYED_TRACKS_ID, USER_TOP_TRACKS_ID,
     },
 };
@@ -20,6 +17,10 @@ use std::io::Write;
 
 use anyhow::Context as _;
 use anyhow::Result;
+
+#[cfg(feature = "streaming")]
+use parking_lot::Mutex;
+
 use reqwest::StatusCode;
 use rspotify::model::{AdditionalType, CurrentPlaybackContext};
 use rspotify::{
@@ -212,7 +213,7 @@ impl Client {
                 // because `TransferPlayback` doesn't require an active playback
                 self.transfer_playback(&device_id, Some(force_play)).await?;
                 tracing::info!("Transferred playback to device with id={}", device_id);
-                return Ok(playback);
+                return Ok(None);
             }
             PlayerRequest::StartPlayback(p, shuffle) => {
                 // Set the playback's shuffle state if specified in the request
@@ -226,7 +227,7 @@ impl Client {
                 if let Some(ref playback) = playback {
                     self.shuffle(playback.shuffle_state, device_id).await?;
                 }
-                return Ok(playback);
+                return Ok(None);
             }
             _ => {}
         }
@@ -366,7 +367,7 @@ impl Client {
                 self.retrieve_current_playback(state, true).await?;
             }
             ClientRequest::GetDevices => {
-                let devices = self.device().await?;
+                let devices = self.available_devices().await?;
                 state.player.write().devices = devices
                     .into_iter()
                     .filter_map(Device::try_from_device)
@@ -610,6 +611,19 @@ impl Client {
         Ok(())
     }
 
+    /// Get user available devices
+    // This is a custom API to replace `rspotify::device` API to support Spotify Connect feature
+    pub async fn available_devices(&self) -> Result<Vec<rspotify::model::Device>> {
+        Ok(self
+            .http_get::<rspotify::model::DevicePayload>(
+                &format!("{SPOTIFY_API_ENDPOINT}/me/player/devices"),
+                &Query::new(),
+                true,
+            )
+            .await?
+            .devices)
+    }
+
     pub fn update_playback(&self, state: &SharedState) {
         // After handling a request changing the player's playback,
         // update the playback state by making multiple get-playback requests.
@@ -652,7 +666,8 @@ impl Client {
 
     /// Find an available device. If found, return the device's ID.
     async fn find_available_device(&self) -> Result<Option<String>> {
-        let devices = self.device().await?.into_iter().collect::<Vec<_>>();
+        let devices = self.available_devices().await?;
+
         if devices.is_empty() {
             tracing::warn!("No device found. Please make sure you already setup Spotify Connect \
                             support as described in https://github.com/aome510/spotify-player#spotify-connect.");
@@ -754,6 +769,7 @@ impl Client {
             .http_get::<Page<SimplifiedPlaylist>>(
                 &format!("{SPOTIFY_API_ENDPOINT}/me/playlists"),
                 &Query::from([("limit", "50")]),
+                false,
             )
             .await?;
         // let first_page = self
@@ -780,7 +796,7 @@ impl Client {
         let mut maybe_next = first_page.next;
         while let Some(url) = maybe_next {
             let mut next_page = self
-                .http_get::<rspotify_model::CursorPageFullArtists>(&url, &Query::new())
+                .http_get::<rspotify_model::CursorPageFullArtists>(&url, &Query::new(), false)
                 .await?
                 .artists;
             artists.append(&mut next_page.items);
@@ -1272,6 +1288,7 @@ impl Client {
             .http_get::<FullPlaylist>(
                 &format!("{SPOTIFY_API_ENDPOINT}/playlists/{}", playlist_id.id()),
                 &market_query(),
+                false,
             )
             .await?;
 
@@ -1392,7 +1409,12 @@ impl Client {
     }
 
     /// Make a GET HTTP request to the Spotify server
-    async fn http_get<T>(&self, url: &str, payload: &Query<'_>) -> Result<T>
+    async fn http_get<T>(
+        &self,
+        url: &str,
+        payload: &Query<'_>,
+        use_user_client_id: bool,
+    ) -> Result<T>
     where
         T: serde::de::DeserializeOwned,
     {
@@ -1408,7 +1430,12 @@ impl Client {
                 .replace("\"name\":null", "\"name\":\"\"")
         }
 
-        let access_token = self.access_token().await?;
+        let access_token = if use_user_client_id {
+            self.access_token_from_user_client_id().await
+        } else {
+            self.access_token().await
+        }
+        .context("get access token")?;
 
         tracing::debug!("{access_token} {url}");
 
@@ -1448,7 +1475,7 @@ impl Client {
 
         while let Some(url) = maybe_next {
             let mut next_page = self
-                .http_get::<rspotify_model::Page<T>>(&url, payload)
+                .http_get::<rspotify_model::Page<T>>(&url, payload, false)
                 .await?;
             if next_page.items.is_empty() {
                 break;
@@ -1471,7 +1498,7 @@ impl Client {
         let mut maybe_next = first_page.next;
         while let Some(url) = maybe_next {
             let mut next_page = self
-                .http_get::<rspotify_model::CursorBasedPage<T>>(&url, &Query::new())
+                .http_get::<rspotify_model::CursorBasedPage<T>>(&url, &Query::new(), false)
                 .await?;
             items.append(&mut next_page.items);
             maybe_next = next_page.next;
@@ -1492,7 +1519,6 @@ impl Client {
         let new_playback = {
             // update the playback state
             let playback = self.current_playback2().await?;
-            log::info!("current_playback: {playback:?}");
             let mut player = state.player.write();
 
             let prev_item = player.currently_playing();
diff --git a/spotify_player/src/client/spotify.rs b/spotify_player/src/client/spotify.rs
index ddc64575..1dd244dd 100644
--- a/spotify_player/src/client/spotify.rs
+++ b/spotify_player/src/client/spotify.rs
@@ -9,7 +9,7 @@ use rspotify::{
 };
 use std::{fmt, sync::Arc};
 
-use crate::{config, token};
+use crate::{auth::SPOTIFY_CLIENT_ID, config, token};
 
 #[derive(Clone, Default)]
 /// A Spotify client to interact with Spotify API server
@@ -19,7 +19,12 @@ pub struct Spotify {
     config: Config,
     token: Arc<Mutex<Option<Token>>>,
     http: HttpClient,
-    client_id: String,
+    /// User-provided client ID
+    ///
+    /// This client ID is mainly used to support Spotify Connect feature
+    /// because Spotify client ID doesn't have access to user available devices
+    /// (https://developer.spotify.com/documentation/web-api/reference/get-a-users-available-devices)
+    user_client_id: String,
     pub(crate) session: Arc<tokio::sync::Mutex<Option<Session>>>,
 }
 
@@ -47,9 +52,7 @@ impl Spotify {
             },
             token: Arc::new(Mutex::new(None)),
             http: HttpClient::default(),
-            // Spotify client uses different `client_id` from Spotify session (`auth::SPOTIFY_CLIENT_ID`)
-            // to support user-provided `client_id`, which is required for Spotify Connect feature
-            client_id: config::get_config()
+            user_client_id: config::get_config()
                 .app_config
                 .get_client_id()
                 .expect("get client_id"),
@@ -84,6 +87,14 @@ impl Spotify {
             )),
         }
     }
+
+    /// Get a Spotify access token based on a user-provided client ID
+    // TODO: implement caching
+    pub async fn access_token_from_user_client_id(&self) -> Result<String> {
+        let session = self.session().await;
+        let token = token::get_token_librespot(&session, &self.user_client_id).await?;
+        Ok(token.access_token)
+    }
 }
 
 // TODO: remove the below uses of `maybe_async` crate once
@@ -116,7 +127,7 @@ impl BaseClient for Spotify {
             return Ok(old_token);
         }
 
-        match token::get_token(&session, &self.client_id).await {
+        match token::get_token_rspotify(&session, SPOTIFY_CLIENT_ID).await {
             Ok(token) => Ok(Some(token)),
             Err(err) => {
                 tracing::error!("Failed to get a new token: {err:#}");
diff --git a/spotify_player/src/token.rs b/spotify_player/src/token.rs
index d2874217..e5b0fe44 100644
--- a/spotify_player/src/token.rs
+++ b/spotify_player/src/token.rs
@@ -25,7 +25,7 @@ const SCOPES: [&str; 15] = [
     "user-library-modify",
 ];
 
-async fn retrieve_token(
+pub async fn get_token_librespot(
     session: &Session,
     client_id: &str,
 ) -> Result<librespot_core::token::Token> {
@@ -46,10 +46,10 @@ async fn retrieve_token(
     Ok(token)
 }
 
-pub async fn get_token(session: &Session, client_id: &str) -> Result<rspotify::Token> {
+pub async fn get_token_rspotify(session: &Session, client_id: &str) -> Result<rspotify::Token> {
     tracing::info!("Getting a new authentication token...");
 
-    let fut = retrieve_token(session, client_id);
+    let fut = get_token_librespot(session, client_id);
     let token =
         match tokio::time::timeout(std::time::Duration::from_secs(TIMEOUT_IN_SECS), fut).await {
             Ok(Ok(token)) => token,
diff --git a/spotify_player/src/ui/playback.rs b/spotify_player/src/ui/playback.rs
index 6095b4b3..d636f112 100644
--- a/spotify_player/src/ui/playback.rs
+++ b/spotify_player/src/ui/playback.rs
@@ -141,32 +141,33 @@ pub fn render_playback_window(
                 duration,
             );
             render_playback_progress_bar(frame, ui, progress, duration, progress_bar_rect);
+            return other_rect;
         }
-    } else {
-        // Previously rendered image can result in a weird rendering text,
-        // clear the previous widget's area before rendering the text.
-        #[cfg(feature = "image")]
-        {
-            if ui.last_cover_image_render_info.rendered {
-                clear_area(
-                    frame,
-                    ui.last_cover_image_render_info.render_area,
-                    &ui.theme,
-                );
-                ui.last_cover_image_render_info = ImageRenderInfo::default();
-            }
+    }
+
+    // Previously rendered image can result in a weird rendering text,
+    // clear the previous widget's area before rendering the text.
+    #[cfg(feature = "image")]
+    {
+        if ui.last_cover_image_render_info.rendered {
+            clear_area(
+                frame,
+                ui.last_cover_image_render_info.render_area,
+                &ui.theme,
+            );
+            ui.last_cover_image_render_info = ImageRenderInfo::default();
         }
+    }
 
-        frame.render_widget(
+    frame.render_widget(
             Paragraph::new(
-                "No playback found.\n \
-                 Please make sure there is a running Spotify device and try to connect to one using the `SwitchDevice` command.\n \
+                "No playback found. Please start a new playback.\n \
+                 Make sure there is a running Spotify device and try to connect to one using the `SwitchDevice` command.\n \
                  You may also need to set up Spotify Connect to see available devices as in https://github.com/aome510/spotify-player#spotify-connect."
             )
             .wrap(Wrap { trim: true }),
             rect,
         );
-    };
 
     other_rect
 }


Test patch: