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

Issue:
Requires sudo
**Describe the bug**
I wouldn't expect a Spotify player to require elevated privileges just to run, so I think this may be a strange bug.

When running the player without `sudo` I get:
```
Error: initialize new Spotify session

Caused by:
    0: new streaming connection
    1: intialize spirc
    2: Permission denied { Login failed with reason: Bad credentials }
```

So I ran `spotify_player authenticate` and got no output. I then thought that I might need elevated privileges just to authenticate so ran `sudo spotify_player authenticate` and got a link to authenticate with. Then running `spotify_player` again I got the same:
```
Error: initialize new Spotify session

Caused by:
    0: new streaming connection
    1: intialize spirc
    2: Permission denied { Login failed with reason: Bad credentials }
```

So I resorted to `sudo spotify_player` which gave me a new link to authenticate with and the finally I got through to the player, however I still can't without `sudo`. 

**To Reproduce**
Install through `yay` on Arch Linux and attempt to authenticate.

**Expected behaviour**
`spotify_player authenticate` should give me a link to authenticate with and `spotify_player` should take me to the player, without requiring elevated privileges.

**Log and backtrace**
```
2024-10-31T14:48:25.628955Z  INFO spotify_player: Configurations: Configs { app_config: AppConfig { theme: "dracula", client_id: "REDACTED", client_id_command: None, client_port: 8080, player_event_hook_command: None, playback_format: "{track} • {artists}\n{album}\n{metadata}", notify_format: NotifyFormat { summary: "{track} • {artists}", body: "{album}" }, notify_timeout_in_secs: 0, tracks_playback_limit: 50, proxy: None, ap_port: None, app_refresh_duration_in_ms: 32, playback_refresh_duration_in_ms: 0, page_size_in_rows: 20, play_icon: "▶", pause_icon: "▌▌", liked_icon: "♥", border_type: Plain, progress_bar_type: Rectangle, layout: LayoutConfig { library: LibraryLayoutConfig { playlist_percent: 40, album_percent: 40 }, playback_window_position: Top, playback_window_height: 6 }, cover_img_length: 9, cover_img_width: 5, cover_img_scale: 1.0, enable_media_control: true, enable_streaming: Always, enable_notify: true, enable_cover_image_cache: true, default_device: "spotify-player", device: DeviceConfig { name: "spotify-player", device_type: "speaker", volume: 70, bitrate: 320, audio_cache: false, normalization: false, autoplay: false }, notify_streaming_only: false, seek_duration_secs: 5 }, keymap_config: KeymapConfig { keymaps: [Keymap { key_sequence: KeySequence { keys: [None(Char('n'))] }, command: NextTrack }, Keymap { key_sequence: KeySequence { keys: [None(Char('p'))] }, command: PreviousTrack }, Keymap { key_sequence: KeySequence { keys: [None(Char('.'))] }, command: PlayRandom }, Keymap { key_sequence: KeySequence { keys: [None(Char(' '))] }, command: ResumePause }, Keymap { key_sequence: KeySequence { keys: [Ctrl(Char('r'))] }, command: Repeat }, Keymap { key_sequence: KeySequence { keys: [Alt(Char('r'))] }, command: ToggleFakeTrackRepeatMode }, Keymap { key_sequence: KeySequence { keys: [Ctrl(Char('s'))] }, command: Shuffle }, Keymap { key_sequence: KeySequence { keys: [None(Char('+'))] }, command: VolumeUp }, Keymap { key_sequence: KeySequence { keys: [None(Char('-'))] }, command: VolumeDown }, Keymap { key_sequence: KeySequence { keys: [None(Char('_'))] }, command: Mute }, Keymap { key_sequence: KeySequence { keys: [None(Char('>'))] }, command: SeekForward }, Keymap { key_sequence: KeySequence { keys: [None(Char('<'))] }, command: SeekBackward }, Keymap { key_sequence: KeySequence { keys: [None(Enter)] }, command: ChooseSelected }, Keymap { key_sequence: KeySequence { keys: [None(Char('r'))] }, command: RefreshPlayback }, Keymap { key_sequence: KeySequence { keys: [None(Char('/'))] }, command: Search }, Keymap { key_sequence: KeySequence { keys: [None(Char('z'))] }, command: Queue }, Keymap { key_sequence: KeySequence { keys: [Ctrl(Char('z'))] }, command: AddSelectedItemToQueue }, Keymap { key_sequence: KeySequence { keys: [None(Char('Z'))] }, command: AddSelectedItemToQueue }, Keymap { key_sequence: KeySequence { keys: [Ctrl(Char(' '))] }, command: ShowActionsOnSelectedItem }, Keymap { key_sequence: KeySequence { keys: [None(Char('g')), None(Char('a'))] }, command: ShowActionsOnSelectedItem }, Keymap { key_sequence: KeySequence { keys: [None(Char('a'))] }, command: ShowActionsOnCurrentTrack }, Keymap { key_sequence: KeySequence { keys: [None(Char('R'))] }, command: RestartIntegratedClient }, Keymap { key_sequence: KeySequence { keys: [None(Tab)] }, command: FocusNextWindow }, Keymap { key_sequence: KeySequence { keys: [None(BackTab)] }, command: FocusPreviousWindow }, Keymap { key_sequence: KeySequence { keys: [None(Char('T'))] }, command: SwitchTheme }, Keymap { key_sequence: KeySequence { keys: [None(Char('D'))] }, command: SwitchDevice }, Keymap { key_sequence: KeySequence { keys: [None(Char('u')), None(Char('p'))] }, command: BrowseUserPlaylists }, Keymap { key_sequence: KeySequence { keys: [None(Char('u')), None(Char('a'))] }, command: BrowseUserFollowedArtists }, Keymap { key_sequence: KeySequence { keys: [None(Char('u')), None(Char('A'))] }, command: BrowseUserSavedAlbums }, Keymap { key_sequence: KeySequence { keys: [None(Char('g')), None(Char(' '))] }, command: CurrentlyPlayingContextPage }, Keymap { key_sequence: KeySequence { keys: [None(Char('g')), None(Char('t'))] }, command: TopTrackPage }, Keymap { key_sequence: KeySequence { keys: [None(Char('g')), None(Char('r'))] }, command: RecentlyPlayedTrackPage }, Keymap { key_sequence: KeySequence { keys: [None(Char('g')), None(Char('y'))] }, command: LikedTrackPage }, Keymap { key_sequence: KeySequence { keys: [None(Char('g')), None(Char('L'))] }, command: LyricPage }, Keymap { key_sequence: KeySequence { keys: [None(Char('l'))] }, command: LyricPage }, Keymap { key_sequence: KeySequence { keys: [None(Char('g')), None(Char('l'))] }, command: LibraryPage }, Keymap { key_sequence: KeySequence { keys: [None(Char('g')), None(Char('s'))] }, command: SearchPage }, Keymap { key_sequence: KeySequence { keys: [None(Char('g')), None(Char('b'))] }, command: BrowsePage }, Keymap { key_sequence: KeySequence { keys: [None(Backspace)] }, command: PreviousPage }, Keymap { key_sequence: KeySequence { keys: [Ctrl(Char('q'))] }, command: PreviousPage }, Keymap { key_sequence: KeySequence { keys: [None(Char('O'))] }, command: OpenSpotifyLinkFromClipboard }, Keymap { key_sequence: KeySequence { keys: [None(Char('?'))] }, command: OpenCommandHelp }, Keymap { key_sequence: KeySequence { keys: [Ctrl(Char('h'))] }, command: OpenCommandHelp }, Keymap { key_sequence: KeySequence { keys: [None(Char('q'))] }, command: Quit }, Keymap { key_sequence: KeySequence { keys: [Ctrl(Char('c'))] }, command: Quit }, Keymap { key_sequence: KeySequence { keys: [None(Esc)] }, command: ClosePopup }, Keymap { key_sequence: KeySequence { keys: [None(Char('j'))] }, command: SelectNextOrScrollDown }, Keymap { key_sequence: KeySequence { keys: [Ctrl(Char('n'))] }, command: SelectNextOrScrollDown }, Keymap { key_sequence: KeySequence { keys: [None(Down)] }, command: SelectNextOrScrollDown }, Keymap { key_sequence: KeySequence { keys: [None(Char('k'))] }, command: SelectPreviousOrScrollUp }, Keymap { key_sequence: KeySequence { keys: [Ctrl(Char('p'))] }, command: SelectPreviousOrScrollUp }, Keymap { key_sequence: KeySequence { keys: [None(Up)] }, command: SelectPreviousOrScrollUp }, Keymap { key_sequence: KeySequence { keys: [None(PageUp)] }, command: PageSelectPreviousOrScrollUp }, Keymap { key_sequence: KeySequence { keys: [Ctrl(Char('b'))] }, command: PageSelectPreviousOrScrollUp }, Keymap { key_sequence: KeySequence { keys: [None(PageDown)] }, command: PageSelectNextOrScrollDown }, Keymap { key_sequence: KeySequence { keys: [Ctrl(Char('f'))] }, command: PageSelectNextOrScrollDown }, Keymap { key_sequence: KeySequence { keys: [None(Char('g')), None(Char('g'))] }, command: SelectFirstOrScrollToTop }, Keymap { key_sequence: KeySequence { keys: [None(Home)] }, command: SelectFirstOrScrollToTop }, Keymap { key_sequence: KeySequence { keys: [None(Char('G'))] }, command: SelectLastOrScrollToBottom }, Keymap { key_sequence: KeySequence { keys: [None(End)] }, command: SelectLastOrScrollToBottom }, Keymap { key_sequence: KeySequence { keys: [None(Char('s')), None(Char('t'))] }, command: SortTrackByTitle }, Keymap { key_sequence: KeySequence { keys: [None(Char('s')), None(Char('a'))] }, command: SortTrackByArtists }, Keymap { key_sequence: KeySequence { keys: [None(Char('s')), None(Char('A'))] }, command: SortTrackByAlbum }, Keymap { key_sequence: KeySequence { keys: [None(Char('s')), None(Char('d'))] }, command: SortTrackByDuration }, Keymap { key_sequence: KeySequence { keys: [None(Char('s')), None(Char('D'))] }, command: SortTrackByAddedDate }, Keymap { key_sequence: KeySequence { keys: [None(Char('s')), None(Char('r'))] }, command: ReverseTrackOrder }, Keymap { key_sequence: KeySequence { keys: [Ctrl(Char('k'))] }, command: MovePlaylistItemUp }, Keymap { key_sequence: KeySequence { keys: [Ctrl(Char('j'))] }, command: MovePlaylistItemDown }, Keymap { key_sequence: KeySequence { keys: [None(Char('N'))] }, command: CreatePlaylist }, Keymap { key_sequence: KeySequence { keys: [None(Char('g')), None(Char('c'))] }, command: JumpToCurrentTrackInContext }], actions: [] }, theme_config: ThemeConfig { themes: [Theme { name: "default", palette: Palette { background: None, foreground: None, black: Color { color: Black }, blue: Color { color: Blue }, cyan: Color { color: Cyan }, green: Color { color: Green }, magenta: Color { color: Magenta }, red: Color { color: Red }, white: Color { color: Gray }, yellow: Color { color: Yellow }, bright_black: Color { color: DarkGray }, bright_white: Color { color: White }, bright_red: Color { color: LightRed }, bright_magenta: Color { color: LightMagenta }, bright_green: Color { color: LightGreen }, bright_cyan: Color { color: LightCyan }, bright_blue: Color { color: LightBlue }, bright_yellow: Color { color: LightYellow } }, component_style: ComponentStyle { block_title: None, border: None, playback_status: None, playback_track: None, playback_artists: None, playback_album: None, playback_metadata: None, playback_progress_bar: None, playback_progress_bar_unfilled: None, current_playing: None, page_desc: None, playlist_desc: None, table_header: None, selection: None, secondary_row: None, like: None } }] }, cache_folder: "/home/pete/.cache/spotify-player" }
2024-10-31T14:48:25.629169Z  INFO spotify_player::state::data: Loading Playlists data from /home/pete/.cache/spotify-player/Playlists_cache.json...
2024-10-31T14:48:25.629211Z ERROR spotify_player::state::data: Failed to load Playlists data: unknown variant `id`, expected `Playlist` or `Folder` at line 1 column 6
2024-10-31T14:48:25.629228Z  INFO spotify_player::state::data: Loading FollowedArtists data from /home/pete/.cache/spotify-player/FollowedArtists_cache.json...
2024-10-31T14:48:25.629311Z  INFO spotify_player::state::data: Successfully loaded FollowedArtists data!
2024-10-31T14:48:25.629316Z  INFO spotify_player::state::data: Loading SavedAlbums data from /home/pete/.cache/spotify-player/SavedAlbums_cache.json...
2024-10-31T14:48:25.629481Z  INFO spotify_player::state::data: Successfully loaded SavedAlbums data!
2024-10-31T14:48:25.629486Z  INFO spotify_player::state::data: Loading SavedTracks data from /home/pete/.cache/spotify-player/SavedTracks_cache.json...
2024-10-31T14:48:25.629826Z  INFO spotify_player::state::data: Successfully loaded SavedTracks data!
2024-10-31T14:48:25.665234Z  INFO spotify_player::auth: Using cached credentials
2024-10-31T14:48:25.665274Z  INFO spotify_player::streaming: Application's connect configurations: ConnectConfig { name: "spotify-player", device_type: Speaker, is_group: false, initial_volume: Some(45875), has_volume_ctrl: true }
2024-10-31T14:48:25.665302Z  INFO librespot_playback::mixer::softmixer: Mixing with softvol and volume control: Log(60.0)    
2024-10-31T14:48:25.665342Z  INFO spotify_player::streaming: Initializing a new integrated player with device_id=REDACTED
2024-10-31T14:48:25.665461Z  INFO spotify_player::streaming: Starting an integrated Spotify player using librespot's spirc protocol
2024-10-31T14:48:25.665688Z  INFO librespot_playback::convert: Converting with ditherer: tpdf    
2024-10-31T14:48:25.665726Z  INFO librespot_playback::audio_backend::pulseaudio: Using PulseAudioSink with format: S16    
2024-10-31T14:48:25.753807Z  INFO librespot_core::session: Connecting to AP "ap-gew1.spotify.com:4070"   
```

