diff --git a/src/cli/sonarr/add_command_handler.rs b/src/cli/sonarr/add_command_handler.rs index c842d0d..ecb1d85 100644 --- a/src/cli/sonarr/add_command_handler.rs +++ b/src/cli/sonarr/add_command_handler.rs @@ -4,6 +4,8 @@ use anyhow::Result; use clap::{ArgAction, Subcommand}; use tokio::sync::Mutex; +use super::SonarrCommand; +use crate::models::servarr_models::AddRootFolderBody; use crate::{ app::App, cli::{CliCommandHandler, Command}, @@ -11,8 +13,6 @@ use crate::{ network::{sonarr_network::SonarrEvent, NetworkTrait}, }; -use super::SonarrCommand; - #[cfg(test)] #[path = "add_command_handler_tests.rs"] mod add_command_handler_tests; @@ -153,9 +153,10 @@ impl<'a, 'b> CliCommandHandler<'a, 'b, SonarrAddCommand> for SonarrAddCommandHan serde_json::to_string_pretty(&resp)? } SonarrAddCommand::RootFolder { root_folder_path } => { + let add_root_folder_body = AddRootFolderBody { path: root_folder_path }; let resp = self .network - .handle_network_event(SonarrEvent::AddRootFolder(Some(root_folder_path)).into()) + .handle_network_event(SonarrEvent::AddRootFolder(add_root_folder_body).into()) .await?; serde_json::to_string_pretty(&resp)? } diff --git a/src/cli/sonarr/add_command_handler_tests.rs b/src/cli/sonarr/add_command_handler_tests.rs index 8018b9e..789fd96 100644 --- a/src/cli/sonarr/add_command_handler_tests.rs +++ b/src/cli/sonarr/add_command_handler_tests.rs @@ -469,17 +469,19 @@ mod tests { use super::*; use mockall::predicate::eq; + use crate::models::servarr_models::AddRootFolderBody; use serde_json::json; use tokio::sync::Mutex; #[tokio::test] async fn test_handle_add_root_folder_command() { let expected_root_folder_path = "/nfs/test".to_owned(); + let expected_add_root_folder_body = AddRootFolderBody { path: expected_root_folder_path.clone() }; let mut mock_network = MockNetworkTrait::new(); mock_network .expect_handle_network_event() .with(eq::( - SonarrEvent::AddRootFolder(Some(expected_root_folder_path.clone())).into(), + SonarrEvent::AddRootFolder(expected_add_root_folder_body.clone()).into(), )) .times(1) .returning(|_| { diff --git a/src/handlers/sonarr_handlers/root_folders/mod.rs b/src/handlers/sonarr_handlers/root_folders/mod.rs index 6e1d850..34d7d24 100644 --- a/src/handlers/sonarr_handlers/root_folders/mod.rs +++ b/src/handlers/sonarr_handlers/root_folders/mod.rs @@ -5,7 +5,7 @@ use crate::handlers::sonarr_handlers::handle_change_tab_left_right_keys; use crate::handlers::table_handler::TableHandlingConfig; use crate::handlers::{handle_clear_errors, handle_prompt_toggle, KeyEventHandler}; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ROOT_FOLDERS_BLOCKS}; -use crate::models::servarr_models::RootFolder; +use crate::models::servarr_models::{AddRootFolderBody, RootFolder}; use crate::models::HorizontallyScrollableText; use crate::network::sonarr_network::SonarrEvent; use crate::{handle_table_events, handle_text_box_keys, handle_text_box_left_right_keys}; @@ -28,6 +28,12 @@ impl<'a, 'b> RootFoldersHandler<'a, 'b> { self.app.data.sonarr_data.root_folders, RootFolder ); + + fn build_add_root_folder_body(&mut self) -> AddRootFolderBody { + let root_folder = self.app.data.sonarr_data.edit_root_folder.as_ref().unwrap().text.clone(); + self.app.data.sonarr_data.edit_root_folder = None; + AddRootFolderBody { path: root_folder } + } } impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for RootFoldersHandler<'a, 'b> { @@ -140,7 +146,7 @@ impl<'a, 'b> KeyEventHandler<'a, 'b, ActiveSonarrBlock> for RootFoldersHandler<' .text .is_empty() => { - self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::AddRootFolder(None)); + self.app.data.sonarr_data.prompt_confirm_action = Some(SonarrEvent::AddRootFolder(self.build_add_root_folder_body())); self.app.data.sonarr_data.prompt_confirm = true; self.app.should_ignore_quit_key = false; self.app.pop_navigation_stack(); diff --git a/src/handlers/sonarr_handlers/root_folders/root_folders_handler_tests.rs b/src/handlers/sonarr_handlers/root_folders/root_folders_handler_tests.rs index 1578234..13d26eb 100644 --- a/src/handlers/sonarr_handlers/root_folders/root_folders_handler_tests.rs +++ b/src/handlers/sonarr_handlers/root_folders/root_folders_handler_tests.rs @@ -1,5 +1,6 @@ #[cfg(test)] mod tests { + use pretty_assertions::assert_eq; use strum::IntoEnumIterator; use crate::app::key_binding::DEFAULT_KEYBINDINGS; @@ -8,7 +9,7 @@ mod tests { use crate::handlers::sonarr_handlers::root_folders::RootFoldersHandler; use crate::handlers::KeyEventHandler; use crate::models::servarr_data::sonarr::sonarr_data::{ActiveSonarrBlock, ROOT_FOLDERS_BLOCKS}; - use crate::models::servarr_models::RootFolder; + use crate::models::servarr_models::{AddRootFolderBody, RootFolder}; use crate::models::HorizontallyScrollableText; mod test_handle_home_end { @@ -257,6 +258,7 @@ mod tests { #[test] fn test_add_root_folder_prompt_confirm_submit() { let mut app = App::default(); + let expected_add_root_folder_body = AddRootFolderBody { path: "Test".to_owned() }; app .data .sonarr_data @@ -280,12 +282,13 @@ mod tests { assert!(!app.should_ignore_quit_key); assert_eq!( app.data.sonarr_data.prompt_confirm_action, - Some(SonarrEvent::AddRootFolder(None)) + Some(SonarrEvent::AddRootFolder(expected_add_root_folder_body)) ); assert_eq!( app.get_current_route(), ActiveSonarrBlock::RootFolders.into() ); + assert!(app.data.sonarr_data.edit_root_folder.is_none()); } #[test] @@ -649,6 +652,23 @@ mod tests { }) } + #[test] + fn test_build_add_root_folder_body() { + let mut app = App::default(); + app.data.sonarr_data.edit_root_folder = Some("/nfs/test".into()); + let expected_add_root_folder_body = AddRootFolderBody { path: "/nfs/test".to_owned() }; + + let root_folder = RootFoldersHandler::with( + DEFAULT_KEYBINDINGS.esc.key, + &mut app, + ActiveSonarrBlock::AddRootFolderPrompt, + None, + ).build_add_root_folder_body(); + + assert_eq!(root_folder, expected_add_root_folder_body); + assert!(app.data.sonarr_data.edit_root_folder.is_none()); + } + #[test] fn test_root_folders_handler_not_ready_when_loading() { let mut app = App::default(); diff --git a/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs b/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs index 226e2dd..6e84c6b 100644 --- a/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs +++ b/src/handlers/sonarr_handlers/sonarr_handler_test_utils.rs @@ -1,6 +1,11 @@ #[cfg(test)] #[macro_use] -mod utils { +pub(in crate::handlers::sonarr_handlers) mod utils { + use crate::models::servarr_models::{Indexer, IndexerField, Language, Quality, QualityWrapper, RootFolder}; + use crate::models::sonarr_models::{AddSeriesSearchResult, AddSeriesSearchResultStatistics, BlocklistItem, DownloadRecord, DownloadStatus, DownloadsResponse, Episode, EpisodeFile, IndexerSettings, MediaInfo, Rating, Season, SeasonStatistics, Series, SeriesStatistics, SeriesStatus, SeriesType, SonarrHistoryData, SonarrHistoryEventType, SonarrHistoryItem, SonarrRelease}; + use crate::models::HorizontallyScrollableText; + use chrono::DateTime; + use serde_json::{json, Number}; #[macro_export] macro_rules! test_edit_series_key { @@ -154,4 +159,301 @@ mod utils { ); }; } + + fn add_series_search_result() -> AddSeriesSearchResult { + AddSeriesSearchResult { + tvdb_id: 1234, + title: HorizontallyScrollableText::from("Test"), + status: Some("continuing".to_owned()), + ended: false, + overview: Some("New series blah blah blah".to_owned()), + genres: genres(), + year: 2023, + network: Some("Prime Video".to_owned()), + runtime: 60, + ratings: Some(rating()), + statistics: Some(add_series_search_result_statistics()), + } + } + + fn add_series_search_result_statistics() -> AddSeriesSearchResultStatistics { + AddSeriesSearchResultStatistics { season_count: 3 } + } + + fn blocklist_item() -> BlocklistItem { + BlocklistItem { + id: 1, + series_id: 1, + series_title: None, + episode_ids: vec![Number::from(1)], + source_title: "Test Source Title".to_owned(), + languages: vec![language()], + quality: quality_wrapper(), + date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), + protocol: "usenet".to_owned(), + indexer: "NZBgeek (Prowlarr)".to_owned(), + message: "test message".to_owned(), + } + } + + fn download_record() -> DownloadRecord { + DownloadRecord { + title: "Test Download Title".to_owned(), + status: DownloadStatus::Downloading, + id: 1, + episode_id: 1, + size: 3543348019f64, + sizeleft: 1771674009f64, + output_path: Some(HorizontallyScrollableText::from( + "/nfs/tv/Test show/season 1/", + )), + indexer: "kickass torrents".to_owned(), + download_client: Some("transmission".to_owned()), + } + } + + fn downloads_response() -> DownloadsResponse { + DownloadsResponse { + records: vec![download_record()], + } + } + + fn episode() -> Episode { + Episode { + id: 1, + series_id: 1, + tvdb_id: 1234, + episode_file_id: 1, + season_number: 1, + episode_number: 1, + title: "Something cool".to_owned(), + air_date_utc: Some(DateTime::from( + DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap(), + )), + overview: Some("Okay so this one time at band camp...".to_owned()), + has_file: true, + monitored: true, + episode_file: Some(episode_file()), + } + } + + 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(), + languages: vec![language()], + date_added: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), + media_info: Some(media_info()), + } + } + + fn genres() -> Vec { + vec!["cool".to_owned(), "family".to_owned(), "fun".to_owned()] + } + + fn history_data() -> SonarrHistoryData { + SonarrHistoryData { + dropped_path: Some("/nfs/nzbget/completed/series/Coolness/something.cool.mkv".to_owned()), + imported_path: Some( + "/nfs/tv/Coolness/Season 1/Coolness - S01E01 - Something Cool Bluray-1080p.mkv".to_owned(), + ), + ..SonarrHistoryData::default() + } + } + + fn history_item() -> SonarrHistoryItem { + SonarrHistoryItem { + id: 1, + source_title: "Test source".into(), + episode_id: 1, + quality: quality_wrapper(), + languages: vec![language()], + date: DateTime::from(DateTime::parse_from_rfc3339("2024-02-10T07:28:45Z").unwrap()), + event_type: SonarrHistoryEventType::Grabbed, + data: history_data(), + } + } + + fn indexer() -> Indexer { + Indexer { + enable_rss: true, + enable_automatic_search: true, + enable_interactive_search: true, + supports_rss: true, + supports_search: true, + protocol: "torrent".to_owned(), + priority: 25, + download_client_id: 0, + name: Some("Test Indexer".to_owned()), + implementation_name: Some("Torznab".to_owned()), + implementation: Some("Torznab".to_owned()), + config_contract: Some("TorznabSettings".to_owned()), + tags: vec![Number::from(1)], + id: 1, + fields: Some(vec![ + IndexerField { + name: Some("baseUrl".to_owned()), + value: Some(json!("https://test.com")), + }, + IndexerField { + name: Some("apiKey".to_owned()), + value: Some(json!("")), + }, + IndexerField { + name: Some("seedCriteria.seedRatio".to_owned()), + value: Some(json!("1.2")), + }, + ]), + } + } + + fn indexer_settings() -> IndexerSettings { + IndexerSettings { + id: 1, + minimum_age: 1, + retention: 1, + maximum_size: 12345, + rss_sync_interval: 60, + } + } + + fn language() -> Language { + Language { + id: 1, + name: "English".to_owned(), + } + } + + fn media_info() -> MediaInfo { + MediaInfo { + audio_bitrate: 0, + audio_channels: Number::from_f64(7.1).unwrap(), + audio_codec: Some("AAC".to_owned()), + audio_languages: Some("eng".to_owned()), + audio_stream_count: 1, + video_bit_depth: 10, + video_bitrate: 0, + video_codec: "x265".to_owned(), + video_fps: Number::from_f64(23.976).unwrap(), + resolution: "1920x1080".to_owned(), + run_time: "23:51".to_owned(), + scan_type: "Progressive".to_owned(), + subtitles: Some("English".to_owned()), + } + } + fn quality() -> Quality { + Quality { + name: "Bluray-1080p".to_owned(), + } + } + + fn quality_wrapper() -> QualityWrapper { + QualityWrapper { quality: quality() } + } + + fn rating() -> Rating { + Rating { + votes: 406744, + value: 8.4, + } + } + + fn season() -> Season { + Season { + title: None, + season_number: 1, + monitored: true, + statistics: season_statistics(), + } + } + + fn season_statistics() -> SeasonStatistics { + SeasonStatistics { + previous_airing: Some(DateTime::from( + DateTime::parse_from_rfc3339("2022-10-24T01:00:00Z").unwrap(), + )), + next_airing: None, + episode_file_count: 10, + episode_count: 10, + total_episode_count: 10, + size_on_disk: 36708563419, + percent_of_episodes: 100.0, + } + } + + fn series() -> Series { + Series { + title: "Test".to_owned().into(), + status: SeriesStatus::Continuing, + ended: false, + overview: Some("Blah blah blah".to_owned()), + network: Some("HBO".to_owned()), + seasons: Some(vec![season()]), + year: 2022, + path: "/nfs/tv/Test".to_owned(), + quality_profile_id: 6, + language_profile_id: 1, + season_folder: true, + monitored: true, + runtime: 63, + tvdb_id: 371572, + series_type: SeriesType::Standard, + certification: Some("TV-MA".to_owned()), + genres: vec!["cool".to_owned(), "family".to_owned(), "fun".to_owned()], + tags: vec![Number::from(3)], + ratings: rating(), + statistics: Some(series_statistics()), + id: 1, + } + } + + fn series_statistics() -> SeriesStatistics { + SeriesStatistics { + season_count: 2, + episode_file_count: 18, + episode_count: 18, + total_episode_count: 50, + size_on_disk: 63894022699, + percent_of_episodes: 100.0, + } + } + + fn rejections() -> Vec { + vec![ + "Unknown quality profile".to_owned(), + "Release is already mapped".to_owned(), + ] + } + + fn release() -> SonarrRelease { + SonarrRelease { + guid: "1234".to_owned(), + protocol: "torrent".to_owned(), + age: 1, + title: HorizontallyScrollableText::from("Test Release"), + indexer: "kickass torrents".to_owned(), + indexer_id: 2, + size: 1234, + rejected: true, + rejections: Some(rejections()), + seeders: Some(Number::from(2)), + leechers: Some(Number::from(1)), + languages: Some(vec![language()]), + quality: quality_wrapper(), + full_season: false, + } + } + + fn root_folder() -> RootFolder { + RootFolder { + id: 1, + path: "/nfs".to_owned(), + accessible: true, + free_space: 219902325555200, + unmapped_folders: None, + } + } } diff --git a/src/network/sonarr_network.rs b/src/network/sonarr_network.rs index 2dc10be..9f905eb 100644 --- a/src/network/sonarr_network.rs +++ b/src/network/sonarr_network.rs @@ -39,7 +39,7 @@ mod sonarr_network_tests; #[derive(Debug, Eq, PartialEq, Clone)] pub enum SonarrEvent { - AddRootFolder(Option), + AddRootFolder(AddRootFolderBody), AddSeries(Option), AddTag(String), ClearBlocklist, @@ -357,31 +357,14 @@ impl<'a, 'b> Network<'a, 'b> { } } - async fn add_sonarr_root_folder(&mut self, root_folder: Option) -> Result { + async fn add_sonarr_root_folder(&mut self, add_root_folder_body: AddRootFolderBody) -> Result { info!("Adding new root folder to Sonarr"); - let event = SonarrEvent::AddRootFolder(None); - let body = if let Some(path) = root_folder { - AddRootFolderBody { path } - } else { - let mut app = self.app.lock().await; - let path = app - .data - .sonarr_data - .edit_root_folder - .as_ref() - .unwrap() - .text - .clone(); + let event = SonarrEvent::AddRootFolder(add_root_folder_body.clone()); - app.data.sonarr_data.edit_root_folder = None; - - AddRootFolderBody { path } - }; - - debug!("Add root folder body: {body:?}"); + debug!("Add root folder body: {add_root_folder_body:?}"); let request_props = self - .request_props_from(event, RequestMethod::Post, Some(body), None, None) + .request_props_from(event, RequestMethod::Post, Some(add_root_folder_body), None, None) .await; self diff --git a/src/network/sonarr_network_tests.rs b/src/network/sonarr_network_tests.rs index ee9425d..c9865f0 100644 --- a/src/network/sonarr_network_tests.rs +++ b/src/network/sonarr_network_tests.rs @@ -18,7 +18,7 @@ mod test { use crate::models::sonarr_models::{ AddSeriesBody, AddSeriesOptions, AddSeriesSearchResult, AddSeriesSearchResultStatistics, DownloadStatus, EditSeriesParams, IndexerSettings, MonitorEpisodeBody, SeriesMonitor, - SonarrHistoryEventType, + SonarrHistoryEventType }; use crate::app::{App, ServarrConfig}; @@ -28,10 +28,7 @@ mod test { AddSeriesModal, EditSeriesModal, EpisodeDetailsModal, SeasonDetailsModal, }; use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock; - use crate::models::servarr_models::{ - DiskSpace, EditIndexerParams, HostConfig, Indexer, IndexerField, Language, LogResponse, - Quality, QualityProfile, QualityWrapper, QueueEvent, RootFolder, SecurityConfig, Tag, Update, - }; + use crate::models::servarr_models::{AddRootFolderBody, DiskSpace, EditIndexerParams, HostConfig, Indexer, IndexerField, Language, LogResponse, Quality, QualityProfile, QualityWrapper, QueueEvent, RootFolder, SecurityConfig, Tag, Update}; use crate::models::sonarr_models::{ BlocklistItem, DeleteSeriesParams, DownloadRecord, DownloadsResponse, Episode, EpisodeFile, MediaInfo, SonarrRelease, SonarrReleaseDownloadBody, SonarrTaskName, @@ -250,7 +247,7 @@ mod test { #[values( SonarrEvent::GetRootFolders, SonarrEvent::DeleteRootFolder(None), - SonarrEvent::AddRootFolder(None) + SonarrEvent::AddRootFolder(AddRootFolderBody::default()) )] event: SonarrEvent, ) { @@ -310,6 +307,7 @@ mod test { #[tokio::test] async fn test_handle_add_sonarr_root_folder_event() { + let expected_add_root_folder_body = AddRootFolderBody { path: "/nfs/test".to_owned() }; let (async_server, app_arc, _server) = mock_servarr_api( RequestMethod::Post, Some(json!({ @@ -317,49 +315,15 @@ mod test { })), Some(json!({})), None, - SonarrEvent::AddRootFolder(None), + SonarrEvent::AddRootFolder(expected_add_root_folder_body.clone()), None, None, ) .await; - - app_arc.lock().await.data.sonarr_data.edit_root_folder = Some("/nfs/test".into()); let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); assert!(network - .handle_sonarr_event(SonarrEvent::AddRootFolder(None)) - .await - .is_ok()); - - async_server.assert_async().await; - assert!(app_arc - .lock() - .await - .data - .sonarr_data - .edit_root_folder - .is_none()); - } - - #[tokio::test] - async fn test_handle_add_sonarr_root_folder_event_uses_provided_path() { - let (async_server, app_arc, _server) = mock_servarr_api( - RequestMethod::Post, - Some(json!({ - "path": "/test/test" - })), - Some(json!({})), - None, - SonarrEvent::AddRootFolder(None), - None, - None, - ) - .await; - - let mut network = Network::new(&app_arc, CancellationToken::new(), Client::new()); - - assert!(network - .handle_sonarr_event(SonarrEvent::AddRootFolder(Some("/test/test".to_owned()))) + .handle_sonarr_event(SonarrEvent::AddRootFolder(expected_add_root_folder_body)) .await .is_ok());