diff --git a/src/models/servarr_data/sonarr/modals.rs b/src/models/servarr_data/sonarr/modals.rs index 3d96c05..92c7645 100644 --- a/src/models/servarr_data/sonarr/modals.rs +++ b/src/models/servarr_data/sonarr/modals.rs @@ -312,6 +312,7 @@ impl Default for EpisodeDetailsModal { pub struct SeasonDetailsModal { pub episodes: StatefulTable, pub episode_details_modal: Option, + pub season_history: StatefulTable, pub season_releases: StatefulTable, pub season_details_tabs: TabState, } @@ -322,6 +323,7 @@ impl Default for SeasonDetailsModal { episodes: StatefulTable::default(), episode_details_modal: None, season_releases: StatefulTable::default(), + season_history: StatefulTable::default(), season_details_tabs: TabState::new(vec![ TabRoute { title: "Episodes", diff --git a/src/models/servarr_data/sonarr/modals_tests.rs b/src/models/servarr_data/sonarr/modals_tests.rs index 28cd038..4d98505 100644 --- a/src/models/servarr_data/sonarr/modals_tests.rs +++ b/src/models/servarr_data/sonarr/modals_tests.rs @@ -323,6 +323,7 @@ mod tests { assert!(season_details_modal.episodes.is_empty()); assert!(season_details_modal.episode_details_modal.is_none()); assert!(season_details_modal.season_releases.is_empty()); + assert!(season_details_modal.season_history.is_empty()); assert_eq!(season_details_modal.season_details_tabs.tabs.len(), 2); diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 38c4ef9..7cc85dc 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -70,6 +70,7 @@ pub enum SonarrEvent { GetQueuedEvents, GetRootFolders, GetEpisodeReleases(Option), + GetSeasonHistory(Option<(i64, i64)>), GetSeasonReleases(Option<(i64, i64)>), GetSecurityConfig, GetSeriesDetails(Option), @@ -128,7 +129,7 @@ impl NetworkResource for SonarrEvent { | SonarrEvent::DeleteRootFolder(_) | SonarrEvent::AddRootFolder(_) => "/rootfolder", SonarrEvent::GetSeasonReleases(_) | SonarrEvent::GetEpisodeReleases(_) => "/release", - SonarrEvent::GetSeriesHistory(_) => "/history/series", + SonarrEvent::GetSeriesHistory(_) | SonarrEvent::GetSeasonHistory(_) => "/history/series", SonarrEvent::GetStatus => "/system/status", SonarrEvent::GetTasks => "/system/task", SonarrEvent::GetUpdates => "/update", @@ -266,6 +267,10 @@ impl<'a, 'b> Network<'a, 'b> { .get_episode_releases(params) .await .map(SonarrSerdeable::from), + SonarrEvent::GetSeasonHistory(params) => self + .get_sonarr_season_history(params) + .await + .map(SonarrSerdeable::from), SonarrEvent::GetSeasonReleases(params) => self .get_season_releases(params) .await @@ -1887,6 +1892,56 @@ impl<'a, 'b> Network<'a, 'b> { .await } + async fn get_sonarr_season_history( + &mut self, + series_season_id_tuple: Option<(i64, i64)>, + ) -> Result { + let event = SonarrEvent::GetSeasonHistory(None); + let (series_id, season_number) = + if let Some((series_id, season_number)) = series_season_id_tuple { + (Some(series_id), Some(season_number)) + } else { + (None, None) + }; + + let (series_id, series_id_param) = self.extract_series_id(series_id).await; + let (season_number, season_number_param) = self.extract_season_number(season_number).await; + + info!("Fetching history for series with ID: {series_id} and season number: {season_number}"); + + let params = format!("{series_id_param}&{season_number_param}",); + let request_props = self + .request_props_from(event, RequestMethod::Get, None::<()>, None, Some(params)) + .await; + + self + .handle_request::<(), SonarrHistoryWrapper>(request_props, |history_response, mut app| { + if app.data.sonarr_data.season_details_modal.is_none() { + app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); + } + + let mut history_vec = history_response.records; + history_vec.sort_by(|a, b| a.id.cmp(&b.id)); + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .season_history + .set_items(history_vec); + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .season_history + .apply_sorting_toggle(false); + }) + .await + } + async fn get_sonarr_security_config(&mut self) -> Result { info!("Fetching Sonarr security config"); let event = SonarrEvent::GetSecurityConfig; diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index 684dbca..be959fb 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -224,6 +224,17 @@ mod test { assert_str_eq!(event.resource(), "/history"); } + #[rstest] + fn test_resource_series_history( + #[values( + SonarrEvent::GetSeriesHistory(None), + SonarrEvent::GetSeasonHistory(None) + )] + event: SonarrEvent, + ) { + assert_str_eq!(event.resource(), "/history/series"); + } + #[rstest] fn test_resource_queue( #[values(SonarrEvent::GetDownloads, SonarrEvent::DeleteDownload(None))] event: SonarrEvent, @@ -261,7 +272,6 @@ mod test { #[case(SonarrEvent::HealthCheck, "/health")] #[case(SonarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")] #[case(SonarrEvent::GetDiskSpace, "/diskspace")] - #[case(SonarrEvent::GetSeriesHistory(None), "/history/series")] #[case(SonarrEvent::GetLanguageProfiles, "/languageprofile")] #[case(SonarrEvent::GetLogs(Some(500)), "/log")] #[case(SonarrEvent::GetQualityProfiles, "/qualityprofile")] @@ -4156,6 +4166,349 @@ mod test { } } + #[tokio::test] + async fn test_handle_get_sonarr_season_history_event() { + let history_json = json!({"records": [{ + "id": 123, + "sourceTitle": "z episode", + "episodeId": 1007, + "quality": { "quality": { "name": "Bluray-1080p" } }, + "language": { "id": 1, "name": "English" }, + "date": "2024-02-10T07:28:45Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", + "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" + } + }, + { + "id": 456, + "sourceTitle": "A Episode", + "episodeId": 2001, + "quality": { "quality": { "name": "Bluray-1080p" } }, + "language": { "id": 1, "name": "English" }, + "date": "2024-02-10T07:28:45Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", + "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" + } + }]}); + let response: SonarrHistoryWrapper = serde_json::from_value(history_json.clone()).unwrap(); + let expected_history_items = vec![ + SonarrHistoryItem { + id: 123, + episode_id: 1007, + source_title: "z episode".into(), + ..history_item() + }, + SonarrHistoryItem { + id: 456, + episode_id: 2001, + source_title: "A Episode".into(), + ..history_item() + }, + ]; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(history_json), + None, + SonarrEvent::GetSeasonHistory(None), + None, + Some("seriesId=1&seasonNumber=1"), + ) + .await; + app_arc.lock().await.data.sonarr_data.season_details_modal = + Some(SeasonDetailsModal::default()); + app_arc + .lock() + .await + .data + .sonarr_data + .series + .set_items(vec![series()]); + app_arc + .lock() + .await + .data + .sonarr_data + .seasons + .set_items(vec![season()]); + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .season_history + .sort_asc = true; + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::SonarrHistoryWrapper(history) = network + .handle_sonarr_event(SonarrEvent::GetSeasonHistory(None)) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_history + .items, + expected_history_items + ); + assert!( + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_history + .sort_asc + ); + assert_eq!(history, response); + } + } + + #[tokio::test] + async fn test_handle_get_sonarr_season_history_event_uses_provided_series_id_and_season_number() { + let history_json = json!({"records": [{ + "id": 123, + "sourceTitle": "z episode", + "episodeId": 1007, + "quality": { "quality": { "name": "Bluray-1080p" } }, + "language": { "id": 1, "name": "English" }, + "date": "2024-02-10T07:28:45Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", + "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" + } + }, + { + "id": 456, + "sourceTitle": "A Episode", + "episodeId": 2001, + "quality": { "quality": { "name": "Bluray-1080p" } }, + "language": { "id": 1, "name": "English" }, + "date": "2024-02-10T07:28:45Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", + "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" + } + }]}); + let response: SonarrHistoryWrapper = serde_json::from_value(history_json.clone()).unwrap(); + let expected_history_items = vec![ + SonarrHistoryItem { + id: 123, + episode_id: 1007, + source_title: "z episode".into(), + ..history_item() + }, + SonarrHistoryItem { + id: 456, + episode_id: 2001, + source_title: "A Episode".into(), + ..history_item() + }, + ]; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(history_json), + None, + SonarrEvent::GetSeasonHistory(Some((2, 2))), + None, + Some("seriesId=2&seasonNumber=2"), + ) + .await; + app_arc.lock().await.data.sonarr_data.season_details_modal = + Some(SeasonDetailsModal::default()); + app_arc + .lock() + .await + .data + .sonarr_data + .series + .set_items(vec![series()]); + app_arc + .lock() + .await + .data + .sonarr_data + .seasons + .set_items(vec![season()]); + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .season_history + .sort_asc = true; + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::SonarrHistoryWrapper(history) = network + .handle_sonarr_event(SonarrEvent::GetSeasonHistory(Some((2, 2)))) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_history + .items, + expected_history_items + ); + assert!( + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_history + .sort_asc + ); + assert_eq!(history, response); + } + } + + #[tokio::test] + async fn test_handle_get_sonarr_season_history_event_empty_season_details_modal() { + let history_json = json!({"records": [{ + "id": 123, + "sourceTitle": "z episode", + "episodeId": 1007, + "quality": { "quality": { "name": "Bluray-1080p" } }, + "language": { "id": 1, "name": "English" }, + "date": "2024-02-10T07:28:45Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", + "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" + } + }, + { + "id": 456, + "sourceTitle": "A Episode", + "episodeId": 2001, + "quality": { "quality": { "name": "Bluray-1080p" } }, + "language": { "id": 1, "name": "English" }, + "date": "2024-02-10T07:28:45Z", + "eventType": "grabbed", + "data": { + "droppedPath": "/nfs/nzbget/completed/series/Coolness/something.cool.mkv", + "importedPath": "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv" + } + }]}); + let response: SonarrHistoryWrapper = serde_json::from_value(history_json.clone()).unwrap(); + let expected_history_items = vec![ + SonarrHistoryItem { + id: 123, + episode_id: 1007, + source_title: "z episode".into(), + ..history_item() + }, + SonarrHistoryItem { + id: 456, + episode_id: 2001, + source_title: "A Episode".into(), + ..history_item() + }, + ]; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(history_json), + None, + SonarrEvent::GetSeasonHistory(None), + None, + Some("seriesId=1&seasonNumber=1"), + ) + .await; + app_arc + .lock() + .await + .data + .sonarr_data + .series + .set_items(vec![series()]); + app_arc + .lock() + .await + .data + .sonarr_data + .seasons + .set_items(vec![season()]); + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::SonarrHistoryWrapper(history) = network + .handle_sonarr_event(SonarrEvent::GetSeasonHistory(None)) + .await + .unwrap() + { + async_server.assert_async().await; + assert!(app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .is_some()); + assert_eq!( + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_history + .items, + expected_history_items + ); + assert!( + !app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .season_history + .sort_asc + ); + assert_eq!(history, response); + } + } + #[tokio::test] async fn test_handle_get_season_releases_event() { let release_json = json!([