diff --git a/src/trakt.rs b/src/trakt.rs index 3bbd942..16d250e 100644 --- a/src/trakt.rs +++ b/src/trakt.rs @@ -78,6 +78,34 @@ impl Trakt { } } + fn handle_auth_error(&self, status_code: u16, endpoint: &str) { + match status_code { + 401 => { + if self.oauth_access_token.is_some() { + log(&format!( + "OAuth token expired or invalid for endpoint: {}", + endpoint + )); + log( + "Please refresh your OAuth token to continue using authenticated endpoints", + ); + } else { + log(&format!( + "Authentication required for endpoint: {}", + endpoint + )); + } + } + 403 => { + log(&format!( + "Access forbidden for endpoint: {} - check token permissions", + endpoint + )); + } + _ => {} + } + } + pub fn get_watching(&self) -> Option { let endpoint = format!("https://api.trakt.tv/users/{}/watching", self.username); @@ -87,6 +115,7 @@ impl Trakt { .set("Content-Type", "application/json") .set("trakt-api-version", "2") .set("trakt-api-key", &self.client_id); + // add Authorization header if there is a (valid) OAuth access token let request = if self.oauth_access_token.is_some() && !self.oauth_access_token.as_ref().unwrap().is_empty() @@ -99,7 +128,14 @@ impl Trakt { let response = match request.call() { Ok(response) => response, - Err(_) => return None, + Err(ureq::Error::Status(code, _)) => { + self.handle_auth_error(code, &endpoint); + return None; + } + Err(e) => { + log(&format!("Network error calling {}: {}", endpoint, e)); + return None; + } }; response.into_json().unwrap_or_default() @@ -120,20 +156,32 @@ impl Trakt { MediaType::Show => format!("https://api.themoviedb.org/3/tv/{tmdb_id}/season/{season_id}/images?api_key={tmdb_token}") }; - let response = self.agent.get(&endpoint).call(); - - if response.is_err() { - log(&format!( - "{} image not correctly found", - media_type.as_str() - )); - return None; - } + let response = match self.agent.get(&endpoint).call() { + Ok(response) => response, + Err(ureq::Error::Status(401, _)) => { + log(&format!( + "TMDB API key expired or invalid for endpoint: {}", + endpoint + )); + return None; + } + Err(e) => { + log(&format!( + "Error fetching {} image: {}", + media_type.as_str(), + e + )); + return None; + } + }; - match response.unwrap().into_json::() { + match response.into_json::() { Ok(body) => { if body["posters"].as_array().unwrap_or(&vec![]).is_empty() { - log("Show image not correctly found"); + log(&format!( + "{} image not found in TMDB response", + media_type.as_str() + )); return None; } @@ -146,12 +194,16 @@ impl Trakt { .as_str() .unwrap() ); + + // Cache the image URL + self.image_cache.insert(tmdb_id, image_url.clone()); Some(image_url) } - Err(_) => { + Err(e) => { log(&format!( - "{} image not correctly found", - media_type.as_str() + "Failed to parse {} image response: {}", + media_type.as_str(), + e )); None } @@ -175,7 +227,14 @@ impl Trakt { .call() { Ok(response) => response, - Err(_) => return 0.0, + Err(ureq::Error::Status(code, _)) => { + self.handle_auth_error(code, &endpoint); + return 0.0; + } + Err(e) => { + log(&format!("Network error fetching movie rating: {}", e)); + return 0.0; + } }; match response.into_json::() { @@ -184,7 +243,10 @@ impl Trakt { .insert(movie_slug.to_string(), body.rating); body.rating } - Err(_) => 0.0, + Err(e) => { + log(&format!("Failed to parse movie rating response: {}", e)); + 0.0 + } } } } diff --git a/src/utils.rs b/src/utils.rs index 0daf3e3..6c77f21 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -36,24 +36,42 @@ pub struct WatchStats { impl Env { pub fn check_oauth(&mut self) { - if self.trakt_oauth_enabled { - if self.trakt_access_token.is_none() - || self.trakt_access_token.as_ref().unwrap().is_empty() - { + if !self.trakt_oauth_enabled { + return; + } + + // Check if we have no access token + if self.trakt_access_token.is_none() || self.trakt_access_token.as_ref().unwrap().is_empty() + { + log("No OAuth access token found, starting authorization flow"); + self.authorize_app(); + return; + } + + // Check if the refresh token is expired (this is what you were originally checking) + if let Some(refresh_expires_at) = self.trakt_refresh_token_expires_at { + let now = Utc::now().timestamp() as u64; + if now >= refresh_expires_at { + log("OAuth refresh token has expired, need to reauthorize"); self.authorize_app(); - } else if let Some(expires_at) = self.trakt_refresh_token_expires_at { - if Utc::now().timestamp() as u64 > expires_at { - self.exchange_refresh_token_for_access_token(); - } + } else { + // Try to refresh the access token proactively + log("Refresh token is still valid, refreshing access token"); + self.exchange_refresh_token_for_access_token(); } + } else { + log( + "No refresh token expiry time found, unable to determine if refresh token is valid", + ); } } fn authorize_app(&mut self) { + log("Opening browser for OAuth authorization"); if webbrowser::open( &format!("https://trakt.tv/oauth/authorize?response_type=code&client_id={}&redirect_uri=urn:ietf:wg:oauth:2.0:oob", self.trakt_client_id) ).is_err() { - eprintln!("Failed to open webbrowser to authorize discrakt"); + log("Failed to open webbrowser to authorize discrakt"); return; }; self.exchange_code_for_access_token(); @@ -69,10 +87,13 @@ impl Env { .expect("Failed to read line"); let code = code.trim(); + log("Exchanging authorization code for access token"); + let agent = AgentBuilder::new() .timeout_read(Duration::from_secs(5)) .timeout_write(Duration::from_secs(5)) .build(); + let response = match agent .post("https://api.trakt.tv/oauth/token") .set("Content-Type", "application/json") @@ -85,32 +106,65 @@ impl Env { })) { Ok(response) => response, - Err(_) => return, + Err(ureq::Error::Status(code, response)) => { + log(&format!("Failed to exchange authorization code: HTTP {}", code)); + if let Ok(error_body) = response.into_string() { + log(&format!("Error details: {}", error_body)); + } + return; + } + Err(e) => { + log(&format!("Network error during token exchange: {}", e)); + return; + } }; let json_response: Option = response.into_json().unwrap_or_default(); if let Some(json_response) = json_response { + log("Successfully obtained OAuth tokens"); self.trakt_access_token = Some(json_response.access_token.clone()); self.trakt_refresh_token = Some(json_response.refresh_token.clone()); - self.trakt_refresh_token_expires_at = - Some(json_response.created_at + 60 * 60 * 24 * 30 * 3); // secs * mins * hours * days * months => 3 months + + // Calculate refresh token expiry (3 months from now) + let now = Utc::now().timestamp() as u64; + self.trakt_refresh_token_expires_at = Some(now + 60 * 60 * 24 * 30 * 3); // 3 months + set_oauth_tokens(&json_response); + + log(&format!( + "Tokens obtained successfully, refresh token expires at: {}", + DateTime::from_timestamp(self.trakt_refresh_token_expires_at.unwrap() as i64, 0) + .unwrap() + .to_rfc3339_opts(SecondsFormat::Secs, true) + )); } else { - eprintln!("Failed to exchange code for access token"); + log("Failed to parse token response from Trakt API"); } } fn exchange_refresh_token_for_access_token(&mut self) { + let refresh_token = match &self.trakt_refresh_token { + Some(token) if !token.is_empty() => token.clone(), + _ => { + log("No refresh token available, need to reauthorize"); + self.authorize_app(); + return; + } + }; + + log("Attempting to refresh OAuth access token"); + let agent = AgentBuilder::new() .timeout_read(Duration::from_secs(5)) .timeout_write(Duration::from_secs(5)) .build(); + let response = match agent .post("https://api.trakt.tv/oauth/token") .set("Content-Type", "application/json") .send_json(ureq::json!({ - "code": "Get the code from the webbrowser", + "refresh_token": refresh_token, "client_id": self.trakt_client_id, "client_secret": self.trakt_client_secret.as_ref().expect("client_secret not found"), "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", @@ -118,19 +172,50 @@ impl Env { })) { Ok(response) => response, - Err(_) => return, + Err(ureq::Error::Status(400, response)) => { + log("Refresh token is invalid or expired, need to reauthorize"); + if let Ok(error_body) = response.into_string() { + log(&format!("Error details: {}", error_body)); + } + self.authorize_app(); + return; + } + Err(ureq::Error::Status(code, response)) => { + log(&format!("Failed to refresh token: HTTP {}", code)); + if let Ok(error_body) = response.into_string() { + log(&format!("Error details: {}", error_body)); + } + return; + } + Err(e) => { + log(&format!("Network error during token refresh: {}", e)); + return; + } }; let json_response: Option = response.into_json().unwrap_or_default(); if let Some(json_response) = json_response { + log("Successfully refreshed OAuth access token"); self.trakt_access_token = Some(json_response.access_token.clone()); self.trakt_refresh_token = Some(json_response.refresh_token.clone()); - self.trakt_refresh_token_expires_at = - Some(json_response.created_at + 60 * 60 * 24 * 30 * 3); // secs * mins * hours * days * months => 3 months + + // Calculate refresh token expiry (3 months from now) + let now = Utc::now().timestamp() as u64; + self.trakt_refresh_token_expires_at = Some(now + 60 * 60 * 24 * 30 * 3); // 3 months + set_oauth_tokens(&json_response); + + log(&format!( + "Token refreshed successfully, new refresh token expires at: {}", + DateTime::from_timestamp(self.trakt_refresh_token_expires_at.unwrap() as i64, 0) + .unwrap() + .to_rfc3339_opts(SecondsFormat::Secs, true) + )); } else { - eprintln!("Failed to exchange refresh token for access token"); + log("Failed to parse refresh token response from Trakt API"); + log("Will attempt full reauthorization"); + self.authorize_app(); } } } @@ -148,13 +233,13 @@ fn find_config_file() -> Option { return Some(config_file); } } - eprintln!( + log(&format!( "Could not find credentials.ini in {:?}", locations .iter() .map(|loc| loc.to_str().to_owned().unwrap()) .collect::>() - ); + )); None }