**Screenshots**
![image](https://github.com/user-attachments/assets/dabc222f-8120-4e84-a5c0-d2051aa8a317)

**Environment**

- OS: Arch Linux
- Application version: spotify_player 0.20.0
- Application features: default features for the AUR install.



Discussion / hints:
Found the cause of the issue. When running `spotify_player authenticate`, there is no attempt to overwrite the old credentials to re-authenticate and so it just exits, found I had an old `credentials.json` in the cache. It might be helpful to attempt to re-authenticate if bad credentials are found.

Fix patch:
diff --git a/docs/config.md b/docs/config.md
index 70aa7bd3..0aaeec09 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -26,6 +26,7 @@ All configuration files should be placed inside the application's configuration
 | --------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------- |
 | `client_id`                       | the Spotify client's ID                                                                  | `65b708073fc0480ea92a077233ca87bd`                      |
 | `client_id_command`               | a shell command that prints the Spotify client ID to stdout (overrides `client_id`)      | `None`                                                  |
+| `login_redirect_uri`              | the redirect URI for authenticating the application                                      | `http://127.0.0.1:8989/login`                           |
 | `client_port`                     | the port that the application's client is running on to handle CLI commands              | `8080`                                                  |
 | `tracks_playback_limit`           | the limit for the number of tracks played in a **tracks** playback                       | `50`                                                    |
 | `playback_format`                 | the format of the text in the playback's window                                          | `{status} {track} • {artists}\n{album}\n{metadata}`     |
diff --git a/examples/app.toml b/examples/app.toml
index 7c055ae7..2818b38f 100644
--- a/examples/app.toml
+++ b/examples/app.toml
@@ -1,5 +1,6 @@
 theme = "default"
 client_id = "65b708073fc0480ea92a077233ca87bd"
+login_redirect_uri = "http://127.0.0.1:8989/login"
 client_port = 8080
 tracks_playback_limit = 50
 playback_format = "{status} {track} • {artists}\n{album}\n{metadata}"
diff --git a/spotify_player/src/auth.rs b/spotify_player/src/auth.rs
index 53c7c5a4..b7598f00 100644
--- a/spotify_player/src/auth.rs
+++ b/spotify_player/src/auth.rs
@@ -5,7 +5,6 @@ use librespot_oauth::get_access_token;
 use crate::config;
 
 pub const SPOTIFY_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd";
-const CLIENT_REDIRECT_URI: &str = "http://127.0.0.1:8989/login";
 // based on https://github.com/librespot-org/librespot/blob/f96f36c064795011f9fee912291eecb1aa46fff6/src/main.rs#L173
 const OAUTH_SCOPES: &[&str] = &[
     "app-remote-control",
@@ -40,6 +39,7 @@ const OAUTH_SCOPES: &[&str] = &[
 pub struct AuthConfig {
     pub cache: Cache,
     pub session_config: SessionConfig,
+    pub login_redirect_uri: String,
 }
 
 impl Default for AuthConfig {
@@ -47,6 +47,7 @@ impl Default for AuthConfig {
         AuthConfig {
             cache: Cache::new(None::<String>, None, None, None).unwrap(),
             session_config: SessionConfig::default(),
+            login_redirect_uri: "http://127.0.0.1:8989/login".to_string(),
         }
     }
 }
@@ -74,20 +75,36 @@ impl AuthConfig {
         Ok(AuthConfig {
             cache,
             session_config: configs.app_config.session_config()?,
+            login_redirect_uri: configs.app_config.login_redirect_uri.clone(),
         })
     }
 }
 
 /// Get Spotify credentials to authenticate the application
-pub async fn get_creds(auth_config: &AuthConfig, reauth: bool) -> Result<Credentials> {
-    Ok(match auth_config.cache.credentials() {
+///
+/// # Args
+/// - `auth_config`: authentication configuration
+/// - `reauth`: whether to re-authenticate the application if no cached credentials are found
+// - `use_cached`: whether to use cached credentials if available
+pub async fn get_creds(
+    auth_config: &AuthConfig,
+    reauth: bool,
+    use_cached: bool,
+) -> Result<Credentials> {
+    let creds = if use_cached {
+        auth_config.cache.credentials()
+    } else {
+        None
+    };
+
+    Ok(match creds {
         None => {
             let msg = "No cached credentials found, please authenticate the application first.";
             if reauth {
                 eprintln!("{msg}");
                 get_access_token(
                     SPOTIFY_CLIENT_ID,
-                    CLIENT_REDIRECT_URI,
+                    &auth_config.login_redirect_uri,
                     OAUTH_SCOPES.to_vec(),
                 )
                 .map(|t| Credentials::with_access_token(t.access_token))?
diff --git a/spotify_player/src/cli/handlers.rs b/spotify_player/src/cli/handlers.rs
index 43e3a43e..da311fb0 100644
--- a/spotify_player/src/cli/handlers.rs
+++ b/spotify_player/src/cli/handlers.rs
@@ -171,7 +171,6 @@ fn try_connect_to_client(socket: &UdpSocket, configs: &config::Configs) -> Resul
 }
 
 pub fn handle_cli_subcommand(cmd: &str, args: &ArgMatches) -> Result<()> {
-    let socket = UdpSocket::bind("127.0.0.1:0")?;
     let configs = config::get_config();
 
     // handle commands that don't require a client separately
@@ -179,7 +178,7 @@ pub fn handle_cli_subcommand(cmd: &str, args: &ArgMatches) -> Result<()> {
         "authenticate" => {
             let auth_config = AuthConfig::new(configs)?;
             let rt = tokio::runtime::Runtime::new()?;
-            rt.block_on(crate::auth::get_creds(&auth_config, true))?;
+            rt.block_on(crate::auth::get_creds(&auth_config, true, false))?;
             std::process::exit(0);
         }
         "generate" => {
@@ -194,6 +193,7 @@ pub fn handle_cli_subcommand(cmd: &str, args: &ArgMatches) -> Result<()> {
         _ => {}
     }
 
+    let socket = UdpSocket::bind("127.0.0.1:0")?;
     try_connect_to_client(&socket, configs).context("try to connect to a client")?;
 
     // construct a socket request based on the CLI command and its arguments
diff --git a/spotify_player/src/client/mod.rs b/spotify_player/src/client/mod.rs
index 34e7edf5..f21373b3 100644
--- a/spotify_player/src/client/mod.rs
+++ b/spotify_player/src/client/mod.rs
@@ -114,7 +114,7 @@ impl Client {
     /// Create a new client session
     pub async fn new_session(&self, state: Option<&SharedState>, reauth: bool) -> Result<()> {
         let session = self.auth_config.session();
-        let creds = auth::get_creds(&self.auth_config, reauth)
+        let creds = auth::get_creds(&self.auth_config, reauth, true)
             .await
             .context("get credentials")?;
         *self.session.lock().await = Some(session.clone());
diff --git a/spotify_player/src/config/mod.rs b/spotify_player/src/config/mod.rs
index e2e98750..66e007d5 100644
--- a/spotify_player/src/config/mod.rs
+++ b/spotify_player/src/config/mod.rs
@@ -54,6 +54,8 @@ pub struct AppConfig {
 
     pub client_port: u16,
 
+    pub login_redirect_uri: String,
+
     pub player_event_hook_command: Option<Command>,
 
     pub playback_format: String,
@@ -256,6 +258,8 @@ impl Default for AppConfig {
 
             client_port: 8080,
 
+            login_redirect_uri: "http://127.0.0.1:8989/login".to_string(),
+
             tracks_playback_limit: 50,
 
             playback_format: String::from("{status} {track} • {artists}\n{album}\n{metadata}"),


Test patch: