diff --git a/src/models/servarr_data/sonarr/sonarr_data.rs b/src/models/servarr_data/sonarr/sonarr_data.rs index 275e0ff..4707991 100644 --- a/src/models/servarr_data/sonarr/sonarr_data.rs +++ b/src/models/servarr_data/sonarr/sonarr_data.rs @@ -3,8 +3,9 @@ use strum::EnumIter; use crate::models::{ sonarr_models::{BlocklistItem, Series}, + stateful_list::StatefulList, stateful_table::StatefulTable, - Route, + HorizontallyScrollableText, Route, }; #[cfg(test)] @@ -16,6 +17,7 @@ pub struct SonarrData { pub start_time: DateTime, pub series: StatefulTable, pub blocklist: StatefulTable, + pub logs: StatefulList, } impl Default for SonarrData { @@ -25,6 +27,7 @@ impl Default for SonarrData { start_time: DateTime::default(), series: StatefulTable::default(), blocklist: StatefulTable::default(), + logs: StatefulList::default(), } } } diff --git a/src/models/servarr_data/sonarr/sonarr_data_tests.rs b/src/models/servarr_data/sonarr/sonarr_data_tests.rs index 6b9c590..86a1937 100644 --- a/src/models/servarr_data/sonarr/sonarr_data_tests.rs +++ b/src/models/servarr_data/sonarr/sonarr_data_tests.rs @@ -38,6 +38,7 @@ mod tests { assert_eq!(sonarr_data.start_time, >::default()); assert!(sonarr_data.series.is_empty()); assert!(sonarr_data.blocklist.is_empty()); + assert!(sonarr_data.logs.is_empty()); } } } diff --git a/src/models/sonarr_models.rs b/src/models/sonarr_models.rs index 32f4563..d3ea7c9 100644 --- a/src/models/sonarr_models.rs +++ b/src/models/sonarr_models.rs @@ -42,6 +42,23 @@ pub struct Language { pub name: String, } +#[derive(Default, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Log { + pub time: DateTime, + pub exception: Option, + pub exception_type: Option, + pub level: String, + pub logger: Option, + pub message: Option, + pub method: Option, +} + +#[derive(Default, Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] +pub struct LogResponse { + pub records: Vec, +} + #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Ord, PartialOrd)] pub struct Quality { pub name: String, @@ -215,6 +232,7 @@ pub enum SonarrSerdeable { SeriesVec(Vec), SystemStatus(SystemStatus), BlocklistResponse(BlocklistResponse), + LogResponse(LogResponse), } impl From for Serdeable { @@ -235,6 +253,7 @@ serde_enum_from!( SeriesVec(Vec), SystemStatus(SystemStatus), BlocklistResponse(BlocklistResponse), + LogResponse(LogResponse), } ); diff --git a/src/models/sonarr_models_tests.rs b/src/models/sonarr_models_tests.rs index cd47089..7b389c1 100644 --- a/src/models/sonarr_models_tests.rs +++ b/src/models/sonarr_models_tests.rs @@ -5,8 +5,8 @@ mod tests { use crate::models::{ sonarr_models::{ - BlocklistItem, BlocklistResponse, Series, SeriesStatus, SeriesType, SonarrSerdeable, - SystemStatus, + BlocklistItem, BlocklistResponse, Log, LogResponse, Series, SeriesStatus, SeriesType, + SonarrSerdeable, SystemStatus, }, Serdeable, }; @@ -109,4 +109,18 @@ mod tests { SonarrSerdeable::BlocklistResponse(blocklist_response) ); } + + #[test] + fn test_sonarr_serdeable_from_log_response() { + let log_response = LogResponse { + records: vec![Log { + level: "info".to_owned(), + ..Log::default() + }], + }; + + let sonarr_serdeable: SonarrSerdeable = log_response.clone().into(); + + assert_eq!(sonarr_serdeable, SonarrSerdeable::LogResponse(log_response)); + } } diff --git a/src/network/radarr_network.rs b/src/network/radarr_network.rs index 979e52b..936178b 100644 --- a/src/network/radarr_network.rs +++ b/src/network/radarr_network.rs @@ -194,7 +194,10 @@ impl<'a, 'b> Network<'a, 'b> { RadarrEvent::GetDownloads => self.get_downloads().await.map(RadarrSerdeable::from), RadarrEvent::GetHostConfig => self.get_host_config().await.map(RadarrSerdeable::from), RadarrEvent::GetIndexers => self.get_indexers().await.map(RadarrSerdeable::from), - RadarrEvent::GetLogs(events) => self.get_logs(events).await.map(RadarrSerdeable::from), + RadarrEvent::GetLogs(events) => self + .get_radarr_logs(events) + .await + .map(RadarrSerdeable::from), RadarrEvent::GetMovieCredits(movie_id) => { self.get_credits(movie_id).await.map(RadarrSerdeable::from) } @@ -1438,7 +1441,7 @@ impl<'a, 'b> Network<'a, 'b> { .await } - async fn get_logs(&mut self, events: Option) -> Result { + async fn get_radarr_logs(&mut self, events: Option) -> Result { info!("Fetching Radarr logs"); let event = RadarrEvent::GetLogs(events); diff --git a/src/network/radarr_network_tests.rs b/src/network/radarr_network_tests.rs index 97a7781..f386a11 100644 --- a/src/network/radarr_network_tests.rs +++ b/src/network/radarr_network_tests.rs @@ -2450,7 +2450,7 @@ mod test { } #[tokio::test] - async fn test_handle_get_logs_event() { + async fn test_handle_get_radarr_logs_event() { let expected_logs = vec![ HorizontallyScrollableText::from( "2023-05-20 21:29:16 UTC|FATAL|RadarrError|Some.Big.Bad.Exception|test exception", @@ -2518,7 +2518,7 @@ mod test { } #[tokio::test] - async fn test_handle_get_logs_event_uses_provided_events() { + async fn test_handle_get_radarr_logs_event_uses_provided_events() { let expected_logs = vec![ HorizontallyScrollableText::from( "2023-05-20 21:29:16 UTC|FATAL|RadarrError|Some.Big.Bad.Exception|test exception", diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 2ad14be..2e87e22 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -5,9 +5,8 @@ use serde_json::{json, Value}; use crate::{ models::{ servarr_data::sonarr::sonarr_data::ActiveSonarrBlock, - sonarr_models::BlocklistResponse, - sonarr_models::{Series, SonarrSerdeable, SystemStatus}, - Route, + sonarr_models::{BlocklistResponse, LogResponse, Series, SonarrSerdeable, SystemStatus}, + HorizontallyScrollableText, Route, Scrollable, }, network::RequestMethod, }; @@ -22,6 +21,7 @@ pub enum SonarrEvent { ClearBlocklist, DeleteBlocklistItem(Option), GetBlocklist, + GetLogs(Option), GetStatus, HealthCheck, ListSeries, @@ -33,6 +33,7 @@ impl NetworkResource for SonarrEvent { SonarrEvent::ClearBlocklist => "/blocklist/bulk", SonarrEvent::DeleteBlocklistItem(_) => "/blocklist", SonarrEvent::GetBlocklist => "/blocklist?page=1&pageSize=10000", + SonarrEvent::GetLogs(_) => "/log", SonarrEvent::GetStatus => "/system/status", SonarrEvent::HealthCheck => "/health", SonarrEvent::ListSeries => "/series", @@ -61,6 +62,10 @@ impl<'a, 'b> Network<'a, 'b> { .await .map(SonarrSerdeable::from), SonarrEvent::GetBlocklist => self.get_sonarr_blocklist().await.map(SonarrSerdeable::from), + SonarrEvent::GetLogs(events) => self + .get_sonarr_logs(events) + .await + .map(SonarrSerdeable::from), SonarrEvent::GetStatus => self.get_sonarr_status().await.map(SonarrSerdeable::from), SonarrEvent::HealthCheck => self .get_sonarr_healthcheck() @@ -170,6 +175,53 @@ impl<'a, 'b> Network<'a, 'b> { .await } + async fn get_sonarr_logs(&mut self, events: Option) -> Result { + info!("Fetching Sonarr logs"); + let event = SonarrEvent::GetLogs(events); + + let params = format!( + "pageSize={}&sortDirection=descending&sortKey=time", + events.unwrap_or(500) + ); + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params)) + .await; + + self + .handle_request::<(), LogResponse>(request_props, |log_response, mut app| { + let mut logs = log_response.records; + logs.reverse(); + + let log_lines = logs + .into_iter() + .map(|log| { + if log.exception.is_some() { + HorizontallyScrollableText::from(format!( + "{}|{}|{}|{}|{}", + log.time, + log.level.to_uppercase(), + log.logger.as_ref().unwrap(), + log.exception_type.as_ref().unwrap(), + log.exception.as_ref().unwrap() + )) + } else { + HorizontallyScrollableText::from(format!( + "{}|{}|{}|{}", + log.time, + log.level.to_uppercase(), + log.logger.as_ref().unwrap(), + log.message.as_ref().unwrap() + )) + } + }) + .collect(); + + app.data.sonarr_data.logs.set_items(log_lines); + app.data.sonarr_data.logs.scroll_to_bottom(); + }) + .await + } + async fn list_series(&mut self) -> Result> { info!("Fetching Sonarr library"); let event = SonarrEvent::ListSeries; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 538fa85..84159ff 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -9,9 +9,10 @@ mod test { use tokio_util::sync::CancellationToken; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; - use crate::models::sonarr_models::{BlocklistItem, Language}; + use crate::models::sonarr_models::{BlocklistItem, Language, LogResponse}; use crate::models::sonarr_models::{BlocklistResponse, Quality}; use crate::models::sonarr_models::{QualityWrapper, SystemStatus}; + use crate::models::HorizontallyScrollableText; use crate::models::{sonarr_models::SonarrSerdeable, stateful_table::SortOption}; use crate::{ @@ -78,8 +79,9 @@ mod test { #[case(SonarrEvent::ClearBlocklist, "/blocklist/bulk")] #[case(SonarrEvent::DeleteBlocklistItem(None), "/blocklist")] #[case(SonarrEvent::HealthCheck, "/health")] - #[case(SonarrEvent::GetStatus, "/system/status")] #[case(SonarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")] + #[case(SonarrEvent::GetLogs(Some(500)), "/log")] + #[case(SonarrEvent::GetStatus, "/system/status")] fn test_resource(#[case] event: SonarrEvent, #[case] expected_uri: String) { assert_str_eq!(event.resource(), expected_uri); } @@ -276,6 +278,142 @@ mod test { async_server.assert_async().await; } + #[tokio::test] + async fn test_handle_get_sonarr_logs_event() { + let expected_logs = vec![ + HorizontallyScrollableText::from( + "2023-05-20 21:29:16 UTC|FATAL|RadarrError|Some.Big.Bad.Exception|test exception", + ), + HorizontallyScrollableText::from("2023-05-20 21:29:16 UTC|INFO|TestLogger|test message"), + ]; + let logs_response_json = json!({ + "page": 1, + "pageSize": 500, + "sortKey": "time", + "sortDirection": "descending", + "totalRecords": 2, + "records": [ + { + "time": "2023-05-20T21:29:16Z", + "level": "info", + "logger": "TestLogger", + "message": "test message", + "id": 1 + }, + { + "time": "2023-05-20T21:29:16Z", + "level": "fatal", + "logger": "RadarrError", + "exception": "test exception", + "exceptionType": "Some.Big.Bad.Exception", + "id": 2 + } + ] + }); + let response: LogResponse = serde_json::from_value(logs_response_json.clone()).unwrap(); + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(logs_response_json), + None, + SonarrEvent::GetLogs(None), + None, + Some("pageSize=500&sortDirection=descending&sortKey=time"), + ) + .await; + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::LogResponse(logs) = network + .handle_sonarr_event(SonarrEvent::GetLogs(None)) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.sonarr_data.logs.items, + expected_logs + ); + assert!(app_arc + .lock() + .await + .data + .sonarr_data + .logs + .current_selection() + .text + .contains("INFO")); + assert_eq!(logs, response); + } + } + + #[tokio::test] + async fn test_handle_get_sonarr_logs_event_uses_provided_events() { + let expected_logs = vec![ + HorizontallyScrollableText::from( + "2023-05-20 21:29:16 UTC|FATAL|RadarrError|Some.Big.Bad.Exception|test exception", + ), + HorizontallyScrollableText::from("2023-05-20 21:29:16 UTC|INFO|TestLogger|test message"), + ]; + let logs_response_json = json!({ + "page": 1, + "pageSize": 1000, + "sortKey": "time", + "sortDirection": "descending", + "totalRecords": 2, + "records": [ + { + "time": "2023-05-20T21:29:16Z", + "level": "info", + "logger": "TestLogger", + "message": "test message", + "id": 1 + }, + { + "time": "2023-05-20T21:29:16Z", + "level": "fatal", + "logger": "RadarrError", + "exception": "test exception", + "exceptionType": "Some.Big.Bad.Exception", + "id": 2 + } + ] + }); + let response: LogResponse = serde_json::from_value(logs_response_json.clone()).unwrap(); + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(logs_response_json), + None, + SonarrEvent::GetLogs(Some(1000)), + None, + Some("pageSize=1000&sortDirection=descending&sortKey=time"), + ) + .await; + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::LogResponse(logs) = network + .handle_sonarr_event(SonarrEvent::GetLogs(Some(1000))) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc.lock().await.data.sonarr_data.logs.items, + expected_logs + ); + assert!(app_arc + .lock() + .await + .data + .sonarr_data + .logs + .current_selection() + .text + .contains("INFO")); + assert_eq!(logs, response); + } + } + #[rstest] #[tokio::test] async fn test_handle_get_series_event(#[values(true, false)] use_custom_sorting: bool) {