use std::fmt::Debug; use std::sync::Arc; use anyhow::{Result, anyhow}; use async_trait::async_trait; use lidarr_network::LidarrEvent; use log::{debug, error, warn}; use regex::Regex; use reqwest::{Client, RequestBuilder}; use serde::Serialize; use serde::de::DeserializeOwned; use sonarr_network::SonarrEvent; use strum_macros::Display; use tokio::select; use tokio::sync::{Mutex, MutexGuard}; use tokio_util::sync::CancellationToken; use crate::app::{App, ServarrConfig}; use crate::models::Serdeable; use crate::network::radarr_network::RadarrEvent; #[cfg(test)] use mockall::automock; use reqwest::header::HeaderMap; pub mod lidarr_network; pub mod radarr_network; pub mod sonarr_network; mod utils; #[cfg(test)] mod network_tests; #[cfg(test)] pub mod servarr_test_utils; #[cfg_attr(test, automock)] #[async_trait] pub trait NetworkTrait { async fn handle_network_event(&mut self, network_event: NetworkEvent) -> Result; } pub trait NetworkResource { fn resource(&self) -> &'static str; } #[derive(PartialEq, Eq, Debug, Clone)] pub enum NetworkEvent { Radarr(RadarrEvent), Sonarr(SonarrEvent), Lidarr(LidarrEvent), } #[derive(Clone)] pub struct Network<'a, 'b> { client: Client, pub cancellation_token: CancellationToken, pub app: &'a Arc>>, } #[async_trait] impl NetworkTrait for Network<'_, '_> { async fn handle_network_event(&mut self, network_event: NetworkEvent) -> Result { let resp = match network_event { NetworkEvent::Radarr(radarr_event) => self .handle_radarr_event(radarr_event) .await .map(Serdeable::from), NetworkEvent::Sonarr(sonarr_event) => self .handle_sonarr_event(sonarr_event) .await .map(Serdeable::from), NetworkEvent::Lidarr(lidarr_event) => self .handle_lidarr_event(lidarr_event) .await .map(Serdeable::from), }; let mut app = self.app.lock().await; app.is_loading = false; resp } } impl<'a, 'b> Network<'a, 'b> { pub fn new( app: &'a Arc>>, cancellation_token: CancellationToken, client: Client, ) -> Self { Network { client, app, cancellation_token, } } pub(super) async fn reset_cancellation_token(&mut self) { self.cancellation_token = self.app.lock().await.reset_cancellation_token(); } async fn handle_request( &mut self, request_props: RequestProps, mut app_update_fn: impl FnMut(R, MutexGuard<'_, App<'_>>), ) -> Result where B: Serialize + Default + Debug, R: DeserializeOwned + Default + Clone, { let ignore_status_code = request_props.ignore_status_code; let method = request_props.method; let request_uri = request_props.uri.clone(); select! { _ = self.cancellation_token.cancelled() => { warn!("Received Cancel request. Cancelling request to: {request_uri}"); Ok(R::default()) } resp = self.call_api(request_props).await.send() => { match resp { Ok(response) => { if response.status().is_success() || ignore_status_code { match method { RequestMethod::Get | RequestMethod::Post => { match utils::parse_response::(response).await { Ok(value) => { let app = self.app.lock().await; app_update_fn(value.clone(), app); Ok(value) } Err(e) => { error!("Failed to parse response! {e:?}"); self .app .lock() .await .handle_error(anyhow!("Failed to parse response! {e:?}")); Err(anyhow!("Failed to parse response! {e:?}")) } } } RequestMethod::Delete | RequestMethod::Put => Ok(R::default()), } } else { let status = response.status(); let whitespace_regex = Regex::new(r"\s+")?; let response_body = response.text().await.unwrap_or_default(); let error_body = whitespace_regex .replace_all(&response_body.replace('\n', " "), " ") .to_string(); error!("Request failed. Received {status} response code with body: {response_body}"); self.app.lock().await.handle_error(anyhow!("Request failed. Received {status} response code with body: {error_body}")); Err(anyhow!("Request failed. Received {status} response code with body: {error_body}")) } } Err(e) => { error!("Failed to send request. {e:?}"); self .app .lock() .await .handle_error(anyhow!("Failed to send request. {e} ")); Err(anyhow!("Failed to send request. {e} ")) } } } } } async fn call_api( &self, request_props: RequestProps, ) -> RequestBuilder { let RequestProps { uri, method, body, api_token, custom_headers, .. } = request_props; debug!("Creating RequestBuilder for resource: {uri:?}"); debug!("Sending {method:?} request to {uri} with body {body:?}"); match method { RequestMethod::Get => self .client .get(uri) .header("X-Api-Key", api_token) .headers(custom_headers), RequestMethod::Post => self .client .post(uri) .json(&body.unwrap_or_default()) .header("X-Api-Key", api_token) .headers(custom_headers), RequestMethod::Put => self .client .put(uri) .json(&body.unwrap_or_default()) .header("X-Api-Key", api_token) .headers(custom_headers), RequestMethod::Delete => self .client .delete(uri) .json(&body.unwrap_or_default()) .header("X-Api-Key", api_token) .headers(custom_headers), } } async fn request_props_from( &self, network_event: N, method: RequestMethod, body: Option, path: Option, query_params: Option, ) -> RequestProps where T: Serialize + Debug, N: Into + NetworkResource, { let app = self.app.lock().await; let resource = network_event.resource(); let ServarrConfig { host, port, uri, api_token, ssl_cert_path, ssl, custom_headers: custom_headers_option, .. } = app .server_tabs .get_active_config() .as_ref() .expect("Servarr config is undefined"); let network_event_type = network_event.into(); let (default_port, api_version) = match &network_event_type { NetworkEvent::Radarr(_) => (7878, "v3"), NetworkEvent::Sonarr(_) => (8989, "v3"), NetworkEvent::Lidarr(_) => (8686, "v1"), }; let mut uri = if let Some(servarr_uri) = uri { format!("{servarr_uri}/api/{api_version}{resource}") } else { let protocol = if ssl_cert_path.is_some() || ssl.unwrap_or(false) { "https" } else { "http" }; let host = host.as_ref().unwrap(); format!( "{protocol}://{host}:{}/api/{api_version}{resource}", port.unwrap_or(default_port) ) }; if let Some(path) = path { uri = format!("{uri}{path}"); } if let Some(params) = query_params { uri = format!("{uri}?{params}"); } let custom_headers = custom_headers_option.clone().unwrap_or_default(); RequestProps { uri, method, body, api_token: api_token.as_ref().expect("API token not found").clone(), ignore_status_code: false, custom_headers, } } } #[derive(Clone, Copy, Debug, Display, PartialEq, Eq)] pub enum RequestMethod { Get, Post, Put, Delete, } #[derive(Debug)] pub struct RequestProps { pub uri: String, pub method: RequestMethod, pub body: Option, pub api_token: String, pub ignore_status_code: bool, pub custom_headers: HeaderMap, }