From 75c4fcbb9e0b6a64354b876c5864f7880ebb06e7 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Tue, 10 Dec 2024 16:22:02 -0700 Subject: [PATCH] feat(network): Support for fetching all episode files for a given series --- src/models/servarr_data/sonarr/modals.rs | 4 +- .../servarr_data/sonarr/modals_tests.rs | 1 + src/models/sonarr_models.rs | 5 + src/models/sonarr_models_tests.rs | 17 +- src/network/sonarr_network.rs | 46 ++++- src/network/sonarr_network_tests.rs | 171 +++++++++++++++++- src/ui/sonarr_ui/library/season_details_ui.rs | 0 .../library/season_details_ui_tests.rs | 0 8 files changed, 237 insertions(+), 7 deletions(-) create mode 100644 src/ui/sonarr_ui/library/season_details_ui.rs create mode 100644 src/ui/sonarr_ui/library/season_details_ui_tests.rs diff --git a/src/models/servarr_data/sonarr/modals.rs b/src/models/servarr_data/sonarr/modals.rs index ad08bc7..7c90581 100644 --- a/src/models/servarr_data/sonarr/modals.rs +++ b/src/models/servarr_data/sonarr/modals.rs @@ -19,7 +19,7 @@ use crate::{ HorizontallyScrollableText, ScrollableText, TabRoute, TabState, }, }; - +use crate::models::sonarr_models::EpisodeFile; use super::sonarr_data::{ActiveSonarrBlock, SonarrData}; #[cfg(test)] @@ -310,6 +310,7 @@ impl Default for EpisodeDetailsModal { pub struct SeasonDetailsModal { pub episodes: StatefulTable, + pub episode_files: StatefulTable, pub episode_details_modal: Option, pub season_history: StatefulTable, pub season_releases: StatefulTable, @@ -321,6 +322,7 @@ impl Default for SeasonDetailsModal { SeasonDetailsModal { episodes: StatefulTable::default(), episode_details_modal: None, + episode_files: StatefulTable::default(), season_releases: StatefulTable::default(), season_history: StatefulTable::default(), season_details_tabs: TabState::new(vec![ diff --git a/src/models/servarr_data/sonarr/modals_tests.rs b/src/models/servarr_data/sonarr/modals_tests.rs index e35ba5b..e06b06e 100644 --- a/src/models/servarr_data/sonarr/modals_tests.rs +++ b/src/models/servarr_data/sonarr/modals_tests.rs @@ -322,6 +322,7 @@ mod tests { assert!(season_details_modal.episodes.is_empty()); assert!(season_details_modal.episode_details_modal.is_none()); + assert!(season_details_modal.episode_files.is_empty()); assert!(season_details_modal.season_releases.is_empty()); assert!(season_details_modal.season_history.is_empty()); diff --git a/src/models/sonarr_models.rs b/src/models/sonarr_models.rs index b133d83..7c23e6d 100644 --- a/src/models/sonarr_models.rs +++ b/src/models/sonarr_models.rs @@ -176,11 +176,14 @@ impl Display for Episode { #[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct EpisodeFile { + #[serde(deserialize_with = "super::from_i64")] + pub id: i64, pub relative_path: String, pub path: String, #[serde(deserialize_with = "super::from_i64")] pub size: i64, pub language: Language, + pub quality: QualityWrapper, pub date_added: DateTime, pub media_info: Option, } @@ -626,6 +629,7 @@ pub enum SonarrSerdeable { DiskSpaces(Vec), Episode(Episode), Episodes(Vec), + EpisodeFiles(Vec), HostConfig(HostConfig), IndexerSettings(IndexerSettings), Indexers(Vec), @@ -669,6 +673,7 @@ serde_enum_from!( DiskSpaces(Vec), Episode(Episode), Episodes(Vec), + EpisodeFiles(Vec), HostConfig(HostConfig), IndexerSettings(IndexerSettings), Indexers(Vec), diff --git a/src/models/sonarr_models_tests.rs b/src/models/sonarr_models_tests.rs index 87ac76d..2ba98eb 100644 --- a/src/models/sonarr_models_tests.rs +++ b/src/models/sonarr_models_tests.rs @@ -11,7 +11,7 @@ mod tests { }, sonarr_models::{ AddSeriesSearchResult, BlocklistItem, BlocklistResponse, DownloadRecord, DownloadsResponse, - Episode, IndexerSettings, Series, SeriesMonitor, SeriesStatus, SeriesType, + Episode, EpisodeFile, IndexerSettings, Series, SeriesMonitor, SeriesStatus, SeriesType, SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease, SonarrSerdeable, SonarrTask, SonarrTaskName, SystemStatus, }, @@ -236,6 +236,21 @@ mod tests { assert_eq!(sonarr_serdeable, SonarrSerdeable::Episodes(episodes)); } + #[test] + fn test_sonarr_serdeable_from_episode_files() { + let episode_files = vec![EpisodeFile { + id: 1, + ..EpisodeFile::default() + }]; + + let sonarr_serdeable: SonarrSerdeable = episode_files.clone().into(); + + assert_eq!( + sonarr_serdeable, + SonarrSerdeable::EpisodeFiles(episode_files) + ); + } + #[test] fn test_sonarr_serdeable_from_host_config() { let host_config = HostConfig { diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index a782071..48e75b1 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -21,9 +21,9 @@ use crate::{ sonarr_models::{ AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult, BlocklistItem, BlocklistResponse, DeleteSeriesParams, DownloadRecord, DownloadsResponse, EditSeriesParams, Episode, - IndexerSettings, Series, SonarrCommandBody, SonarrHistoryItem, SonarrHistoryWrapper, - SonarrRelease, SonarrReleaseDownloadBody, SonarrSerdeable, SonarrTask, SonarrTaskName, - SystemStatus, + EpisodeFile, IndexerSettings, Series, SonarrCommandBody, SonarrHistoryItem, + SonarrHistoryWrapper, SonarrRelease, SonarrReleaseDownloadBody, SonarrSerdeable, SonarrTask, + SonarrTaskName, SystemStatus, }, stateful_table::StatefulTable, HorizontallyScrollableText, Route, Scrollable, ScrollableText, @@ -62,6 +62,7 @@ pub enum SonarrEvent { GetIndexers, GetEpisodeDetails(Option), GetEpisodes(Option), + GetEpisodeFiles(Option), GetEpisodeHistory(Option), GetLanguageProfiles, GetLogs(Option), @@ -104,7 +105,7 @@ impl NetworkResource for SonarrEvent { SonarrEvent::GetAllIndexerSettings | SonarrEvent::EditAllIndexerSettings(_) => { "/config/indexer" } - SonarrEvent::DeleteEpisodeFile(_) => "/episodefile", + SonarrEvent::GetEpisodeFiles(_) | SonarrEvent::DeleteEpisodeFile(_) => "/episodefile", SonarrEvent::GetBlocklist => "/blocklist?page=1&pageSize=10000", SonarrEvent::GetDownloads | SonarrEvent::DeleteDownload(_) => "/queue", SonarrEvent::GetEpisodes(_) | SonarrEvent::GetEpisodeDetails(_) => "/episode", @@ -225,6 +226,10 @@ impl<'a, 'b> Network<'a, 'b> { .get_episodes(series_id) .await .map(SonarrSerdeable::from), + SonarrEvent::GetEpisodeFiles(series_id) => self + .get_episode_files(series_id) + .await + .map(SonarrSerdeable::from), SonarrEvent::GetEpisodeDetails(episode_id) => self .get_episode_details(episode_id) .await @@ -1420,6 +1425,39 @@ impl<'a, 'b> Network<'a, 'b> { .await } + async fn get_episode_files(&mut self, series_id: Option) -> Result> { + let event = SonarrEvent::GetEpisodeFiles(series_id); + let (id, series_id_param) = self.extract_series_id(series_id).await; + info!("Fetching episodes files for Sonarr series with ID: {id}"); + + let request_props = self + .request_props_from( + event, + RequestMethod::Get, + None::<()>, + None, + Some(series_id_param), + ) + .await; + + self + .handle_request::<(), Vec>(request_props, |episode_file_vec, mut app| { + if app.data.sonarr_data.season_details_modal.is_none() { + app.data.sonarr_data.season_details_modal = Some(SeasonDetailsModal::default()); + } + + app + .data + .sonarr_data + .season_details_modal + .as_mut() + .unwrap() + .episode_files + .set_items(episode_file_vec); + }) + .await + } + async fn get_sonarr_episode_history( &mut self, episode_id: Option, diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index dd1e27b..42db1df 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -108,6 +108,7 @@ mod test { "airDateUtc": "2024-02-10T07:28:45Z", "overview": "Okay so this one time at band camp...", "episodeFile": { + "id": 1, "relativePath": "/season 1/episode 1.mkv", "path": "/nfs/tv/series/season 1/episode 1.mkv", "size": 3543348019, @@ -265,10 +266,20 @@ mod test { assert_str_eq!(event.resource(), "/release"); } + #[rstest] + fn test_resource_episode_file( + #[values( + SonarrEvent::GetEpisodeFiles(None), + SonarrEvent::DeleteEpisodeFile(None) + )] + event: SonarrEvent, + ) { + assert_str_eq!(event.resource(), "/episodefile"); + } + #[rstest] #[case(SonarrEvent::ClearBlocklist, "/blocklist/bulk")] #[case(SonarrEvent::DeleteBlocklistItem(None), "/blocklist")] - #[case(SonarrEvent::DeleteEpisodeFile(None), "/episodefile")] #[case(SonarrEvent::HealthCheck, "/health")] #[case(SonarrEvent::GetBlocklist, "/blocklist?page=1&pageSize=10000")] #[case(SonarrEvent::GetDiskSpace, "/diskspace")] @@ -2589,6 +2600,162 @@ mod test { } } + #[tokio::test] + async fn test_handle_get_episode_files_event() { + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(json!([episode_file()])), + None, + SonarrEvent::GetEpisodeFiles(None), + None, + Some("seriesId=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 { + id: 1, + ..Series::default() + }]); + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::EpisodeFiles(episode_files) = network + .handle_sonarr_event(SonarrEvent::GetEpisodeFiles(None)) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_files + .items, + vec![episode_file()] + ); + assert_eq!(episode_files, vec![episode_file()]); + } + } + + #[tokio::test] + async fn test_handle_get_episode_files_event_empty_season_details_modal() { + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(json!([episode_file()])), + None, + SonarrEvent::GetEpisodeFiles(None), + None, + Some("seriesId=1"), + ) + .await; + app_arc + .lock() + .await + .data + .sonarr_data + .series + .set_items(vec![Series { + id: 1, + ..Series::default() + }]); + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::EpisodeFiles(episode_files) = network + .handle_sonarr_event(SonarrEvent::GetEpisodeFiles(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() + .episode_files + .items, + vec![episode_file()] + ); + assert_eq!(episode_files, vec![episode_file()]); + } + } + + #[tokio::test] + async fn test_handle_get_episode_files_event_uses_provided_series_id() { + let episode_file = EpisodeFile { + id: 2, + ..episode_file() + }; + let (async_server, app_arc, _server) = mock_servarr_api( + RequestMethod::Get, + None, + Some(json!([episode_file.clone()])), + None, + SonarrEvent::GetEpisodeFiles(Some(2)), + None, + Some("seriesId=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 { + id: 1, + ..Series::default() + }]); + let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); + + if let SonarrSerdeable::EpisodeFiles(episode_files) = network + .handle_sonarr_event(SonarrEvent::GetEpisodeFiles(Some(2))) + .await + .unwrap() + { + async_server.assert_async().await; + assert_eq!( + app_arc + .lock() + .await + .data + .sonarr_data + .season_details_modal + .as_ref() + .unwrap() + .episode_files + .items, + vec![episode_file.clone()] + ); + assert_eq!(episode_files, vec![episode_file]); + } + } + #[tokio::test] async fn test_handle_get_sonarr_host_config_event() { let host_config_response = json!({ @@ -7104,9 +7271,11 @@ mod test { fn episode_file() -> EpisodeFile { EpisodeFile { + id: 1, relative_path: "/season 1/episode 1.mkv".to_owned(), path: "/nfs/tv/series/season 1/episode 1.mkv".to_owned(), size: 3543348019, + quality: quality_wrapper(), language: language(), date_added: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), media_info: Some(media_info()), diff --git a/src/ui/sonarr_ui/library/season_details_ui.rs b/src/ui/sonarr_ui/library/season_details_ui.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/sonarr_ui/library/season_details_ui_tests.rs b/src/ui/sonarr_ui/library/season_details_ui_tests.rs new file mode 100644 index 0000000..e69de29