diff --git a/psst-core/src/connection/mod.rs b/psst-core/src/connection/mod.rs index ef1b3fbb..1fdff12a 100644 --- a/psst-core/src/connection/mod.rs +++ b/psst-core/src/connection/mod.rs @@ -99,42 +99,69 @@ pub struct Transport { } impl Transport { - pub fn resolve_ap_with_fallback(proxy_url: Option<&str>) -> String { + pub fn resolve_ap_with_fallback(proxy_url: Option<&str>) -> Vec { match Self::resolve_ap(proxy_url) { - Ok(ap) => ap, + Ok(ap_list) => { + log::info!("successfully resolved {} access points", ap_list.len()); + ap_list + } Err(err) => { - log::error!("using AP fallback, error while resolving: {:?}", err); - AP_FALLBACK.into() + log::error!("error while resolving APs, using fallback: {:?}", err); + vec![AP_FALLBACK.into()] } } } - pub fn resolve_ap(proxy_url: Option<&str>) -> Result { + pub fn resolve_ap(proxy_url: Option<&str>) -> Result, Error> { #[derive(Clone, Debug, Deserialize)] struct APResolveData { ap_list: Vec, } let agent = default_ureq_agent_builder(proxy_url)?.build(); + log::info!("requesting AP list from {}", AP_RESOLVE_ENDPOINT); let data: APResolveData = agent.get(AP_RESOLVE_ENDPOINT).call()?.into_json()?; - data.ap_list - .into_iter() - .next() - .ok_or(Error::UnexpectedResponse) + if data.ap_list.is_empty() { + log::warn!("received empty AP list from server"); + Err(Error::UnexpectedResponse) + } else { + log::info!("received {} APs from server", data.ap_list.len()); + Ok(data.ap_list) + } } - pub fn connect(ap: &str, proxy_url: Option<&str>) -> Result { - log::trace!("connecting to: {:?} with proxy: {:?}", ap, proxy_url); - let stream = if let Some(url) = proxy_url { - Self::stream_through_proxy(ap, url)? - } else { - Self::stream_without_proxy(ap)? - }; - if let Err(err) = stream.set_write_timeout(Some(NET_IO_TIMEOUT)) { - log::warn!("failed to set TCP write timeout: {:?}", err); + pub fn connect(ap_list: &[String], proxy_url: Option<&str>) -> Result { + log::info!( + "attempting to connect using {} access points", + ap_list.len() + ); + for (index, ap) in ap_list.iter().enumerate() { + log::info!("Trying AP {} of {}: {}", index + 1, ap_list.len(), ap); + let stream = if let Some(url) = proxy_url { + match Self::stream_through_proxy(ap, url) { + Ok(s) => s, + Err(e) => { + log::warn!("failed to connect to AP {} through proxy: {:?}", ap, e); + continue; + } + } + } else { + match Self::stream_without_proxy(ap) { + Ok(s) => s, + Err(e) => { + log::warn!("failed to connect to AP {} without proxy: {:?}", ap, e); + continue; + } + } + }; + if let Err(err) = stream.set_write_timeout(Some(NET_IO_TIMEOUT)) { + log::warn!("failed to set TCP write timeout: {:?}", err); + } + log::info!("successfully connected to AP: {}", ap); + return Self::exchange_keys(stream); } - log::trace!("connected"); - Self::exchange_keys(stream) + log::error!("failed to connect to any access point"); + Err(Error::ConnectionFailed) } fn stream_without_proxy(ap: &str) -> Result { diff --git a/psst-core/src/error.rs b/psst-core/src/error.rs index f7663b22..8a16e0c6 100644 --- a/psst-core/src/error.rs +++ b/psst-core/src/error.rs @@ -7,6 +7,7 @@ pub enum Error { MediaFileNotFound, ProxyUrlInvalid, AuthFailed { code: i32 }, + ConnectionFailed, JsonError(Box), AudioFetchingError(Box), AudioDecodingError(Box), @@ -40,6 +41,7 @@ impl fmt::Display for Error { 17 => write!(f, "Authentication failed: application banned"), _ => write!(f, "Authentication failed with error code {}", code), }, + Self::ConnectionFailed => write!(f, "Failed to connect to any access point"), Self::ResamplingError(code) => { write!(f, "Resampling failed with error code {}", code) } diff --git a/psst-core/src/oauth.rs b/psst-core/src/oauth.rs index 69ab3ec8..314c0d08 100644 --- a/psst-core/src/oauth.rs +++ b/psst-core/src/oauth.rs @@ -15,10 +15,10 @@ pub fn get_authcode_listener( socket_address: SocketAddr, timeout: Duration, ) -> Result { - log::info!("Starting OAuth listener on {:?}", socket_address); + log::info!("starting OAuth listener on {:?}", socket_address); let listener = TcpListener::bind(socket_address) .map_err(|e| format!("Failed to bind to address: {}", e))?; - log::info!("Listener bound successfully"); + log::info!("listener bound successfully"); let (tx, rx) = mpsc::channel(); diff --git a/psst-core/src/session/mod.rs b/psst-core/src/session/mod.rs index 8204470e..ee75c9d0 100644 --- a/psst-core/src/session/mod.rs +++ b/psst-core/src/session/mod.rs @@ -133,10 +133,8 @@ impl SessionConnection { pub fn open(config: SessionConfig) -> Result { // Connect to the server and exchange keys. let proxy_url = config.proxy_url.as_deref(); - let ap_url = Transport::resolve_ap_with_fallback(proxy_url); - let mut transport = Transport::connect(&ap_url, proxy_url)?; - // Authenticate with provided credentials (either username/password, or saved, - // reusable credential blob from an earlier run). + let ap_list = Transport::resolve_ap_with_fallback(proxy_url); + let mut transport = Transport::connect(&ap_list, proxy_url)?; let credentials = transport.authenticate(config.login_creds)?; Ok(Self { credentials, diff --git a/psst-gui/src/controller/playback.rs b/psst-gui/src/controller/playback.rs index c52a2e5e..bd01edee 100644 --- a/psst-gui/src/controller/playback.rs +++ b/psst-gui/src/controller/playback.rs @@ -223,7 +223,7 @@ impl PlaybackController { fn send(&mut self, event: PlayerEvent) { if let Some(s) = &self.sender { s.send(event) - .map_err(|e| log::error!("Error sending message: {:?}", e)) + .map_err(|e| log::error!("error sending message: {:?}", e)) .ok(); } } diff --git a/psst-gui/src/data/album.rs b/psst-gui/src/data/album.rs index c04664a4..29b0b00a 100644 --- a/psst-gui/src/data/album.rs +++ b/psst-gui/src/data/album.rs @@ -50,7 +50,7 @@ impl Album { pub fn release_year_int(&self) -> usize { self.release_year().parse::().unwrap_or_else(|err| { - log::error!("Error parsing release year for {}: {}", self.name, err); + log::error!("error parsing release year for {}: {}", self.name, err); usize::MAX }) } diff --git a/psst-gui/src/ui/preferences.rs b/psst-gui/src/ui/preferences.rs index fd75b0b5..8cb77d0c 100644 --- a/psst-gui/src/ui/preferences.rs +++ b/psst-gui/src/ui/preferences.rs @@ -347,14 +347,37 @@ impl> Controller for Authenticate { ) { Ok(code) => { let token = oauth::exchange_code_for_token(8888, code, pkce_verifier); - let response = - Authentication::authenticate_and_get_credentials(SessionConfig { - login_creds: Credentials::from_access_token(token), - ..config - }); - event_sink - .submit_command(Self::RESPONSE, response, widget_id) - .unwrap(); + let mut retries = 3; + while retries > 0 { + let response = Authentication::authenticate_and_get_credentials( + SessionConfig { + login_creds: Credentials::from_access_token(token.clone()), + ..config.clone() + }, + ); + match response { + Ok(credentials) => { + event_sink + .submit_command( + Self::RESPONSE, + Ok(credentials), + widget_id, + ) + .unwrap(); + return; + } + Err(e) if retries > 1 => { + log::warn!("authentication failed, retrying: {:?}", e); + retries -= 1; + } + Err(e) => { + event_sink + .submit_command(Self::RESPONSE, Err(e), widget_id) + .unwrap(); + return; + } + } + } } Err(e) => { event_sink